/* Hallmark · press layer - the scroll journey.
 *
 * One idea, one system: the page prints itself as the reader descends.
 * Everything here is downstream of three scalars published by press-run.js -
 *   --press-ink  0 → 1   how much ink has hit the paper
 *   --press-reg  1 → .12 how far the plates sit out of register
 *   --press-spot a colour the section owns
 * - plus --press-nx/--press-ny, the pointer lean published by print-plates.js.
 *
 * The whole colour journey is implemented by redeclaring THREE role tokens on
 * :root. styles.css is untouched: every component in it already reads
 * var(--color-bg) / var(--color-text) / var(--color-accent), so re-pointing
 * those three re-inks the entire page with no per-component work.
 *
 * NOTE: this page carries no photography, so the compound SVG separation
 * (#sep-all in print-plates.js) is never invoked here - only the CSS text
 * plates. print-plates.js is loaded unmodified for the pointer lean it
 * publishes; its filter defs sit inert.
 */

/* ─── the scalars ────────────────────────────────────────────────────────── */
/* Registration is load-bearing, not decoration: unregistered custom properties
   interpolate discretely AND a bad JS write (undefined, "") makes the whole
   calc() invalid-at-computed-value-time, which drops the entire `translate`
   declaration and snaps the plate to zero offset. Registered, a bad write
   falls back to initial-value and the construction survives. */
