/* ==========================================================================
   home.css — homepage: Obys vertical works strip
   Loads after frame.css; page-scoped styles only. Ink is the voice, the
   orange-red accent stays a SIGNAL (numbers, hovers, underlines).

   The halls section renders two ways from one markup:
     - default (no JS, or prefers-reduced-motion): .works-stage is
       display:none and .works-static shows four stacked
       [image | name | category | intro | link] rows — the complete page;
     - html.works-live (added by home.js only when init succeeds): the
       section becomes a 500vh track with a sticky 100svh stage replicating
       the obys.agency composition — a centered vertical filmstrip, names
       down the left edge, one info column (index / category / intro / CTA)
       between names and strip, giant square brackets around the current
       frame, the studio blurb pinned top-right and a receding three-slot
       "next up" stack on the right. home.js writes ONE transform per frame
       (the strip, Y only); every state change is a class toggle animated
       by CSS (opacity / visibility / color / scale), never by the rAF loop.
   ========================================================================== */

.works {
  /* The strip geometry, echoed by every anchored sibling. 2026-10-15: the
     filmstrip grew again to 26vw (intro2: image-first composition);
     brackets, info column and preview stack all key off this one var. */
  --works-w: clamp(260px, 26vw, 520px);
  /* Bracket glyph size. Archivo Black square-bracket ink measures 0.897em
     tall (headless render), so ink ~= 0.70 x --works-w ~= the frame's
     height (0.75 x --works-w): the brackets hug the current image. */
  --works-paren-fs: min(34vh, calc(var(--works-w) * 0.78));
  /* Preview stack: the first (widest) slot; later slots scale off it. */
  --works-next-w: clamp(110px, 13vw, 190px);
  /* The uniform small card from slot 4 on: capped at 0.58 x the first slot
     so the receding stagger stays monotonic (is-1 > is-2 > is-3 >= small);
     its 4:3 box lands ~72-88px tall on common desktops. */
  --works-next-small: clamp(72px, calc(var(--works-next-w) * 0.58), 170px);
  /* Info column keeps this much air off the strip: the left bracket lives
     in that gap (its 5vw anchor + one glyph box, 0.389em, + ~0.05em air).
     Narrow desktop re-tunes both terms — see the <1100px block below. */
  --works-info-gap: calc(5vw + var(--works-paren-fs) * 0.44);
}

/* ---- Works blurb ------------------------------------------------------------
   Static: a plain intro block (replaces the old intro line). Live: the aside
   becomes a ZERO-HEIGHT sticky shim — it pins for the whole 500vh without
   costing flow height, so the stage's exact 100svh scroll math is preserved —
   and the inner box is absolutely positioned under the header, right edge. */
.works-blurb {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-4);
  padding-block-start: var(--space-8);
}

@media (min-width: 720px) {
  .works-blurb {
    padding-inline: var(--space-6);
  }
}

.works-blurb-in {
  display: grid;
  gap: var(--space-2);
  max-width: 40em;
  color: var(--text-dim);
  text-wrap: pretty;
}

/* ---- Halls, static state: four stacked rows (no JS / reduced motion) --------
   Full-bleed section, rows aligned to the container grid via the same
   max-width + gutters as .container. */
.works-static .ws-row {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-4);
  padding-block: var(--space-6);
  border-bottom: 1px solid var(--border-color);
  display: grid;
  gap: var(--space-4) var(--space-8);
  align-items: center;
}

@media (min-width: 720px) {
  .works-static .ws-row {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    padding-inline: var(--space-6);
    padding-block: var(--space-8);
  }
}

/* The work window: sharp ink frame, 4/3 so the 600x450 sources fit with
   zero layout shift; inset pure-white hairline keeps photo edges crisp. */
.ws-window {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
}

.ws-window::after,
.works-item::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.08); /* pure-white image edge */
}

.ws-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Hover zoom is static-state only: live mode owns the strip transform, a
   transition here would fight nothing but keep parity with the old page. */
html:not(.works-live) .ws-img {
  transition: transform var(--dur-base) var(--ease-out);
}

