/* ==========================================================================
   echanxyz — "Press Proof"
   --------------------------------------------------------------------------
   The material here is ink on paper, not light through glass. That distinction
   decides every rule below, so it is worth stating plainly:

     - Elevation is a VALUE STEP, never a shadow. A plate is lighter than the
       page (or darker, in the negative), and its edge is a 1px impression —
       lighter lip on top, darker lip beneath. No blur, no glow, no colour.
     - Emphasis is INVERSION. Where a colour design would reach for a brand
       hue, this one flips ink and paper. It is the loudest move monochrome
       owns, so it is rationed to one full-bleed band per page.
     - Shading is HATCH and HALFTONE, not opacity gradients. Engravings shade
       with line; a smooth alpha ramp is the giveaway that this is a colour
       design with the saturation slider at zero.

   Nothing on this page uses backdrop-filter, animated filters, or animated
   gradients. The whole atmosphere is static; every transition is transform or
   opacity. That is not asceticism — it is what buys the grain and the halftone
   at 60fps on a phone.

   Tailwind (CDN) is loaded for layout utilities only. Anything that carries the
   material — plates, rules, bands, type roles — lives here, so the two never
   fight over the same property.
   ========================================================================== */

/* ---------------------------------------------------------------- tokens */
:root {
  /* LIGHT — "paper": white plates on a grey desk.
     The page is deliberately NOT #fff; if the page were white the plates would
     have nothing to be lighter than, and the whole elevation system collapses. */
  --bg:           #f4f4f4;
  --surface:      #ffffff;
  --surface-2:    #ececec;
  /* A recess, not a plate: the full-bleed band reads as a tray sunk into the
     sheet, so whatever sits in it still has something to be lighter than. In
     the negative it has to go DOWN from the page, not up, or the plate inside
     ends up darker than its own ground and the elevation reads backwards. */
  --recess:       #ececec;

  --fg:           #111111;
  /* .66 rather than the .5-ish that looks right in a mockup: this token carries
     real prose (ledes, item descriptions), and anything under 4.5:1 fails AA at
     those sizes. --fg-faint is decorative-only and is held to labels. */
  --fg-secondary: rgb(17 17 17 / .66);
  --fg-faint:     rgb(17 17 17 / .42);

  --hairline:     rgb(17 17 17 / .12);
  --line-strong:  rgb(17 17 17 / .28);
  --grid-line:    rgb(17 17 17 / .045);

  /* The impression. Paper catches light on its top lip and shades under its
     bottom one — 1px each, value only. This pair replaces every box-shadow. */
  --emboss:       inset 0 1px 0 #ffffff;
  --deboss:       inset 0 -1px 0 rgb(17 17 17 / .06);

  --ink:          #111111;
  --ink-fg:       #f4f4f4;
  --ink-faint:    rgb(244 244 244 / .45);

  --lamp: radial-gradient(120% 90% at 50% -10%, #fbfbfb 0%, var(--bg) 55%, #efefef 100%);
  --grain-opacity: .05;
  --rim: linear-gradient(150deg,
           rgb(17 17 17 / .16) 0%,
           rgb(17 17 17 / .05) 30%,
           rgb(17 17 17 / .02) 55%,
           rgb(17 17 17 / .09) 100%);
}

.dark {
  /* DARK — "negative": the same sheet, inverted. */
  --bg:           #0a0a0a;
  --surface:      #111111;
  --surface-2:    #1a1a1a;
  --recess:       #050505;

  --fg:           #f2f2f2;
  --fg-secondary: rgb(242 242 242 / .62);
  --fg-faint:     rgb(242 242 242 / .40);

  --hairline:     rgb(255 255 255 / .09);
  --line-strong:  rgb(255 255 255 / .22);
  --grid-line:    rgb(255 255 255 / .04);

  --emboss:       inset 0 1px 0 rgb(255 255 255 / .07);
  --deboss:       inset 0 -1px 0 rgb(0 0 0 / .5);

  --ink:          #f2f2f2;
  --ink-fg:       #0a0a0a;
  --ink-faint:    rgb(10 10 10 / .5);

  --lamp: radial-gradient(110% 80% at 50% -15%, #131313 0%, var(--bg) 60%);
  --grain-opacity: .09;
  --rim: linear-gradient(150deg,
           rgb(255 255 255 / .16) 0%,
           rgb(255 255 255 / .04) 30%,
           rgb(255 255 255 / .02) 55%,
           rgb(255 255 255 / .10) 100%);
}

:root {
  /* Motion. No springs anywhere: a spring is the physics of the glass world.
     Paper does not overshoot — it glides, or it snaps. */
  --glide: cubic-bezier(.22, .78, .26, 1);
  --snap:  cubic-bezier(.7, 0, .25, 1);
  --t-micro:  150ms;
  --t-hover:  250ms;
  --t-reveal: 650ms;
  --t-theme:  200ms;

  --gutter: clamp(1.25rem, 5vw, 4rem);
  --measure: 1200px;
  --section: clamp(4rem, 9vw, 7.5rem);
  --section-tight: clamp(2rem, 4vw, 3rem);
}

/* ---------------------------------------------------------------- base */
html { -webkit-text-size-adjust: 100%; }

body {
  background-color: var(--bg);
  color: var(--fg);
  font-family: 'Geist Sans', 'Inter', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color var(--t-theme) ease, color var(--t-theme) ease;
  /* `clip`, not `hidden`: `overflow-x: hidden` turns body into a scroll
     container, which breaks position:sticky on the nav in several engines.
     Needed because the full-bleed bands are sized off 100vw. */
  overflow-x: hidden;
  overflow-x: clip;
}

::selection { background: var(--fg); color: var(--bg); }

::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--hairline); }
::-webkit-scrollbar-thumb:hover { background: var(--fg-faint); }