@property --press-ink  { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --press-reg  { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --press-p    { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --line-reg   { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --press-nx   { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --press-ny   { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --press-spot { syntax: "<color>";  inherits: true; initial-value: #0088b0; }

/* --press-ink defaults to 1 so a JS-less or pre-hydration render is the fully
   printed page - maximum legibility. press-run.js takes it down on first frame. */

/* ─── the stock and the inks ─────────────────────────────────────────────── */
:root {
  --stock-0: #efeeed;   /* cold, barely-inked sheet */
  --stock-1: #f3f2f2;   /* the design system's paper, fully printed */
  /* --ink-0 is the FIRST IMPRESSION, not an invisible one. It was #8f8c8a on
     the first pass, which measures ~2.6:1 on the cold stock - the entire first
     fold would have failed WCAG AA. "The page prints itself" must never mean
     "the top of the page is unreadable": the journey is a deepening, and its
     palest end still has to clear AA on its own - INCLUDING the muted tints
     layered on top of it, which is what actually set this value. At #4a4744
     the palest ink measures 7.96:1 at full strength and ~4.9:1 at the faintest
     tint the page uses (--ink-3, 82%), so every level clears AA even at the
     top of the journey where the sheet is barely inked. */
  --ink-0:   #4a4744;
  --ink-1:   #201e1d;   /* the design system's text ink */

  /* Three ink levels, and only three. The first pass had eight scattered
     alphas (52/58/62/66/68/72/86/90%) inherited from the old page - a
     looseness that also put eight elements below 4.5:1 at the top of the
     journey. Measured, tokenised, and cut to three. */
  --ink-2: color-mix(in srgb, var(--color-text) 88%, transparent);  /* body, ledes */
  --ink-3: color-mix(in srgb, var(--color-text) 82%, transparent);  /* captions, furniture */

  /* the three process inks, authored in oklch so the spot transition
     interpolates through a polar space - cyan → magenta rotates the long way
     round through blue and violet, which reads as ink turning on a press.
     In oklab the midpoint passes near grey and looks washed out. */
  --spot-c:   oklch(57% 0.105 226);
  --spot-m:   oklch(55% 0.212 3);
  --spot-y:   oklch(81% 0.155 88);
  --spot-ink: oklch(30% 0.012 60);

  /* THE JOURNEY - three tokens, the whole page follows */
  --color-bg:   color-mix(in oklab, var(--stock-0), var(--stock-1) calc(var(--press-ink) * 100%));
  --color-text: color-mix(in oklab, var(--ink-0),   var(--ink-1)   calc(var(--press-ink) * 100%));
  /* The process inks ARE the identity - they must not ramp from zero. Mixing
     the accent all the way down to neutral grey at ink 0 wiped the cyan plate
     off the hero, leaving a magenta x yellow core that read dark red instead
     of black: the signature of the whole site, absent on arrival. The plates
     stay unmistakably themselves from the first frame; what the journey
     carries is a shallow 75% → 100% laying-down of the ink, plus the hue
     rotation --press-spot performs per section, which is the real colour
     story. */
  --color-accent: color-mix(in oklab, var(--color-neutral-500),
                            var(--press-spot) calc((0.75 + 0.25 * var(--press-ink)) * 100%));

  /* styles.css hard-codes these four against #201e1d / static hexes, so they
     would strand behind the journey - links staying cyan while the accent has
     rotated to magenta. Re-derive them. */
  --color-divider: color-mix(in srgb, var(--color-text) 16%, transparent);
  --shadow-sm: 0 1px 2px  color-mix(in srgb, var(--color-text) 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, var(--color-text) 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, var(--color-text) 22%, transparent);

  /* The lightness clamp is the contrast guarantee: body-size text in
     --color-accent-700 must stay ≥ 4.5:1 on paper WHATEVER hue the spot
     rotates to. A naive color-mix with black lands ~4.4:1 on the process
     yellow - a failure. Clamping L makes it hue-independent. */
  --color-accent-600: oklch(from var(--press-spot) clamp(0.30, calc(l - 0.08), 0.58) c h);
  --color-accent-700: oklch(from var(--press-spot) clamp(0.28, calc(l - 0.18), 0.46) c h);
  --color-accent-800: oklch(from var(--press-spot) clamp(0.24, calc(l - 0.28), 0.38) c h);

  /* the spot cross-fade: press-run.js writes a DISCRETE value per section and
     the browser tweens it. Zero per-frame cost - scrolling fast costs no more
     than scrolling slow. */
  transition: --press-spot 620ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* The paper lives on the ROOT element, not on a fixed pseudo-element.
   The first pass used `body::before { position: fixed; inset: 0; z-index: -1 }`
   to keep re-inking to a single viewport-sized rect. It rendered a 410px band
   of raw white across the top of the page: `overflow-x: clip` on html/body
   clips fixed descendants, and the layer stopped covering the viewport.
   The root background always paints the whole canvas, cannot be clipped by an
   ancestor (it has none), is already a single rect as far as the compositor is
   concerned, and still sits beneath everything in the root stacking context -
   so the multiply plates keep seeing it as their backdrop. */
html { background: var(--color-bg); }
body { background: transparent; }

/* ─── the plates read the registration ───────────────────────────────────── */
/* styles.css's .cmyk-num offsets are exactly 2x .cmyk-head's in every term
   (bar 0.0005em on the y-lean - 0.046px at the 92px numeral, invisible), so
   the two constructions collapse into one parameterised rule set. */
.cmyk-head { --plate-spread: 1; }
.cmyk-num  { --plate-spread: 2; }
.cmyk-head, .cmyk-num {
  --u: calc(var(--plate-spread) * var(--press-reg) * var(--line-reg));
}

/* C carries a small offset of its own, opposite the other two.
   The design system holds C perfectly still as the reference plate, which is
   how a real press works - but on screen it meant the cyan never showed a
   fringe at all, so the headline read as a red/orange anaglyph rather than a
   four-colour separation. The whole identity of the page is CMYK; the C ink
   has to be visible as ink. Its offset is deliberately ~half the others', so
   it still reads as the plate everything else is out of register *against*. */
.cmyk-head .plate-c, .cmyk-num .plate-c {
  color: var(--color-accent);
  translate: calc((-0.009em + 0.005em * var(--press-nx)) * var(--u))
             calc(( 0.008em + 0.004em * var(--press-ny)) * var(--u));
}

/* The pointer lean scales by --u too: a headline that has come fully into
   register but still wobbles under the mouse reads as a bug, not as a press.
   When the sheet is square, the hand has nothing left to nudge. */
.cmyk-head .plate-m, .cmyk-num .plate-m {
  color: var(--color-accent-2);
  translate: calc((0.018em  + 0.009em * var(--press-nx)) * var(--u))
             calc((0.0125em + 0.006em * var(--press-ny)) * var(--u));
}
.cmyk-head .plate-y, .cmyk-num .plate-y {
  color: var(--color-process-yellow);
  translate: calc((-0.0155em + 0.009em * var(--press-nx)) * var(--u))
             calc((-0.0115em + 0.006em * var(--press-ny)) * var(--u));
}

/* ─── the impression - how content arrives ───────────────────────────────── */
/* Not a fade-up. Ink taking on paper: a wipe, plus a registration overshoot
   that resolves. The overshoot costs nothing, cannot break blending, and says
   "ink resolving" in the vocabulary the page already speaks.
   [data-reveal] alone stays visible - only the "pending" state JS sets is
   hidden, so a JS-less page renders complete. */
@media (prefers-reduced-motion: no-preference) {
  [data-reveal="pending"] {
    clip-path: inset(0 100% 0 0);
    --line-reg: 1.7;
  }
  [data-reveal="in"] {
    animation: impression 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}
@keyframes impression {
  from { clip-path: inset(0 100% 0 0); --line-reg: 1.7; }
  to   { clip-path: inset(0 0 0 0);    --line-reg: 1;   }
}

/* Any element that creates a stacking context isolates the multiply group
   beneath it, and the C/M/Y plates then paint as flat opaque colour with no
   dark core. clip-path creates one. The plate constructions carry their own
   paper ground (.cmyk-num paints --cmyk-num-ground on its padding box), which
   is exactly why the numerals survive a reveal - but anything else that wraps
   a plate and clips must paint paper too. */
.specs__row, .work { background: var(--color-bg); }

/* ─── the hero settles into register, once, on arrival ───────────────────── */
/* Rare tier (once per load), purpose: EXPLANATION. The whole page turns on
   plates coming into register; showing that once on arrival teaches the reader
   what the fringes mean before they have scrolled a pixel.
   CSS animation, not JS - it must stay smooth while the page is still loading,
   which is exactly where a rAF loop drops frames.
   It drives --line-reg, NOT --press-reg: the scroll owns page registration,
   this owns the line's own settle, and they multiply in --u. No fighting.
   Deliberately NOT animating opacity: opacity would create a stacking context
   on .hero__line and isolate the multiply plates, flattening the very effect
   the animation exists to show. The convergence is the entrance. */
@media (prefers-reduced-motion: no-preference) {
  .hero__line { animation: settle 900ms cubic-bezier(0.23, 1, 0.32, 1) both; }
  .hero__line + .hero__line { animation-delay: 90ms; }
}
@keyframes settle {
  from { --line-reg: 2.8; }
  to   { --line-reg: 1; }
}

/* ─── the career thread draws itself ─────────────────────────────────────── */
/* This one IS a scroll-driven CSS timeline, and the contrast with the reveals
   above is the point: a progress thread SHOULD retreat when you scroll back up
   (it reports where you are), whereas a reveal must not un-reveal. Scrubbed
   here, one-shot there - same page, opposite requirements.
   scaleY on a pseudo-element, so it stays on the compositor; animating height
   would relayout the section on every frame. */
.track::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--color-accent);
  transform-origin: top;
}
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .track::before {
      animation: draw-thread linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 88%;
    }
  }
}
@keyframes draw-thread { from { transform: scaleY(0); } to { transform: scaleY(1); } }

/* ─── press furniture ────────────────────────────────────────────────────── */
/* The one place a CSS scroll-driven timeline earns its keep: ~2 fixed elements
   animating transform and opacity, compositor-driven, immune to jank while the
   main thread is busy revealing content. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .furniture__target {
      animation: mark-square linear both;
      animation-timeline: scroll(root block);
    }
  }
  @supports not (animation-timeline: scroll()) {
    .furniture__target {
      rotate: calc(-9deg * (1 - var(--press-p)));
      opacity: calc(0.16 + 0.18 * var(--press-p));
    }
  }
}
@keyframes mark-square {
  from { rotate: -9deg; opacity: 0.16; }
  to   { rotate: 0deg;  opacity: 0.34; }
}

/* ─── reduced motion ─────────────────────────────────────────────────────── */
/* Standing the driver down must NOT leave the page at ink 0 - that would be a
   pale, low-contrast page, which is a contrast regression rather than a
   graceful degradation. Publish the fully-printed end state instead, and keep
   a modest misregistration so the idea survives without any movement. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --press-ink: 1;
    --press-reg: 0.35;
    --press-p: 1;
    transition: none;
  }
  [data-reveal], [data-reveal="pending"], [data-reveal="in"] {
    clip-path: none;
    animation: none;
    --line-reg: 1;
  }
  .furniture__target { rotate: 0deg; opacity: 0.3; }
}