@media (hover: hover) {
  html:not(.works-live) .ws-window:hover .ws-img {
    transform: scale(1.03);
  }
}

/* The intro column. One whole-block hit area: the CTA is a stretched link
   (::after covers .ws-info), so clicking anywhere on the copy navigates. */
.ws-info {
  position: relative;
  display: grid;
  gap: var(--space-2);
  justify-items: start;
}

.ws-meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
}

.ws-tags {
  color: var(--text-faint);
  text-transform: uppercase;
}

.ws-name {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(1.75rem, 1.1rem + 2.6vw, 3.25rem); /* 28 -> 52px */
  line-height: 1.05;
  text-wrap: balance;
  color: var(--text);
}

.ws-desc {
  max-width: 36em;
  color: var(--text-dim);
  text-wrap: pretty;
}

.ws-cta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ws-cta::after {
  content: "";
  position: absolute;
  inset: 0;
}

.ws-cta:focus-visible {
  outline: none;
}

.ws-cta:focus-visible::after {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

@media (hover: hover) {
  .ws-info:hover .ws-name {
    text-decoration: underline;
    text-decoration-color: var(--accent);
    text-decoration-thickness: 2px;
    text-underline-offset: 6px;
  }
}

/* Shared arrow nudge (static rows + live credits). */
.works-arr {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-out);
}

@media (hover: hover) {
  .works-cta:hover .works-arr {
    transform: translateX(3px);
  }
}

/* ==========================================================================
   WORKS LIVE — the pinned Obys stage (html.works-live, added by home.js).
   Section = 500svh of scroll, stage = sticky 100svh. The current hall parks
   dead center for a full viewport (hold), then the strip travels to the
   next (travel), etc. All per-frame motion is home.js's single translate3d
   on the strip; everything else is CSS transitions on class toggles.
   ========================================================================== */

.works-stage {
  display: none; /* the pinned stage exists only when home.js succeeds */
}

html.works-live .works {
  /* (N+1)*100svh, N = 4 halls: park / travel / park / travel / park ...
     home.js re-writes this inline from the derived N (items / 3), so CSS
     and JS cannot drift; the values here match the shipped 4-hall markup. */
  height: 500vh;
  height: calc(100svh * 5);
}

html.works-live .works-stage {
  display: block;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: clip;
}

html.works-live .works-static {
  display: none;
}

html.works-live .works-blurb {
  position: sticky;
  top: 0;
  height: 0; /* zero-height shim: pins without shifting the scroll math */
  max-width: none;
  margin: 0;
  padding: 0;
  overflow: visible;
  z-index: var(--z-raised);
}

html.works-live .works-blurb-in {
  position: absolute;
  top: calc(var(--header-height) + var(--space-3));
  /* 2026-10-15: pinned top-LEFT at every live desktop height — the right
     column now belongs to the full-height per-hall preview stack, so the
     old top-right / short-window-top-left split would collide with it. */
  right: auto;
  left: clamp(var(--space-4), 3vw, var(--space-8));
  width: clamp(11rem, 15vw, 16rem);
  max-width: none;
  font-size: var(--text-xs);
  line-height: var(--leading-body);
  color: var(--text);
}

/* ---- The strip: a centered vertical filmstop --------------------------------
   Centered horizontally via left + negative margin (transform stays free for
   home.js's per-frame translate3d, which is written on the Y axis only). */
html.works-live .works-strip {
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--works-w);
  margin-left: calc(var(--works-w) / -2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2); /* 8px, the obys inter-image gap */
  will-change: transform; /* per-frame rAF transform: the justified case */
}

.works-item {
  position: relative;
  display: block;
  flex: none;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-2);
}

.works-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  outline: 1px solid var(--border-color); /* pale cards stay legible dimmed */
  outline-offset: -1px;
  background: var(--surface-2);
  opacity: 0.45; /* neighbors recede; the current frame carries */
  transition: opacity 200ms var(--ease-out);
}

.works-item.is-cur img {
  opacity: 1;
}

/* ---- Left edge: the vertical list of work names ------------------------------ */
html.works-live .works-names {
  position: absolute;
  left: clamp(var(--space-2), 1vw, var(--space-4));
  top: 50%;
  translate: 0 -50%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  z-index: var(--z-raised);
}