:focus-visible {
  outline: 1px solid var(--fg);
  outline-offset: 3px;
}

/* Crawler-readable mirror of the page content. Visually hidden, still in the
   DOM and still announced — the GEO work is independent of this redesign. */
.seo-readable {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- atmosphere */
/* Fixed, so the sheet stays lit from the same place while content scrolls over
   it. Three static layers — no animation on any of them. */
.atmosphere {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

/* The lamp. One radial VALUE field, not three colour masses: paper brightest
   under the light, falling off to a darker desk edge. Total delta is under 4% —
   it should never be legible as a gradient, only as "this page has a light
   source". Static, deliberately: the aurora it replaces had to move because
   coloured light is the show, but a value field is the stage. */
.lamp {
  position: absolute;
  inset: 0;
  background: var(--lamp);
  transition: background var(--t-theme) ease;
}

/* Drafting grid. Present only near the top of the page — masked out before it
   reaches the content proper, so it reads as the sheet the hero is set on
   rather than as decoration running the whole length of the site. */
.gridfield {
  position: absolute;
  inset: -1px;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 96px 96px;
  -webkit-mask-image: radial-gradient(ellipse 90% 65% at 50% 12%, #000 8%, transparent 72%);
          mask-image: radial-gradient(ellipse 90% 65% at 50% 12%, #000 8%, transparent 72%);
}

/* Static grain. Animated feTurbulence is a Safari performance disaster and buys
   nothing: the still noise already does the whole job — it kills gradient
   banding and takes the CG sheen off every flat surface on the page. */
.grain {
  position: absolute;
  inset: 0;
  opacity: var(--grain-opacity);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (max-width: 40rem) {
  /* Proportion, not performance: a 96px grid across a 375px screen is under
     four cells, which reads as a few stray lines rather than as a grid. */
  .gridfield { background-size: 64px 64px; }
}

/* ---------------------------------------------------------------- type */
.eyebrow {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .6875rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

.display {
  font-weight: 700;
  font-size: clamp(3.75rem, 1.9rem + 10.5vw, 9rem);
  letter-spacing: -.045em;
  line-height: .92;
  text-wrap: balance;
}

.h2 {
  font-weight: 600;
  font-size: clamp(1.75rem, 1.35rem + 2vw, 2.75rem);
  letter-spacing: -.03em;
  line-height: 1.05;
  text-wrap: balance;
}

.h3 {
  font-weight: 500;
  font-size: 1.125rem;
  letter-spacing: -.015em;
  line-height: 1.3;
}

.lede {
  max-width: 44ch;
  font-size: clamp(1rem, .95rem + .3vw, 1.125rem);
  line-height: 1.6;
  color: var(--fg-secondary);
  text-wrap: pretty;
}

.prose {
  font-size: .9375rem;
  line-height: 1.6;
  color: var(--fg-secondary);
  text-wrap: pretty;
}

/* Every number, label and datum on the site. Tabular by default so a ticking
   clock and a column of counts never shift width between frames. */
.datum {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .8125rem;
  font-weight: 400;
  letter-spacing: .02em;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}

.idx {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  font-weight: 300;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--fg-faint);
  transition: color var(--t-hover) var(--glide);
}

.idx--sm { font-size: .75rem; font-weight: 400; letter-spacing: .06em; }

.chip {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--hairline);
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .625rem;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* The micro-inversion. Used for exactly one thing — the NEW stamp — because an
   inverted chip reads as a stamped mark, and a second one would read as UI. */
.chip--ink {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--ink-fg);
}

/* ---------------------------------------------------------------- material */
/* The plate. Value step for elevation, hairline for extent, impression for
   edge. This is the whole material — there is no second one. */
.plate {
  background: var(--surface);
  border: 1px solid var(--hairline);
  box-shadow: var(--emboss), var(--deboss);
  transition: border-color var(--t-hover) var(--glide),
              background-color var(--t-theme) ease;
}

/* The one place the reference's mask-composite ring earns its keep: a 1px edge
   that can carry a gradient, so the impression falls off around the shape
   instead of sitting at a uniform weight. Two plates per page, maximum —
   on everything it stops reading as craft and starts reading as a style. */
.plate--rim { position: relative; isolation: isolate; }
.plate--rim::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 1px;
  background: var(--rim);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}

/* Registration marks — the crosshairs a press sheet carries at its corners so
   the plates line up. Eight background layers (two arms per corner) rather than
   four extra elements, so any box can wear them without touching its markup. */
.regmarks {
  position: relative;
  background-image:
    linear-gradient(var(--fg-faint), var(--fg-faint)), linear-gradient(var(--fg-faint), var(--fg-faint)),
    linear-gradient(var(--fg-faint), var(--fg-faint)), linear-gradient(var(--fg-faint), var(--fg-faint)),
    linear-gradient(var(--fg-faint), var(--fg-faint)), linear-gradient(var(--fg-faint), var(--fg-faint)),
    linear-gradient(var(--fg-faint), var(--fg-faint)), linear-gradient(var(--fg-faint), var(--fg-faint));
  background-repeat: no-repeat;
  background-size:
    11px 1px, 1px 11px,
    11px 1px, 1px 11px,
    11px 1px, 1px 11px,
    11px 1px, 1px 11px;
  background-position:
    left -5px top 0,      left 0 top -5px,
    right -5px top 0,     right 0 top -5px,
    left -5px bottom 0,   left 0 bottom -5px,
    right -5px bottom 0,  right 0 bottom -5px;
}

/* Hairline rule that runs out to fill its row — the sectioning device the whole
   site is built on. Paired with an .eyebrow to its left. */
.rule { flex: 1 1 0%; height: 1px; background: var(--hairline); }

/* Spec table. The gaps ARE the rules: a 1px grid gap over a hairline-coloured
   background shows through between cells, so there is no double-border problem
   at any breakpoint and no last-child exception to write. */
.specs {
  display: grid;
  gap: 1px;
  background: var(--hairline);
  border: 1px solid var(--hairline);
}
.specs > * {
  min-width: 0;
  padding: .875rem 1rem;
  background: var(--bg);
  transition: background-color var(--t-theme) ease;
}
.specs dt {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .625rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.specs dd {
  margin: .5rem 0 0;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}

/* Halftone screen, CSS flavour: a fixed 4px dot grid that reads as a tint
   rather than as dots. It is a TINT, not a screen — every dot is the same size,
   so it cannot carry tone.
   The avatar no longer uses it. That plate is screened for real in
   lib/halftone.ts, where dot AREA is solved per cell from the photograph's
   luminance — which is what the rule in the header actually asks for. Keep this
   class for flat surfaces that want the texture without the measurement. */
.halftone {
  background-image: radial-gradient(circle, currentColor .75px, transparent .75px);
  background-size: 4px 4px;
}

/* ---------------------------------------------------------------- bands */
/* Full bleed. The band breaks the measure because the thing inside it is the
   page's differentiator and should not sit in the same box as everything else.
   One per page — it is a budget, not a quota.
   Sized off the element's own position rather than 100vw so a visible scrollbar
   cannot push the page sideways. */
.band {
  width: auto;
  /* A percentage margin resolves against the containing block's width, so
     `50% - 50vw` cancels out to exactly the viewport edge whatever the parent's
     max-width and padding happen to be.
     The `--sbw` term is not optional polish: `vw` is measured against the
     viewport INCLUDING the classic scrollbar, while layout is laid out against
     the width excluding it. Without the correction the band overhangs by half
     the scrollbar on each side — which `overflow-x: clip` hides — but its inner
     padding is also off by that much, so the band's content no longer lines up
     with the column above it. App.tsx measures the gutter and publishes it. */
  margin-inline: calc(50% - 50vw + var(--sbw, 0px) / 2);
  /* Re-inset the content to the measure's content box: the space outside the
     measure, plus the gutter the measure itself carries. */
  padding-inline: calc(
    max(0px, 50vw - var(--sbw, 0px) / 2 - var(--measure) / 2) + var(--gutter)
  );
}

/* The inversion. Ink and paper trade places — the single loudest gesture in a
   monochrome system, and the reason this design does not need an accent colour. */
.band--ink {
  background: var(--ink);
  color: var(--ink-fg);
  transition: background-color var(--t-theme) ease, color var(--t-theme) ease;
}
.band--ink .eyebrow,
.band--ink .idx,
.band--ink .datum { color: var(--ink-faint); }

/* Hairline-ruled band — used where content needs to break the measure without
   spending the page's one inversion. It is a RECESS: `--surface` here would
   match the plate that sits inside it exactly, and the plate would vanish into
   its own ground with only its border left to describe it. Elevation in this
   system is a value step, so the tray has to step the other way. */
.band--ruled {
  background: var(--recess);
  box-shadow: inset 0 1px 0 var(--hairline), inset 0 -1px 0 var(--hairline);
  transition: background-color var(--t-theme) ease;
}

/* ---------------------------------------------------------------- the turn */
/* The homepage is ONE sheet, ONE screen, and the sheet turns over.

   The rule at the top of this file rations inversion to one full-bleed band per
   page. The homepage spends its entire budget here, on itself: sweep the
   pointer, or tilt the phone, and a NEGATIVE travels across the proof —
   headline, rules, spec table and the screened portrait all turning together.

   It is one white plane in `difference`, which over a palette with zero hue is
   exactly `1 − backdrop`: a photographic negative, which is a real step in
   platemaking rather than an effect borrowed from a colour system. It costs
   what it looks like it costs and no more — `difference` is a compositing mode,
   not a filter, and the sweep is `scaleX` on a flat fill, so there is no
   repaint and nothing to resample. The no-blur, no-filter rule above stands.

   Coverage rests at ZERO rather than at half. A lenticular looks worst
   half-turned, and at rest is exactly the state a page loads into — hence the
   dead zone shared with the plate (lib/tilt.ts), and hence the sweep being
   anchored to the edge the tilt comes FROM rather than sitting on a seam. */
.sheet-shell {
  display: flex;
  flex-direction: column;
  gap: .625rem;
  /* One screen: the viewport, less the nav, less the mobile tab rail that
     `main` already pads for. `dvh` so a phone's collapsing URL bar cannot leave
     the sheet taller than the screen it exists to fill. */
  min-height: calc(100vh - 3.5rem - 5rem);
  min-height: calc(100dvh - 3.5rem - 5rem);
  padding-block: clamp(.5rem, 1.5vw, 1rem);
}
@media (min-width: 768px) {
  .sheet-shell {
    min-height: calc(100vh - 3.5rem);
    min-height: calc(100dvh - 3.5rem);
  }
}

.sheet {
  position: relative;
  border: 1px solid var(--hairline);
  padding: clamp(1rem, 3.2vw, 2rem);
}
/* Full only on the homepage. The index pages wear a sheet as a masthead and let
   their catalog scroll out from under it — burying a catalog under a whole
   screen of masthead is the effect winning an argument it should lose. */
.sheet--full {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
}

/* A sheet is a fixed size and the job is to set the page to fit it — so on this
   one route the headline answers to the viewport's HEIGHT as well as its width.
   Two lines of the unclamped display is 265px, which on a 13" laptop is most of
   the vertical budget before anything else has been set. */
.sheet--full .display { font-size: clamp(2.25rem, min(1.9rem + 8.5vw, 15.5vh), 9rem); }
/* Auto margins on the body rather than `justify-content` on the sheet: the rail
   has to sit on the bottom edge, and an auto margin beats any distribution the
   parent would otherwise do. */
.sheet__body { margin-block: auto; }

.negative {
  position: absolute;
  /* −6px: the frame, and the registration crosshairs that hang 5px outside it,
     turn WITH the sheet. At −1px the marks survive the sweep as four little
     hairline arms that did not get the message. */
  inset: -6px;
  z-index: 5;
  background: #fff;
  mix-blend-mode: difference;
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: right center;
}

/* The same turn, COMMITTED. Two ways to invert a page is one too many: with the
   sheet already turning under your hand, a ◐ button that also flips ink and
   paper reads as a second, unrelated mechanism doing the same job. So it is not
   a second mechanism — the toggle is this turn carried all the way through.
   Tilt peeks at the negative; the toggle commits to it.

   Site-wide and above the nav (z-index over the header's 50), because a wipe
   that stopped at the sheet's edge would leave the bar and the catalog under it
   snapping to the new theme while the sheet was still turning. The theme swaps
   FIRST, under full coverage — the negative of the new theme is the old one, so
   the instant of the click shows no change at all and the retract is what
   reveals it. */
.turn-wipe {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: #fff;
  mix-blend-mode: difference;
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: right center;
}

/* Token transitions off for the length of the wipe. `body` cross-fades its
   background over 200ms on a theme change, and a ground still fading behind the
   retreating edge is what makes a wipe look muddy instead of printed. The wipe
   sets its own transition with `important`, which inline beats this. */
html.turning *,
html.turning *::before,
html.turning *::after { transition: none !important; }

/* Destination rail — the sheet's bottom edge. The two disciplines used to be
   two large panels below the fold, repeating counts the spec table already
   carries; on a one-screen page they are wayfinding, so they are set as
   wayfinding. Gaps are the rules again, exactly as in `.specs`. */
.sheet__rail {
  display: grid;
  gap: 1px;
  margin-top: clamp(1rem, 2.5vw, 1.5rem);
  background: var(--hairline);
  border-top: 1px solid var(--hairline);
}
@media (min-width: 640px) { .sheet__rail { grid-template-columns: 1fr 1fr; } }
/* The sub-line is what makes these read as entries rather than as a menu — and
   on a phone it is also 55px of the screen this page is trying to close inside.
   The label and the numeral already say where the link goes. */
/* Two class selectors, because Tailwind's `block` utility is loaded after this
   file and would otherwise win the tie on source order. */
@media (max-width: 639px) { .sheet__rail .dest__sub { display: none; } }

.dest {
  display: flex;
  align-items: baseline;
  gap: .875rem;
  padding: .875rem 1rem .875rem 0;
  background: var(--bg);
  color: var(--fg);
  text-decoration: none;
  transition: background-color var(--t-hover) var(--glide);
}
@media (min-width: 640px) { .dest { padding-inline: 1rem; } .dest:first-child { padding-left: 0; } }
.dest__arrow {
  margin-left: auto;
  color: var(--fg-faint);
  transition: transform var(--t-hover) var(--glide), color var(--t-hover) var(--glide);
}
@media (hover: hover) {
  /* A value step, like every other elevation on the site. */
  .dest:hover { background: var(--surface-2); }
  .dest:hover .dest__arrow { color: var(--fg); transform: translateX(3px); }
  .dest:hover .idx--sm { color: var(--fg); }
}
@media (pointer: coarse) { .dest { padding-block: 1rem; } }

/* The press log — the fourth atmosphere layer, and the only one that moves.
   Fixed behind everything, above the lamp and the drafting grid and under all
   type, so the negative and the theme wipe take it with them when they turn.
   What it draws and why it is allowed to move is in components/PressLog.tsx.

   The opacity lives here rather than in the canvas so the two grounds can
   differ: ink on paper carries further than paper on ink, so the negative needs
   a little more of it to sit at the same weight. */
.presslog {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: .11;
  /* Masked away from the middle band, where the display type is set. A field
     that runs uniformly edge to edge is a texture swatch; one that thins out
     behind the headline reads as something happening BEHIND the page. Static
     mask, no animation — the same call `.gridfield` makes. */
  -webkit-mask-image: linear-gradient(#000 0%, transparent 26%, transparent 74%, #000 100%);
          mask-image: linear-gradient(#000 0%, transparent 26%, transparent 74%, #000 100%);
  transition: opacity var(--t-theme) ease;
}
.dark .presslog { opacity: .19; }

/* The printer's imprint, hung under the sheet rather than inside it: who set
   it, in what, where. It is the colophon, which is why the site footer stands
   down on this route — see App.tsx. */
.imprint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem .875rem;
}
/* The imprint sets on one line from 360px up, but only just — see the note in
   Hero on what is dropped below sm to get it there. The wider column gap is
   what a colophon wants and what it gets as soon as there is room for it. */
@media (min-width: 640px) { .imprint { column-gap: 1.25rem; } }
.imprint a { color: var(--fg-faint); text-decoration: none; transition: color var(--t-hover) var(--glide); }
@media (hover: hover) { .imprint a:hover { color: var(--fg); } }

/* ---------------------------------------------------------------- rows */
/* The index row — the workhorse of every list on the site. The hover moves an
   inner wrapper by transform rather than changing padding: the old `hover:pl-4`
   animated layout, which reflows the row and its neighbours on every frame. */
.row {
  display: grid;
  align-items: start;
  gap: 0 clamp(1rem, 3vw, 2rem);
  padding: 1.5rem 0;
  border-bottom: 1px solid var(--hairline);
  transition: border-color var(--t-hover) var(--glide);
}
.row__shift { transition: transform var(--t-hover) var(--glide); }
.row__arrow {
  color: var(--fg-faint);
  transition: transform var(--t-hover) var(--glide), color var(--t-hover) var(--glide);
}

/* Hover promotions only where hovering is real. On a touchscreen :hover sticks
   after a tap, so the row would stay nudged until you touch something else. */
@media (hover: hover) {
  .row:hover { border-color: var(--line-strong); }
  .row:hover .row__shift { transform: translateX(6px); }
  .row:hover .idx { color: var(--fg); }
  /* The title is NOT faded on hover. Dimming the element you just pointed at is
     backwards; the surrounding apparatus brightens instead. */
  .row:hover .row__arrow { color: var(--fg); transform: translate(2px, -2px); }
}

/* A row that is NOT itself a link. The promotions above are an affordance — they
   say "this whole row is clickable" — so a row whose only live targets are a
   title and two glyphs must opt out of them, or it promises a click it cannot
   honour anywhere except on those few pixels. */
.row--static:hover { border-color: var(--hairline); }
.row--static:hover .row__shift { transform: none; }
.row--static:hover .idx { color: var(--fg-faint); }

/* ---------------------------------------------------------------- links */
.link-inline {
  color: var(--fg-secondary);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--t-hover) var(--glide);
}
@media (hover: hover) { .link-inline:hover { color: var(--fg); } }

/* Block link — a bordered mono label. The border, not a colour, is what moves. */
.link-block {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  padding: .625rem 1rem;
  border: 1px solid var(--hairline);
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .6875rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-secondary);
  text-decoration: none;
  transition: border-color var(--t-hover) var(--glide), color var(--t-hover) var(--glide);
}
.link-block svg { transition: transform var(--t-hover) var(--glide); }
/* An 11px label in .625rem padding lands around 36px tall, which is under every
   touch-target guideline. Pin the result rather than leaving it to whatever the
   padding and the current font size happen to add up to. */
@media (pointer: coarse) {
  .link-block { min-height: 44px; padding-block: .75rem; }
}
@media (hover: hover) {
  .link-block:hover { border-color: var(--line-strong); color: var(--fg); }
  .link-block:hover svg { transform: translate(2px, -2px); }
}

/* Nav link. The active state is a rule under the word, not a colour change —
   value and position, never hue. */
.navlink {
  position: relative;
  padding: .25rem 0;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: .6875rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-faint);
  text-decoration: none;
  transition: color var(--t-hover) var(--glide);
}
@media (hover: hover) { .navlink:hover { color: var(--fg); } }
.navlink[aria-current="page"] { color: var(--fg); }
.navlink[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 1px;
  background: var(--fg);
}

/* ---------------------------------------------------------------- reveal */
/* Rise-and-fade, once, on entry. `--d` carries the stagger so the delay lives
   inline on the element rather than in a generated class per index. */
.reveal {
  opacity: 0;
  transform: translateY(14px);
  animation: rise var(--t-reveal) var(--glide) forwards;
  animation-delay: var(--d, 0ms);
}
@keyframes rise { to { opacity: 1; transform: none; } }

.on-scroll {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--t-reveal) var(--glide), transform var(--t-reveal) var(--glide);
  transition-delay: var(--d, 0ms);
}
.on-scroll.is-in { opacity: 1; transform: none; }

/* ---------------------------------------------------------------- reduced motion */
/* One blanket block rather than a flag per component: anything that moves is
   declared above, so this is the complete list. The clock is exempt because it
   is information, not motion. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { animation: none; opacity: 1; transform: none; }
  .on-scroll { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
