/* ==========================================================================
   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. A dot grid at 4px, which at normal viewing distance reads as
   a tint rather than as dots — the print equivalent of the reference's grain
   over glass. Two uses only: the avatar, and the inverted band. */
.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;
}

/* ---------------------------------------------------------------- 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;
  }
}