.works-name {
  padding: 0;
  font-family: var(--font-mono);
  font-size: 15px; /* up from --text-xs: the name list is a first-class index */
  line-height: 1.7;
  letter-spacing: 0.04em;
  text-align: left;
  color: var(--text-dim);
  transition: color 200ms var(--ease-out);
}

.works-name.is-on {
  color: var(--text);
}

@media (hover: hover) {
  .works-name:hover {
    color: var(--accent-dim);
  }
}

/* ---- Stacked variant chunks (cat / credits / index) ---------------------------
   All three states occupy the same grid cell; the visible one fades in while
   hidden ones are visibility:hidden, so they leave both the tab order and
   the a11y tree (no ghost links). 180ms = the spec's 150-200ms band. */
.works-cat-chunk,
.works-credit,
.works-index-chunk {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out),
              visibility 0s var(--dur-base);
}

.works-cat-chunk.is-on,
.works-credit.is-on,
.works-index-chunk.is-on {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-base) var(--ease-out), visibility 0s;
}

/* ---- Left info column: index / category / intro + CTA -------------------------
   One stacked block between the name list and the strip; its right edge
   holds a bracket glyph of air off the filmstrip. The three rows keep the
   grid-area 1/1 chunk swap (visible chunk = .is-on, see the rule above);
   left-aligned, vertically centered. Narrow-desktop re-tunes live in the
   <1100px block after the bracket rules below (source order decides). */
html.works-live .works-info {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  right: calc(50% + var(--works-w) / 2 + var(--works-info-gap));
  width: clamp(150px, 15.5vw, 300px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
  text-align: left;
  z-index: var(--z-raised);
}

html.works-live .works-cat {
  display: grid;
  font-family: var(--font-mono);
  font-size: 17px;
  letter-spacing: 0.08em;
  color: var(--text);
}

html.works-live .works-credits {
  display: grid;
}

.works-credit {
  display: grid;
  gap: var(--space-2);
  justify-items: start;
}

.works-desc {
  font-size: 18px;
  line-height: 1.7;
  color: var(--text);
  text-wrap: pretty;
}

.works-cta {
  font-family: var(--font-mono);
  font-size: 16px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

html.works-live .works-index {
  display: grid;
  font-size: clamp(22px, 2vw, 30px);
  font-variant-numeric: tabular-nums; /* 01 -> 02 -> 03 -> 04 never shifts */
}

html.works-live .works-index .works-index-chunk {
  font-size: inherit; /* beat .index-num's xs: the column number is display-ish */
}

/* ---- The giant square brackets framing the current frame -----------------------
   2026-10-12: glyphs swapped from ( ) to [ ]. Headless-Chrome probe of the
   shipped Archivo Black (font-size 1000px, line-height 0): `[` ink is 897px
   tall / 273px wide (0.897em / 0.273em) with the ink center +72.5px below
   the box anchor — `( `measures identical vertically (897px, +72.5px), so
   only the width differs. translate compensates that offset exactly; the
   font-size formula keeps ink ~= 0.93 x the frame height at every viewport. */
html.works-live .works-paren {
  position: absolute;
  top: 50%;
  /* Optical: line-height:0 makes the box 0-height, so % translate is dead —
     pull the ink center back up onto the stage's midline in em instead. */
  translate: 0 -0.0725em;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--works-paren-fs);
  line-height: 0;
  color: var(--text);
  user-select: none;
  pointer-events: none;
}

html.works-live .works-paren--l {
  right: calc(50% + var(--works-w) / 2 + 6vw);
}

html.works-live .works-paren--r {
  left: calc(50% + var(--works-w) / 2 + 6vw);
}

/* Index change: one quick 0.92 -> 1 pulse (retriggered by home.js). The
   individual `scale` property composes with the centering `translate`. */
html.works-live .works-paren.is-pulse {
  animation: works-paren-pulse 240ms var(--ease-out);
}

@keyframes works-paren-pulse {
  from { scale: 0.92; }
  to { scale: 1; }
}

/* Narrow desktop: the giant brackets hug the strip tighter (3vw) and the
   info column slims down, so names, column and bracket share the left half
   without touching. This block must stay AFTER the base paren / info rules
   it overrides — equal specificity, so source order decides. */
@media (max-width: 1099.98px) {
  .works {
    --works-info-gap: calc(3vw + var(--works-paren-fs) * 0.44);
  }

  html.works-live .works-paren--l {
    right: calc(50% + var(--works-w) / 2 + 3vw);
  }

  html.works-live .works-paren--r {
    left: calc(50% + var(--works-w) / 2 + 3vw);
  }

  html.works-live .works-info {
    width: clamp(120px, 15vw, 150px);
  }
}

/* Below ~940px the name list and the info column can no longer share the
   left edge; the strip, CTA and preview stack carry navigation instead. */
@media (max-width: 939.98px) {
  html.works-live .works-names {
    display: none;
  }
}

/* ---- Next-level preview stack, right edge ---------------------------------------
   One slot per entry of the CURRENT hall (slots built by home.js): the first
   three keep the receding stagger (0.9 / 0.55 / 0.32 opacity, 100% / 78% /
   60% of the base width), slot 4+ shrink to the uniform small card with a
   .55 -> .45 opacity tail. The whole column scrolls (thin ink scrollbar on
   transparent track, paper base) and stays inside the viewport safe zone.
   Each slot is a real link, so nav.js's ink-curtain page transition owns the
   navigation. Display gates: none by default (no JS / reduced motion), flex
   only under html.works-live from 768px up (phones get the horizontal strip
   — see the <768px block). */
.works-next {
  display: none;
}

@media (min-width: 768px) {
  html.works-live .works-next {
    display: flex;
    position: absolute;
    /* Centered, capped at ~86svh: clears the header on top and the page
       chrome at the bottom at common window heights. */
    right: clamp(2rem, 6vw, 6rem);
    top: 50%;
    translate: 0 -50%;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-2);
    max-height: 86svh;
    overflow-y: auto;
    overscroll-behavior: contain; /* wheel over the stack scrolls the stack */
    padding: var(--space-2) 0 var(--space-2) var(--space-2);
    background: var(--bg); /* paper base behind the scrolling thumbnails */
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
    z-index: var(--z-raised);
  }

  /* WebKit: thin ink thumb on a transparent track */
  html.works-live .works-next::-webkit-scrollbar {
    width: 4px;
  }

  html.works-live .works-next::-webkit-scrollbar-track {
    background: transparent;
  }

  html.works-live .works-next::-webkit-scrollbar-thumb {
    background: var(--border-strong);
  }
}

.works-next-slot {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
  text-decoration: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.works-next-slot.is-1 { width: var(--works-next-w); opacity: 0.9; }
.works-next-slot.is-2 { width: calc(var(--works-next-w) * 0.78); opacity: 0.55; }
.works-next-slot.is-3 { width: calc(var(--works-next-w) * 0.6); opacity: 0.32; }

/* Slots 4+: the uniform small card. Its opacity is a per-slot custom
   property (set inline by home.js, .55 declining to .45 across the tail)
   so the hover step-forward below can still win the cascade. */
.works-next-slot.is-small {
  width: var(--works-next-small);
  opacity: var(--slot-o, 0.5);
}

.works-next-slot.is-small .works-next-img {
  max-height: 88px; /* keeps the tail compact; object-fit crops the rest */
}

/* Reserved semantics: is-empty hides a slot, a child without a page renders
   dead — visible in the stack, not clickable. */
.works-next-slot.is-empty { display: none; }

.works-next-slot.is-dead { pointer-events: none; cursor: default; }

@media (hover: hover) {
  .works-next-slot.is-1:hover,
  .works-next-slot.is-2:hover,
  .works-next-slot.is-3:hover,
  .works-next-slot.is-small:hover {
    opacity: 1; /* the receding stack steps forward on approach */
  }

  .works-next-slot:hover .works-next-num {
    color: var(--accent-dim);
  }
}

.works-next-img {
  width: 100%;
  height: auto; /* the HTML height attribute would pin 450px and kill 4/3 */
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--surface-2);
  outline: 1px solid var(--border-color); /* pale cards stay legible (T3 rework) */
  outline-offset: -1px;
  border: 1px solid var(--border-strong); /* the ink hairline */
}

.works-next-num {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* Swap fade: animates opacity from 0 up to each slot's OWN base value
   (0.9 / 0.55 / 0.32) — a `from`-only keyframe. home.js retriggers the
   .is-swap class per hall change, same pattern as the bracket pulse. */
html.works-live .works-next.is-swap .works-next-slot {
  animation: works-next-fade 160ms var(--ease-out);
}

@keyframes works-next-fade {
  from { opacity: 0; }
}

/* ---- Scroll affordance, bottom left --------------------------------------------- */
html.works-live .works-hint {
  position: absolute;
  left: clamp(var(--space-2), 1vw, var(--space-4));
  bottom: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

/* ---- Narrow screens: the stage reflows, the strip stays the hero ---------------- */
@media (max-width: 767.98px) {
  html.works-live .works {
    --works-w: 60vw; /* the spec'd mobile strip width */
  }

  html.works-live .works-names,
  html.works-live .works-paren,
  html.works-live .works-hint,
  html.works-live .works-blurb {
    display: none; /* side chrome cannot fit; the about teaser carries it */
  }

  /* The preview stack becomes a horizontal snap filmstrip docked in the
     safe zone: it clears the 46px cat band at the top by anchoring to the
     bottom, and sits above the 38px index band + the credits slab. Thumb
     width ~68px, wheel/touch scroll on x, no scrollbar (touch affordance). */
  html.works-live .works-next {
    display: flex;
    position: absolute;
    left: 0;
    right: 0;
    top: auto;
    bottom: 132px; /* 38px index band + the credits slab + one air step */
    translate: 0 0;
    flex-direction: row;
    align-items: flex-end;
    gap: var(--space-2);
    max-height: none;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior: contain;
    scroll-snap-type: x proximity;
    padding: var(--space-2) var(--space-3);
    background: var(--bg);
    scrollbar-width: none;
    z-index: 2; /* the strip slides behind, like through a film gate */
  }

  /* Mobile bands keep the compact voice: the enlarged 18px desktop desc
     wraps to two lines here and pushes the credits slab into the strip. */
  html.works-live .works-desc {
    font-size: 15px;
  }

  html.works-live .works-next::-webkit-scrollbar {
    display: none;
  }

  html.works-live .works-next-slot,
  html.works-live .works-next-slot.is-1,
  html.works-live .works-next-slot.is-2,
  html.works-live .works-next-slot.is-3 {
    flex: none;
    width: 68px;
    scroll-snap-align: start;
    opacity: 0.75;
  }

  html.works-live .works-next-slot.is-1 {
    opacity: 0.9; /* keep one anchor card carrying */
  }

  html.works-live .works-next-slot.is-small .works-next-img {
    max-height: none; /* uniform 68px cards: plain 4:3 */
  }

  html.works-live .works-next-num {
    font-size: 10px; /* the xs label shrinks for the 68px card */
  }

  /* The info column dissolves: its three rows re-dock as full-width paper
     bands (cat under the header, credits + index at the bottom) against the
     stage. display:contents removes the column box, so the absolutely
     positioned rows resolve against .works-stage like before. */
  html.works-live .works-info {
    display: contents;
  }

  /* Mobile: full-width paper bands — cat docks under the header, credits and
     index dock at the bottom; the strip slides behind the bands like film
     through a gate, so text never sits on a passing cover. z-index:2 keeps
     the bands above the transformed strip (its translate3d stacks it late). */
  html.works-live .works-cat {
    position: absolute;
    right: 0;
    left: 0;
    top: var(--header-height);
    translate: 0 0;
    align-content: center;
    justify-items: center;
    height: 46px;
    background: var(--bg);
    z-index: 2;
  }

  html.works-live .works-credits {
    position: absolute;
    left: 0;
    right: 0;
    top: auto;
    bottom: 38px;
    translate: 0 0;
    max-width: none;
    justify-items: center;
    align-content: center;
    background: var(--bg);
    padding: var(--space-2) var(--space-4) var(--space-3);
    z-index: 2;
  }

  html.works-live .works-credit {
    justify-items: center;
    text-align: center;
  }

  html.works-live .works-index {
    position: absolute;
    right: 0;
    left: 0;
    top: auto;
    bottom: 0;
    translate: 0 0;
    justify-items: center;
    align-content: center;
    height: 38px;
    font-size: var(--text-xs); /* the desktop column number shrinks back */
    background: var(--bg);
    z-index: 2;
  }
}

/* ---- About teaser -------------------------------------------------------------- */
.about-copy {
  margin-top: var(--space-6);
  max-width: 38em;
  display: grid;
  gap: var(--space-3);
  color: var(--text-dim);
  text-wrap: pretty;
}

.about-link {
  display: inline-block;
  margin-top: var(--space-6);
  font-weight: 600;
}

/* ---- Footer clock: Chengdu local time, ticking every second --------------------
   Information, not motion: it runs under reduced motion too. Without JS the
   digits stay hidden and only the label shows. */
.footer-clock {
  margin-left: auto; /* the right end of the footer row */
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: rgba(242, 240, 234, 0.64); /* same voice as .footer-sign */
}

.footer-clock-time {
  color: var(--bg); /* the payload reads brightest, like .footer-sign .out */
  font-variant-numeric: tabular-nums; /* ticking digits never shift layout */
}

html:not(.js) .footer-clock-time {
  display: none;
}

/* ---- Marquee accents: the ✕ separators take the spot color ----------------------
   Frame.css owns the ink slab, type and loop; only the separators are
   page-flavored. */
.marquee .mx {
  color: var(--accent);
}

/* ---- Nav link underline reveal (scoped, additive to frame.css) ------------------
   The underline trails in on hover/focus and disappears again on leave; the
   active page keeps its solid 2px orange-red underline from frame.css (that
   rule wins on specificity). */
.site-nav-link {
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color var(--dur-fast) var(--ease-out),
              text-decoration-color var(--dur-fast) var(--ease-out);
}

.site-nav-link:hover,
.site-nav-link:focus-visible {
  text-decoration-color: var(--accent-dim);
}

/* ==========================================================================
   FX LAYER — the percentage preloader. Additive layer only: tokens are
   referenced, never redefined; nothing here moves the document (fixed
   overlay only). Gated on html.boot-pending, added pre-paint by the head
   bootstrap for the first visit of the session (JS on, motion OK).
   ========================================================================== */

/* ---- FX A: percentage preloader. Paper slab, giant Archivo Black counter
   bottom-left, mono meta top-left. Click/any key skips; boot-lift raises
   the curtain while the page underneath is already interactive. */
.boot {
  display: none;
}

html.boot-pending {
  overflow: hidden;
}

html.boot-pending .boot {
  display: block;
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: var(--bg);
  cursor: pointer;
}

.boot-meta {
  position: absolute;
  top: var(--space-6);
  left: var(--space-6);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-mono);
  letter-spacing: 0.08em;
  color: var(--accent-dim); /* body-size orange-red: the AA-safe voice */
}

.boot-line {
  white-space: pre;
}

.boot-line .boot-fail {
  font-weight: 600; /* the punchline, same hue */
}

/* The Obys counter: display-size ink, tabular digits so 9->10->100 never
   jitters; anchored bottom-left it only ever grows rightward. */
.boot-num {
  position: absolute;
  left: var(--space-4);
  bottom: var(--space-2);
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(7rem, 26vw, 15rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

@media (min-width: 720px) {
  .boot-num {
    left: var(--space-6);
    bottom: var(--space-4);
  }
}

.boot-skip {
  position: absolute;
  right: var(--space-6);
  bottom: var(--space-6);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-faint);
}

/* The curtain: whole slab slides up; cleanup (display:none) is home.js's
   job once the transition ends. Reduced-motion visitors never see this —
   the head bootstrap never adds boot-pending for them. */
html.boot-lift .boot {
  transform: translateY(-100%);
  transition: transform 520ms var(--ease-inout);
}
