/* styles.css — casa-estatica's own stylesheet.
   NOT a vendored kit file (no @spine-kit tag) — this is the stack's own
   layout, built on top of tokens.css + motion.css + params-panel.css,
   which are linked before this file in every page's <head>.

   Rule: every color in this file resolves through a token declared in
   tokens.css — either a var() directly, or a color-mix() of one. There
   is no hex, rgb(), or named colour anywhere below, and nothing here
   redefines a --tp-* or Night Shift value.

   ── WHICH CONTEXT, AND WHY IT CHANGED ───────────────────────────────
   This file used to end its header with "No --tp-* token is read
   anywhere below", on the reasoning that a stack which is not one of
   the two rebuilt outward sites defaults to Context A alone. That was a
   misreading of CONTEXTS.md, and it cost the stack its whole palette:
   62 Rainbow tokens shipped in tokens.css and exactly three greys ever
   reached a pixel.

   The split now held here is the one colors.md actually describes:

     CHROME is Context A.  Near-black ground, --text / --muted copy,
                           --accent and --grad-cta on calls to action,
                           --grad on eyebrows and the brand dot. Every
                           text pair on the page is a Context A pair.
     ART is Context D.     The generated hero field, the section
                           ornaments, the page's light sources, and the
                           hairline card edges read the Rainbow ramp —
                           which colors.md sanctions by name for "hero
                           art, illustration accents."

   The rule that governs the boundary: a Rainbow hue may carry light,
   never language. No --tp-* value is ever a `color:` on body copy here;
   the warm hues in particular are decorative-only per colors.md, and
   they appear below only inside gradients, shadows, and 1px edges. */

/* ── stack-local variables ────────────────────────────────────────────
   tokens.css advertises its trailing block as "the ONLY place a stack
   may add its own variables" — but that file is vendored verbatim and
   check-spine-drift fails on any edit to it, so in practice a stack
   cannot put anything there without changing the shared spine for all
   three stacks at once. (Flagged rather than worked around silently.)
   These live here instead. None of them is a canonical token name, and
   none of them introduces a colour value: each is a recipe composed
   from tokens that already exist. */
:root {
  /* Fluid section rhythm, capped under the vendored --section-rhythm
     (96px). The gap a reader actually sees between two sections is
     rhythm + the thread's own top margin + the thread — around 165px on
     a desktop and 105px on a phone. Sizing this at the full 96px made
     that closer to 190px, which reads as an unfinished page rather than
     a generous one. */
  --rhythm: clamp(52px, 6.4vw, 84px);

  /* ── THE ELEVATION SCALE — BASE LAYER ───────────────────────────────
     ⚠ THIS BLOCK IS THE FALLBACK, AND IT IS THE ONE THAT MUST BE RIGHT.
     The richer values live in the @supports block below. Read that one
     second; a reader who only reads this one still sees a correct page,
     which is the entire point of the ordering.

     ── why a fallback exists at all: the failure mode is asymmetric ────
     There are two ways to spend a modern colour function, and they fail
     COMPLETELY differently. This is the mechanism, and it is worth
     holding on to because it decides where a guard is needed:

       color-mix() written DIRECTLY in a declaration
         → the declaration is invalid at PARSE time and is thrown away,
           so an earlier declaration for the same property still wins.
           Graceful. A preceding plain-token line is a full fallback.

       color-mix() written into a CUSTOM PROPERTY
         → the custom property parses fine (custom properties accept
           almost any token stream). The failure surfaces later, at
           SUBSTITUTION: var(--plane-1) resolves to something invalid at
           computed-value time, and the *using* declaration becomes
           `unset`. No earlier declaration rescues it, because the
           earlier declaration was never the loser of a cascade — it was
           overwritten by a var() that only failed afterwards.
           Catastrophic. `background-image: linear-gradient(var(--plane-1),
           var(--plane-1))` does not fall back to a darker card; it falls
           back to NO CARD, and the page returns to black-on-black.

     Every plane, edge, band and lift below went through the second door.
     That is why the whole elevation scale — not the odd decorative glow —
     is what gets a base layer here, and why the @supports block below
     re-declares custom properties rather than sprinkling guards at the
     use sites.

     ── what "old enough to need this" means ───────────────────────────
     color-mix() shipped in Firefox 113 / Safari 16.2 / Chrome 111, and
     :has() in Firefox 121 / Safari 15.4 / Chrome 105. A browser older
     than those lands here. NOTE, because it was measured rather than
     assumed: this is NOT the explanation for the 2026-07 "Firefox looks
     worse than Safari" report — that machine runs Firefox 153, which
     resolves both features, and Firefox/Chromium/WebKit were measured
     rendering this stylesheet to within 0.05 of a ratio point of each
     other. This layer is hardening for genuinely old clients. The
     Firefox report's real cause is recorded at the grain plate below.

     ── the original bug this scale fixes ──────────────────────────────
     The page used --surface (#111118) directly as its card fill, on a
     --bg (#0a0a0f) ground. That is 1.05:1 in token math — and MEASURED
     on rendered pixels it was 1.004:1, because the grain plate and the
     light rig lift the ground by almost exactly the surface's own delta.
     Every card, tile and panel was, literally, invisible as a surface:
     the operator's "black on black" report.

     What a plane is here: an OPAQUE colour, composed from tokens. Opaque
     on purpose — the previous fill sat over a fixed light rig, so the
     same card read differently depending on where the viewport put the
     glow behind it, and in the rig's bright corner the "raised" surface
     was darker than the ground beside it. An opaque plane is one value
     everywhere and can actually be verified with a pixel sample.

     ── the arithmetic ceiling, stated up front ───────────────────────
     On a near-black page these two requirements FIGHT, and the fight is
     not a matter of taste:

       body text (--text) on a plane >= 15:1   =>  plane Y <= 0.011625
       plane vs ground              >= 1.25:1  =>  plane Y >= 0.0125
                                                   (against PURE BLACK;
                                                    more against --bg)

     There is no value that satisfies both. WCAG's ratio adds 0.05 to
     both luminances, and near black that constant swamps the signal —
     so the whole usable range between "page" and "card" is worth about
     0.19 of a ratio point. Holding --text at 15:1 caps the surface at
     1.2325:1 over pure black and 1.1592:1 over --bg; holding --muted at
     6:1 caps it at 1.1853:1 over --bg. We hold the text floors (they
     are what a reader actually needs) and take the ceiling those allow.
     CIE L* is the honest metric down here: dL* went 0.19 -> ~6, i.e.
     from "no perceptible step" to roughly six just-noticeable ones.

     Levels: ground (--bg) < band < plane-1 (cards) < plane-2 (hover),
     with --plane-sunk for chips recessed INTO a card. Warmed toward the
     Rainbow ramp (--tp-peach) rather than lifted as flat grey, because
     the key light is warm and a neutral-grey lift under a warm light
     reads as a hole. The blue cast of --surface is pulled out against
     --tp-black first, so the warmth lands as warmth and not as violet.

     ── the base ladder ────────────────────────────────────────────────
     Built from the neutral ramp tokens.css already ships, in the order
     it already ships it: --bg (#0a0a0f) < --bg-alt (#0c0c14) <
     --surface (#111118) < --raised (#16161f). The card takes --raised,
     which is what that token is FOR ("raised card, hover") and which is
     the only rung that clears the bar: measured against the rendered
     ground, --raised is 1.099:1 / dL* +4.76 where --surface would be
     1.045:1 / +2.15 — under the 1.08 floor and most of the way back to
     invisible.

     --plane-2 (hover) deliberately EQUALS --plane-1 here. Nothing in the
     neutral ramp sits above --raised, and inventing a lighter value
     would be forking the design system for a hover state. Hover instead
     differentiates through --edge-hot and --lift-hover — a brighter lit
     edge and a deeper shadow, which is what a raised surface on a
     near-black page actually shows anyway. The @supports layer restores
     a real fill lift. */
  --plane-base: var(--surface);
  --plane-band: var(--bg-alt);
  --plane-1:    var(--raised);
  --plane-2:    var(--raised);
  --plane-sunk: var(--bg);

  /* Edges. The old ones were --border / --border-strong (0.08 / 0.16
     alpha white) composited onto the fill: 1.28:1 against a lifted
     plane, which is why replacing a flat 1px border with a gradient
     hairline made the page read FLATTER even as it made it prettier.

     The base layer keeps the warm→cool character rather than collapsing
     to a grey hairline, because tokens.css already ships pre-alpha'd
     hues for exactly this: the `--tp-*-soft` family (0.15 alpha). They
     are plain token references, so they need no colour function at all.
     That is the general move throughout this fallback — where the rich
     layer wants "this hue, quieter", the base layer reaches for the
     `-soft` token instead of mixing one. */
  --edge-warm:        var(--tp-sun-soft);
  --edge-neutral:     var(--border-strong);
  --edge-cool:        var(--tp-cyan-soft);
  --edge-warm-hot:    var(--tp-cream);
  --edge-neutral-hot: var(--border-strong);
  --edge-cool-hot:    var(--tp-cyan);

  /* a 1px edge that catches light: warm at the top-left corner, cool at
     the bottom-right, a neutral lift across the middle. Used as the
     second background layer of every raised surface, so the border area
     shows a gradient instead of a flat line. */
  --edge: linear-gradient(148deg,
      var(--edge-warm) 0%,
      var(--edge-neutral) 30%,
      var(--edge-neutral) 62%,
      var(--edge-cool) 100%);
  --edge-hot: linear-gradient(148deg,
      var(--edge-warm-hot) 0%,
      var(--edge-neutral-hot) 28%,
      var(--edge-neutral-hot) 60%,
      var(--edge-cool-hot) 100%);

  /* the lit lip along the top of a banded section — the band's fill is
     deliberately a whisper (about 1.4 dL*), and this line is what the
     eye actually catches. Boundary carries the cadence, not the fill.
     Base: --border-strong, the strongest neutral edge token there is. */
  --band-line: var(--border-strong);

  /* atmosphere gradient for banded sections — a warm radial glow that
     lifts the band fill subtly so it reads as a lit surface rather than
     a flat plane. Base: the same `-soft` token used by the light rig. */
  --band-atmos: var(--tp-ember-soft);

  /* --subtle (#6e6e80) is 3.96:1 on --bg — already under AA-normal
     BEFORE this pass, and the band would have pushed it to 3.84. The
     rich value sits between --subtle and --muted: quiet enough to keep
     the three-step text hierarchy, over 4.5:1 on every plane in this
     file. The base value is simply --muted — erring toward MORE
     contrast, which is the only safe direction for a fallback to err. */
  --quiet: var(--muted);

  /* depth: a 1px inner top highlight + one soft, far-offset ambient
     shadow. Two shadows, not five — a raised surface on a dark ground
     needs a lit edge and a shadow that reads as distance, and stacking
     more just turns to smoke. The shadow does little work on the bare
     ground (near-black has almost nowhere left to go down); it earns
     its keep over the band and under the rig's lit corner.

     Base uses --border-strong for the resting highlight and --tp-cream
     for the hover one, so hover still lights its own edge even where
     --plane-2 cannot lift. The ambient shadow takes opaque --tp-black;
     at 48px of blur behind 30px of negative spread it reads as the same
     soft distance the mixed version does. */
  --lift:
    inset 0 1px 0 0 var(--border-strong),
    0 24px 48px -30px var(--tp-black);
  --lift-hover:
    inset 0 1px 0 0 var(--tp-cream),
    0 30px 60px -28px var(--tp-black);

  /* ── the Rainbow's vertical presence ──────────────────────────────
     The section divider used to be a 1px thread. It is now a ribbon
     with real height — see .thread. One variable so the SVG, the CSS
     fallback and the small-screen override can never drift apart. */
  --ribbon-h: clamp(54px, 5.6vw, 76px);

  /* ── THEME-SWITCHED PAINTS AND PLATES ─────────────────────────────
     Everything a theme has to re-aim, pulled out of its rule and into a
     variable so the rule itself is written once. The theme layer at the
     foot of this file overrides these five groups and nothing else
     touches a selector.

     Base values are the plain-token fallbacks; the @supports block
     below upgrades them; the light theme (also inside @supports)
     replaces them. */
  --rig-1: var(--tp-orange-soft);   /* warm key, high left      */
  --rig-2: var(--tp-cyan-soft);     /* cool counter, top right  */
  --rig-3: var(--tp-ocean-soft);    /* cool bounce, from below  */
  --rig-4: var(--tp-ember-soft);    /* warm spill, lower right  */
  --grain-op: 0.34;                 /* see the grain plate below */
  --bloom: var(--tp-ember-soft);    /* the hero mark's spill onto the page */
  --bloom-cool: var(--tp-ocean-soft); /* cool counter-glow behind the hero */

  /* The three places a gradient is poured into type or a control. Named
     rather than inlined because on paper none of the three survives:
     --grad-title is white→lavender, --grad and --grad-cta run through a
     1.56:1 amber and a 3.91:1 purple. */
  --brand-paint: var(--grad);       /* eyebrow + brand dot */
  --title-paint: var(--grad-title); /* h1 */
  --cta-fill:    var(--grad-cta);   /* primary button fill */
  --cta-ink:     var(--bg);         /* primary button text */
  --cta-glow:    var(--tp-ember-soft);

  /* The full spectral sweep, warm through cool, used at the top lip of a
     banded section. Raw hues at full saturation: this is a 3px line of
     LIGHT, never a text colour, which is the boundary this file holds. */
  --band-spectrum: linear-gradient(90deg,
      transparent 0%,
      var(--tp-sun)   14%,
      var(--tp-ember) 34%,
      var(--tp-red)   50%,
      var(--tp-teal)  72%,
      var(--tp-cyan)  88%,
      transparent 100%);
}

/* ── THE ELEVATION SCALE — ENHANCEMENT LAYER ─────────────────────────
   Everything above is complete and correct on its own. This block makes
   it richer where the engine can afford it, and it is the ONLY place
   color-mix() touches a custom property — which is the one door with the
   catastrophic failure mode (see the base layer's note).

   Warmed toward the Rainbow ramp (--tp-peach) rather than lifted as flat
   grey, because the key light is warm and a neutral-grey lift under a
   warm light reads as a hole. The blue cast of --surface is pulled out
   against --tp-black first, so the warmth lands as warmth, not violet.
   The edges are opaque and composed off the plane they sit on, so every
   stop clears 1.5:1 against its own surface by construction.

   ── do not "tidy" the feature test back to `red 50%, blue` ────────────
   Every @supports colour test in this file mixes currentColor with
   transparent. The idiomatic probe is `color-mix(in srgb, red 50%, blue)`
   — and it would put two named colours in a stylesheet whose standing
   constraint is zero hex, zero rgb(), zero named colours. currentColor
   and transparent are CSS-wide keywords rather than named colours, they
   probe exactly the same function, and they keep the file honest. A
   future editor who "corrects" this back breaks the constraint without
   changing a single rendered pixel, which is the hardest kind of
   regression to notice. */
@supports (color: color-mix(in srgb, currentColor 50%, transparent)) {
  :root {
    --plane-base: color-mix(in srgb, var(--surface) 70%, var(--tp-black));
    --plane-band: color-mix(in srgb, var(--tp-peach)  3.5%, var(--bg));
    --plane-1:    color-mix(in srgb, var(--tp-peach)  6.5%, var(--plane-base));
    --plane-2:    color-mix(in srgb, var(--tp-peach) 11.5%, var(--plane-base));
    --plane-sunk: color-mix(in srgb, var(--tp-ember)    3%, var(--tp-black));

    --edge-warm:        color-mix(in srgb, var(--tp-cream) 38%, var(--plane-1));
    --edge-neutral:     color-mix(in srgb, var(--tp-paper) 18%, var(--plane-1));
    --edge-cool:        color-mix(in srgb, var(--tp-cyan)  32%, var(--plane-1));
    --edge-warm-hot:    color-mix(in srgb, var(--tp-cream) 46%, var(--plane-2));
    --edge-neutral-hot: color-mix(in srgb, var(--tp-paper) 30%, var(--plane-2));
    --edge-cool-hot:    color-mix(in srgb, var(--tp-cyan)  50%, var(--plane-2));

    --band-line: color-mix(in srgb, var(--tp-cream) 22%, var(--plane-band));
    --band-atmos: color-mix(in srgb, var(--tp-peach) 26%, transparent);
    --quiet:     color-mix(in srgb, var(--muted) 62%, var(--subtle));

    --lift:
      inset 0 1px 0 0 color-mix(in srgb, var(--tp-cream) 22%, transparent),
      0 24px 48px -30px color-mix(in srgb, var(--tp-black) 96%, transparent);
    --lift-hover:
      inset 0 1px 0 0 color-mix(in srgb, var(--tp-cream) 34%, transparent),
      0 30px 60px -28px color-mix(in srgb, var(--tp-black) 98%, transparent);

    --rig-1: color-mix(in srgb, var(--tp-orange) 30%, transparent);
    --rig-2: color-mix(in srgb, var(--tp-cyan)   26%, transparent);
    --rig-3: color-mix(in srgb, var(--tp-ocean)  32%, transparent);
    --rig-4: color-mix(in srgb, var(--tp-ember)  14%, transparent);
    --bloom: color-mix(in srgb, var(--tp-ember)  24%, transparent);
    --bloom-cool: color-mix(in srgb, var(--tp-ocean) 20%, transparent);
    --cta-glow: color-mix(in srgb, var(--tp-ember) 60%, transparent);
  }
}

*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  /* html carries the ground too, so body's background never propagates
     to the viewport canvas — which would otherwise paint ABOVE body's
     own z-index:-1 pseudo-elements and hide the light rig entirely. */
  background: var(--bg);
}
body {
  margin: 0;
  position: relative;
  isolation: isolate;   /* makes z-index:-1 below mean "behind body's
                           content", not "behind body's background" */
  background: var(--bg);
  color: var(--text);
  font: 16px/1.65 var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ── the light rig ────────────────────────────────────────────────────
   Two fixed, viewport-anchored layers under everything: a set of radial
   sources, and a grain plate. Fixed rather than folded into body's
   background shorthand so the light stays put while the page scrolls —
   a glow that scrolls away reads as a picture, not as lighting.
   Both are pointer-events:none and neither carries information. */
body::before,
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
/* A warm key light high on the left, a cool counter-source top-right,
   and a cool bounce from below.

   Raised from 11–17% alpha: at those values the rig was doing nothing a
   viewer could name, and the page read as an unlit void. But alpha is
   not free — every unit of it lifts the GROUND, and the ground is one
   half of the surface-separation ratio the elevation scale above is
   fighting for. So the spend is deliberately uneven:

     - the lobes are TIGHTER and pushed off-canvas, so the light lands
       on the page's corners and the hero rather than spreading evenly
       under the card rows;
     - the cool sources take the bigger raise. --tp-ocean and --tp-cyan
       carry roughly a third of --tp-orange's relative luminance per
       unit alpha (the coefficient on the blue channel is 0.0722, on
       green 0.7152), so a cool glow buys about three times as much
       visible colour per point of ground lift. Coloured light at low
       luminance is exactly what a dark page wants. */
/* Four lobes, one rule. The GEOMETRY is fixed and the COLOUR is a
   variable (--rig-1..4), because that is the axis every layer of this
   file varies on: the plain-token fallback, the color-mix() upgrade, and
   the light theme all want these same four ellipses in these same four
   places, filled differently. Base values are the `-soft` tokens
   (0.15/0.16 alpha), dimmer than the mixed ones — for a fallback that is
   the right direction to be wrong in, since a dimmer rig lifts the
   ground less and the elevation ladder above gets MORE of the contrast
   budget, not less. */
body::before {
  background:
    radial-gradient(72% 48% at 0% -14%,   var(--rig-1), transparent 66%),
    radial-gradient(58% 42% at 100% -6%,  var(--rig-2), transparent 64%),
    radial-gradient(92% 50% at 50% 114%,  var(--rig-3), transparent 70%),
    radial-gradient(70% 44% at 96% 78%,   var(--rig-4), transparent 72%);
}
/* Grain. feTurbulence rendered through a data URI: no request, nothing
   to cache-bust, works with JS off. Desaturated in the filter chain so
   it textures without tinting.

   The blend mode is the fix, not the opacity. This plate used to sit in
   NORMAL blending at 5.5%, which meant it added mid-grey to a near-black
   ground: measured, it lifted the page ground to almost exactly the old
   card fill, and that — not the token values — is what made the cards
   vanish. fractalNoise is centred on 0.5, and `overlay` against a base
   below 0.5 resolves to 2 x base x blend, so a plate centred on 0.5 is
   the IDENTITY in the mean: it modulates the ground up and down in equal
   measure and adds nothing to it. That buys a far more present grain
   (0.055 -> 0.34) for zero luminance cost.

   ── this plate WAS the cross-browser bug, and the numbers say so ─────
   The 2026-07 report was "my firefox doesn't let me see the same quality",
   then, from Safari, "there we go... looking good" — same URL, same
   deploy. That was read as a missing color-mix() in an old Firefox. It
   was not: the reporting machine runs Firefox 153, which resolves
   color-mix() and :has() both, and all three engines were measured
   rendering this file to within 0.05 of a ratio point.

   What actually differed was HERE, in the previous version of this rule,
   which blended NORMAL at 0.055. Measured on the deployed build, at the
   same probe, same scroll position:

     engine     card fill        page ground      dL* (card - ground)
     Firefox    rgb(17,17,24)    rgb(16,16,20)    +0.57
     Chromium   rgb(17,17,24)    rgb(21,18,20)    -0.50
     WebKit     rgb(17,17,24)    rgb(21,18,20)    -0.50

   The CARD is identical in all three. The GROUND is not: a normal-blended
   grey plate over a fixed, viewport-anchored light rig composites
   differently in Gecko than in Blink/WebKit, and the divergence lands
   entirely on the ground — the half of the ratio nobody was watching.
   Both sides were ruined (1.01:1 either way), but ruined DIFFERENTLY,
   which is exactly what "looks fine over there, wrong over here" is.

   Moving the plate to `overlay` removes the divergence rather than
   papering over it: overlay against a base below 0.5 resolves to
   2 x base x blend, so a plate centred on 0.5 is the identity in the
   mean and adds no luminance for any engine to disagree about. Measured
   after the change, all three engines return ground rgb(11,11,16) and
   card rgb(30,26,25). The fix is the blend mode, and the reason it is
   the fix is that it makes the ground engine-independent.

   Support: mix-blend-mode ships everywhere since 2015 — strictly earlier
   than color-mix(), so any engine old enough to need the base layer above
   still has blend modes. The guard below is belt-and-braces for the one
   client that has neither: without it, a normal-blended plate at 0.34
   would lift the ground straight back into the old bug.

   ── ON PAPER THE BRANCH FLIPS. THE MEAN SURVIVES; THE GAIN DOES NOT ──
   Overlay picks its formula from the BASE, not the blend: 2·b·s below
   0.5, and 1 − 2(1−b)(1−s) at or above it. The light theme moves the
   ground from b≈0.043 to b≈0.90, so paper takes the second branch. Two
   consequences, and they are NOT the same consequence:

     the mean SURVIVES — and this is the load-bearing half. At s = 0.5
       the upper branch gives 1 − 2(1−b)(0.5) = b, the identity, exactly
       as the lower branch gives 2·b·0.5 = b. A plate centred on 0.5
       still adds no luminance on either side, so the whole
       engine-independence argument above holds unchanged in light mode.
       Worth stating plainly, because "the branch flipped" reads like
       "the maths broke" and the part that mattered did not.

     the GAIN does not survive. d/ds is 2b below and 2(1−b) above:
       ~0.098 at the near-black ground, ~0.21 on paper, i.e. 2.1× more
       swing per unit of plate. On top of that a unit of sRGB buys more
       L* near white than near black (measured at the two grounds:
       ~89 vs ~70 per unit), and 8-bit quantisation is coarser up there
       too (0.34 L* per step vs 0.27), so the same number of flipped
       pixels reads louder.

   Theory gets the direction and about half the magnitude; the plate was
   then swept and measured rather than trusted. Grain-only standard
   deviation of L* over an 8300-pixel detrended ground strip, plate on
   minus plate off, casa-estatica at 1280px:

     dark  op 0.34  ->  0.108        light  op 0.055  ->  0.074
                                     light  op 0.060  ->  0.096
                                     light  op 0.065  ->  0.107   <- match
                                     light  op 0.070  ->  0.122
                                     light  op 0.130  ->  0.141
                                     light  op 0.340  ->  0.266

   So light runs at 0.065 — a 5.2× cut, not the 2.7× the branch algebra
   alone predicts. Equal PERCEIVED texture, not equal opacity, and the
   number came off the pixels. */
body::after {
  opacity: var(--grain-op);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url%28%23g%29'/%3E%3C/svg%3E");
}
/* No blend modes: drop the plate rather than let it add grey to the
   ground. Texture is worth having; it is not worth the elevation scale. */
@supports not (mix-blend-mode: overlay) {
  body::after { opacity: 0; }
}

img, svg { max-width: 100%; display: block; }
a { color: inherit; }

/* padding-INLINE, not the `padding: 0 24px` shorthand this replaces.
   That shorthand also set padding-block: 0, and because a class beats
   two type selectors it silently overrode `main section`'s
   padding-block for every section on every page — so --section-rhythm
   was declared, documented, and dead, and sections butted together with
   nothing between them but a 1px rule. */
.wrap { max-width: var(--wrap); margin: 0 auto; padding-inline: 24px; }
.wrap-narrow { max-width: var(--wrap-narrow); margin: 0 auto; padding-inline: 24px; }

/* ── skip link ─────────────────────────────────────────────────── */
.skip-link {
  position: absolute;
  left: 12px;
  top: -48px;
  background: var(--accent);
  color: var(--bg);
  padding: 10px 16px;
  border-radius: var(--r-badge);
  font: 600 14px var(--font-sans);
  text-decoration: none;
  z-index: 100;
  transition: top 160ms ease;
}
.skip-link:focus { top: 12px; }

/* ── header / nav ──────────────────────────────────────────────── */
.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  min-height: var(--header-height);
  display: flex;
  align-items: center;
  /* Base: OPAQUE. A translucent header without a blur behind it is not a
     softer header, it is content scrolling visibly through the nav — so
     the translucency is gated on the blur that makes it work, not merely
     on the colour function that expresses it. */
  background: var(--bg);
  /* --border (0.08 alpha white) over near-black is a 1.28:1 line — the
     header floated with no bottom at all. --border-strong is the same
     token family at twice the alpha and reads as an actual edge. */
  border-bottom: 1px solid var(--border-strong);
}
@supports ((backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px))) {
  .site-head {
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
}
.nav { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  font-weight: 800;
  font-size: 18px;
  letter-spacing: -0.02em;
  color: var(--text);
}
.brand-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--brand-paint);
  flex: none;
}
.nav-links { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.nav-links a {
  text-decoration: none;
  color: var(--muted);
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
}
.nav-links a:hover, .nav-links a:focus-visible { color: var(--text); }
.lang-pair { font: 600 13px var(--font-mono); color: var(--muted); letter-spacing: 0.02em; }
.lang-pair .lang-sep { padding: 0 4px; opacity: 0.5; }
.lang-link { color: var(--muted); text-decoration: none; }
.lang-link:hover, .lang-link:focus-visible { color: var(--accent); }
.btn-nav {
  background: var(--accent);
  color: var(--bg) !important;
  padding: 9px 16px;
  border-radius: var(--r-chip);
  font-weight: 700 !important;
}

/* ── generic buttons ───────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 22px;
  border-radius: var(--r-chip);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: -0.005em;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: filter 160ms ease, border-color 160ms ease, color 160ms ease;
}
/* color: var(--bg), not a literal hex — chosen for contrast against the
   WORST stop of --grad-cta's three-color sweep (the purple end), which a
   token-name-only audit can't see since it never resolves a gradient's
   individual color stops. Verified by hand: text vs each stop is
   13.14:1 (amber) / 7.15:1 (pink) / 4.66:1 (purple) — all >=4.5:1 AA-
   normal at this button's 15px/700 weight (which does not qualify for
   the 3:1 large-text threshold; that needs >=18.66px bold). */
.btn-primary {
  background: var(--cta-fill);
  color: var(--cta-ink);
  box-shadow: 0 14px 30px -18px var(--cta-glow);
}
.btn-primary:hover, .btn-primary:focus-visible { filter: brightness(1.06); }
.btn-ghost { background: transparent; color: var(--text); border-color: var(--border-strong); }
.btn-ghost:hover, .btn-ghost:focus-visible { border-color: var(--accent); color: var(--accent); }

/* ── layout rhythm ─────────────────────────────────────────────────
   The section divider is the generated thread at the end of each
   section, not a border — see .thread below. */
main section, main > .band { padding-block: var(--rhythm); }
section[id] { scroll-margin-top: calc(var(--header-height) + 14px); }

/* ── section cadence ───────────────────────────────────────────────
   Sections used to differ only by the thread hairline at their foot, so
   the page read as one undifferentiated column and the eye had nothing
   to count. Alternate sections now sit on --plane-band with a lit lip
   along the top.

   Full-bleed WITHOUT touching the markup and without `100vw`: a spread
   box-shadow paints the band outward past the .wrap measure, and
   clip-path trims that spread back to the element's own top and bottom.
   box-shadow never contributes to scrollable overflow, so unlike the
   `left:50%; width:100vw` idiom this cannot produce a horizontal
   scrollbar on a platform with classic (space-taking) scrollbars, and
   needs no `overflow: clip` anywhere — which would have risked the
   sticky header.

   Both pseudo-elements are z-index:-1, so in body's isolation context
   they paint after the light rig and BEFORE the grain plate: ground ->
   band -> grain -> content, which is the order a real surface stack
   has. Decoration only; nothing here is load-bearing with JS off or
   with motion disabled.

   The :has() clause is the whole reason this reads as elevation rather
   than as noise. A band lifts the ground under everything in its
   section — including any card sitting on it — and on a near-black page
   the entire card-to-ground budget is about 0.16 of a ratio point, so a
   band under a card row spends a quarter of it on decoration. Bands go
   where there is nothing raised to compete with; sections that carry
   cards, stat tiles or a panel keep the darkest possible ground so the
   surfaces on them get the full budget. Measured: a card on bare ground
   is 1.145:1 / 5.9 dL* against its surround, on a band 1.102:1 / 5.0.

   The whole block is inside @supports selector(:has(*)). Strictly it need
   not be — a selector an engine cannot parse takes its rule down with it,
   so the band would drop on its own — but making the dependency explicit
   is what lets the next reader see, without testing, that the fallback
   page is a page with NO bands rather than a page with broken ones. It
   also means the fallback can be proven by stripping @supports blocks,
   which is how the numbers in this file's header were obtained.

   Losing the band costs the page less than it looks: the section cadence
   is carried by the ribbon at each section's foot (see .thread), which
   needs no :has() and no colour function at all.

   clip-path is in the condition for a harder reason than tidiness. The
   full-bleed trick is a 100vmax spread shadow TRIMMED back to the
   element's own top and bottom. Without the trim the shadow still
   paints — 100vmax in every direction — and the whole page turns
   --plane-band at z-index -1, erasing the ground/band distinction the
   band exists to create. A feature this rule cannot survive the loss of
   belongs in its @supports, not in a comment. */
@supports selector(:has(*)) and (clip-path: inset(0 -1px)) {
  main section:nth-of-type(even):not(:has(.cap-card, .stat, .params-panel)) { position: relative; }
  main section:nth-of-type(even):not(:has(.cap-card, .stat, .params-panel))::before,
  main section:nth-of-type(even):not(:has(.cap-card, .stat, .params-panel))::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    z-index: -1;
    pointer-events: none;
    clip-path: inset(0 -100vmax);
  }
  main section:nth-of-type(even):not(:has(.cap-card, .stat, .params-panel))::before {
    top: 0;
    bottom: 0;
    background:
      /* A subtle atmosphere gradient — warm towards the centre of the
         band, cooling at the edges. Follows the same axis as the light
         rig, so the band reads as a lit surface rather than a flat fill.
         Low opacity — it's atmosphere, not another colour stop. */
      radial-gradient(56% 68% at 20% 30%, var(--band-atmos), transparent 72%),
      var(--plane-band);
    box-shadow: 0 0 0 100vmax var(--plane-band);
  }
  /* ── the Rainbow's second home ────────────────────────────────────
     The lit lip is now 3px and spectral, not 1px and cream. Two colours
     in one element, on purpose: the ELEMENT's own background paints the
     spectrum, and it stops at the .wrap measure because that is how wide
     the section actually is; the box-shadow spread that carries the line
     out to the viewport edges can only be a flat colour, so it stays the
     quiet --band-line. The read is a spectral segment sitting inside the
     content measure with a hairline running off both sides of it —
     which is the correct emphasis anyway, since the measure is where the
     content is.

     One per banded section, and bands are already rationed (even
     sections, minus any that carry a raised surface). On index.html that
     is exactly one. The ribbon is the bold moment; this is its echo. */
  main section:nth-of-type(even):not(:has(.cap-card, .stat, .params-panel))::after {
    top: 0;
    height: 3px;
    background: var(--band-spectrum);
    box-shadow: 0 0 0 100vmax var(--band-line);
  }
}

/* ── type scale ────────────────────────────────────────────────────
   Three real steps with distinct tracking, not one size ramp: display
   type gets tight negative tracking because large glyphs need less air
   between them, caption/eyebrow gets wide positive tracking because
   small uppercase needs more, and body sits at neutral. Measure is
   capped in ch so the column stays readable however wide the viewport
   gets. System stack only — no webfont anywhere in this palette. */
h1, h2, h3 { margin: 0 0 14px; text-wrap: balance; }
/* ── gradient text, and why it is the most dangerous rule in this file ──
   `background-clip: text` + `color: transparent` is the standard way to
   pour a gradient into type. It is also the standard way to ship an
   INVISIBLE HEADLINE: the two declarations are independent, so an engine
   that drops the clip still honours the transparent colour, and the
   result is not "an ungradiented h1" — it is nothing at all, on the one
   element the page cannot afford to lose.

   Unlike the color-mix() sites, this failure does not need an old engine
   to appear. Any condition that strips the clip while leaving `color`
   standing produces it. So the transparent colour is never stated
   outside the @supports that earns it: base is solid --text, and the
   gradient is added only where the clip is known to work. */
h1 {
  font-size: clamp(38px, 7.4vw, 74px);
  line-height: 1.02;
  letter-spacing: -0.035em;
  font-weight: 800;
  color: var(--text);
}
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  h1 {
    background: var(--title-paint);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
h2 {
  font-size: clamp(27px, 3.5vw, 40px);
  line-height: 1.08;
  letter-spacing: -0.026em;
  font-weight: 750;
  color: var(--text);
}
h3 {
  font-size: 18.5px;
  line-height: 1.28;
  letter-spacing: -0.014em;
  font-weight: 700;
  color: var(--text);
}
p { color: var(--muted); margin: 0 0 16px; max-width: 68ch; }
.lede {
  font-size: clamp(16.5px, 1.4vw, 18.5px);
  line-height: 1.6;
  max-width: 60ch;
  color: var(--muted);
}
/* Wide positive tracking is the point of an eyebrow, but it also makes
   the line ~25% longer than it looks in a mockup — long labels wrap, and
   at line-height 1 the two lines collide. 1.5 and a capped measure keep
   a wrapped eyebrow legible instead of overlapped. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font: 700 11.5px/1.5 var(--font-mono);
  letter-spacing: 0.16em;
  /* 68ch, not 52 — `ch` is measured WITHOUT letter-spacing, and 0.16em of
     it makes each glyph ~25% wider than the unit assumes, so a 52ch cap
     actually wrapped a 41-character eyebrow. The cap is here to stop an
     absurdly long label running the width of the page, not to wrap a
     normal one. */
  max-width: 68ch;
  text-transform: uppercase;
  /* Same trap as h1 — solid base, gradient only where the clip works.
     --accent (not a --tp-* hue) keeps this a Context A text colour, so
     the fallback never puts a Rainbow value on language. */
  color: var(--accent);
  margin: 0 0 16px;
}
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .eyebrow {
    background: var(--brand-paint);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
.eyebrow-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
  box-shadow: 0 0 12px var(--gold-glow);
}
@supports (color: color-mix(in srgb, currentColor 50%, transparent)) {
  .eyebrow-dot { box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 70%, transparent); }
}

/* ── raised surfaces ───────────────────────────────────────────────
   The border is 1px of TRANSPARENT, and --edge shows through it as the
   second background layer (background-clip: padding-box, border-box).
   That is the whole trick: a flat `1px solid var(--border)` cannot vary
   along its length, so every card edge on a dark page reads identically
   and the page goes flat. A gradient hairline catches light on one
   corner and cools on the other, which is what an edge does.

   ── the remaining version-sensitive features, and why they are unguarded ──
   Audited alongside the color-mix()/:has() work rather than left implicit.
   Each one either predates the base layer's own trigger, or degrades to
   something merely plainer instead of something broken:

     background-clip: padding-box, border-box (this rule) — multi-value
       background-clip is IE9-era. If it were lost, the opaque plane layer
       would cover the border box and the gradient edge would stop showing;
       the card keeps its fill, its shadow and its radius. Plainer, not
       broken, and no engine that can run this file lacks it.
     inset: 0 (the light rig, .hero-mark::before) — Chrome 87 / FF 66 /
       Safari 14.1, all strictly older than color-mix(). Any engine that
       reached the base layer for colour reasons already has it.
     text-wrap: balance (h1/h2/h3) — an unsupported value is dropped and
       headings wrap normally. Purely typographic.
     100svh (.error-wrap) — falls back to no min-height, so the 404 body
       sits at its natural height instead of vertically centred.

   Guarded elsewhere because they FAIL rather than degrade: background-clip
   on text (invisible headline), mix-blend-mode on the grain plate (ground
   lift), backdrop-filter on the header (see-through nav), clip-path on the
   band (100vmax flood). */
.cap-card,
.stat,
.params-panel {
  border: 1px solid transparent;
  border-radius: var(--r-card);
  background-image: linear-gradient(var(--plane-1), var(--plane-1)), var(--edge);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  box-shadow: var(--lift);
}
/* The fill is FLAT, not a top-lit vertical gradient. A gradient would be
   the obvious way to make a plane look lit — but --plane-1 already sits
   on the ceiling that keeps --text at 15:1, so a lighter top band would
   put the card's heading on the one strip of the card that fails the
   floor. The 1px inner highlight in --lift does that job instead: it
   lights the edge, which is where a real lit surface shows it. */

/* ── hero ──────────────────────────────────────────────────────── */
.hero { padding-block: 0 var(--rhythm); }
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(200px, 330px);
  gap: clamp(28px, 4.6vw, 62px);
  align-items: center;
}
.hero-copy .thesis {
  font-size: clamp(17px, 2.1vw, 21px);
  line-height: 1.5;
  letter-spacing: -0.008em;
  max-width: 46ch;
  /* base errs bright: full --text is more contrast than the mix, and a
     fallback that is too legible is not a bug. */
  color: var(--text);
}
@supports (color: color-mix(in srgb, currentColor 50%, transparent)) {
  .hero-copy .thesis { color: color-mix(in srgb, var(--text) 78%, var(--muted)); }
}
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 22px; }
.hero-mark {
  position: relative;
  z-index: 0;
  display: flex;
  justify-content: center;
}
/* the aperture's own spill onto the page — a soft ember bloom sitting
   behind the SVG, so the art belongs to the page instead of being
   pasted onto it */
.hero-mark::before {
  content: "";
  position: absolute;
  inset: -14% -14% -20%;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--bloom), transparent 76%);
  filter: blur(20px);
}
/* A cool counter-glow on the opposite side — large, soft, and behind the
   warm bloom, so the hero mark floats in atmosphere rather than sitting on
   a flat ground. Decorative only, aria-hidden container. The --bloom-cool
   token is the light theme's warm-to-cool complement. */
.hero-mark::after {
  content: "";
  position: absolute;
  inset: -30% -20% -40%;
  z-index: -2;
  border-radius: 50%;
  background: radial-gradient(closest-side at 70% 30%, var(--bloom-cool), transparent 72%);
  filter: blur(44px);
  opacity: 0.55;
  pointer-events: none;
}
.hero-mark svg { width: 100%; height: auto; border-radius: 50%; }

@media (min-width: 760px) {
  .hero { padding-block-start: clamp(24px, 3.4vw, 56px); }
}
@media (max-width: 760px) {
  .hero-grid { grid-template-columns: 1fr; }
  /* The mark stays AFTER the copy — no `order: -1`. It is aria-hidden decoration,
     and leading with it on a phone pushed the headline 292px down in English and
     408px in Spanish: half the first screen spent on a shape that screen readers
     are explicitly told to ignore. Content first, decoration second. Measured on
     the live showcase POC, which inherited this from the template.
     The hero also keeps ZERO top padding at this width, for the same reason —
     the desktop rule above is deliberately min-width-scoped, not a clamp that
     would leak 20-odd pixels onto the phone first screen. */
  .hero-mark { max-width: 200px; margin: 28px auto 0; }
}

/* ── section ornament — THE RIBBON ─────────────────────────────────
   A seeded spectral ribbon closing each section, in place of a flat
   border. app.js redraws each one from the session seed plus a
   per-section offset, so no two are alike and a re-seed re-cuts them
   together.

   ── why this stopped being a hairline ──────────────────────────────
   It used to be a 1px thread. The spectrum was present and it had no
   presence: at one pixel the palette is a rumour, and the page's only
   generated-per-section element was also its least visible one. It is
   now 54–76px tall — an event, not a rule.

   Height is the cheap half. The expensive half is that a big shape made
   of one flat gradient reads as a printed bar, which is the opposite of
   what a seeded ornament should say. So the ribbon is built in three
   depths, and each depth does a different job:

     halo   100% of the height, `-soft` alpha  — the colour a reader
            feels before they look at it, and what makes the divider
            occupy space rather than mark a line.
     body   38% of the height, `-soft` again   — the second wash doubles
            alpha where the two overlap, so saturation RAMPS toward the
            middle instead of stepping. This is what stops the flat-bar
            read without any blur.
     core   3px, raw hue at full strength      — the filament. The eye
            lands here; the halo tells it how far the colour reaches.

   Everything below is the JS-OFF state, and it is a complete ribbon, not
   a placeholder for one: same three depths, same section palettes, same
   height. With JS on, app.js replaces it with a curved, ticked, seeded
   version of the same idea. Decoration degrading to simpler decoration —
   never load-bearing, per Motion Rules 0/0a. */
.thread {
  --t1: var(--tp-sun);
  --t2: var(--tp-ember);
  --t3: var(--tp-teal);
  --t4: var(--tp-navy);
  --t1s: var(--tp-sun-soft);
  --t2s: var(--tp-ember-soft);
  --t3s: var(--tp-teal-soft);
  --t4s: var(--tp-navy-soft);
  display: block;
  width: 100%;
  height: var(--ribbon-h);
  /* The negative bottom margin is what puts the ribbon ON the boundary
     between two sections — it eats the section's own padding-bottom, so
     it lands where the old 1px border-bottom used to, rather than adding
     a third band of space to the stack. The top margin came DOWN as the
     height went up (was clamp(32px,4.4vw,56px)); the element now brings
     its own presence and no longer needs to be announced by a gap. */
  margin-block: clamp(12px, 1.9vw, 26px) calc(var(--rhythm) * -1 + 2px);
  background:
    /* core — the saturated filament */
    linear-gradient(90deg,
      transparent 3%, var(--t1) 18%, var(--t2) 42%, var(--t3) 66%, var(--t4) 85%, transparent 97%)
    left center / 100% 3px no-repeat,
    /* body — the ramp */
    linear-gradient(90deg,
      transparent 0%, var(--t1s) 15%, var(--t2s) 42%, var(--t3s) 68%, var(--t4s) 86%, transparent 100%)
    left center / 100% 38% no-repeat,
    /* halo — the reach */
    linear-gradient(90deg,
      transparent 0%, var(--t1s) 20%, var(--t2s) 45%, var(--t3s) 70%, var(--t4s) 88%, transparent 100%)
    left center / 100% 100% no-repeat;
}
/* Per-section palettes. Every hue here has BOTH a raw and a `-soft` form
   in tokens.css, which is why each palette carries four pairs and not
   three — a fourth deep stop anchors the spectrum on the cool side so
   the warmer three don't all drift mid-range. */
main section:nth-of-type(2) .thread {
  --t1: var(--tp-orange); --t2: var(--tp-red); --t3: var(--tp-cyan); --t4: var(--tp-navy);
  --t1s: var(--tp-orange-soft); --t2s: var(--tp-red-soft); --t3s: var(--tp-cyan-soft); --t4s: var(--tp-navy-soft);
}
main section:nth-of-type(3) .thread {
  --t1: var(--tp-sun);   --t2: var(--tp-lime);    --t3: var(--tp-ocean); --t4: var(--tp-ink);
  --t1s: var(--tp-sun-soft);   --t2s: var(--tp-lime-soft);    --t3s: var(--tp-ocean-soft); --t4s: var(--tp-ink-soft);
}
main section:nth-of-type(4) .thread {
  --t1: var(--tp-ember); --t2: var(--tp-emerald); --t3: var(--tp-ocean); --t4: var(--tp-navy);
  --t1s: var(--tp-ember-soft); --t2s: var(--tp-emerald-soft); --t3s: var(--tp-ocean-soft); --t4s: var(--tp-navy-soft);
}
/* app.js swaps the class once it has drawn the real thing */
.thread-live { background: none; }
.thread svg { width: 100%; height: var(--ribbon-h); display: block; }

/* ── capability cards ──────────────────────────────────────────── */
.cap-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 20px;
  margin-top: 36px;
}
.cap-grid[hidden] { display: none; }
.cap-card {
  padding: 26px 24px;
  transition: box-shadow 200ms ease;
}
.cap-card:hover {
  box-shadow: var(--lift-hover);
  background-image: linear-gradient(var(--plane-2), var(--plane-2)), var(--edge-hot);
}
.cap-card h3 { margin-bottom: 10px; }
.cap-card p { font-size: 14.5px; margin-bottom: 14px; max-width: none; }
.cap-status {
  display: inline-block;
  font: 700 10.5px var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 5px 11px;
  border-radius: var(--r-badge);
  /* recessed INTO the card, not another raised thing on top of it —
     --plane-sunk is below --plane-1, so the chip reads as a slot. */
  background: var(--plane-sunk);
  color: var(--live);
}
.cap-status[data-agent-status="wip"] { color: var(--gated); }

/* ── proof / about ─────────────────────────────────────────────── */
.about-grid { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: clamp(32px, 4.4vw, 56px); align-items: start; }
.about-copy .textlink { color: var(--accent); font-weight: 700; text-decoration: none; }
.about-copy .textlink:hover, .about-copy .textlink:focus-visible { text-decoration: underline; }
.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.stat { padding: 20px 16px; text-align: center; }
/* A flat --accent, not the --grad sweep the eyebrows use. These numbers
   are information, and the rule this file holds is that a gradient may
   carry light but never language — at 32px the gradient's pink midpoint
   dominated and the figures read as ornament instead of data. */
.stat-num {
  display: block;
  font: 800 32px/1 var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
  color: var(--accent);
}
.stat-label { display: block; font-size: 12.5px; line-height: 1.4; color: var(--muted); margin-top: 8px; }
@media (max-width: 640px) {
  .about-grid { grid-template-columns: 1fr; }
  .stat-row { grid-template-columns: 1fr; }
}

/* ── contact ───────────────────────────────────────────────────── */
.contact-actions { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; margin-top: 24px; }
/* --quiet, not --subtle: see the token's note above. --subtle was 3.96:1
   on --bg before this pass — under AA-normal — and the band under this
   section would have taken it to 3.84. */
.contact-meta { font-size: 14px; color: var(--quiet); margin-top: 18px; }

/* ── footer ────────────────────────────────────────────────────── */
.site-foot { padding-block: 64px 44px; }
.foot-grid { display: flex; flex-direction: column; gap: 30px; }
.foot-top { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 20px; align-items: flex-start; }
.foot-brand p { margin: 0; }
.foot-name { font-weight: 800; color: var(--text); letter-spacing: -0.02em; }
.foot-line { font-size: 13.5px; max-width: 34ch; }
.foot-links { display: flex; flex-wrap: wrap; gap: 16px; list-style: none; margin: 0; padding: 0; }
.foot-links a { text-decoration: none; color: var(--muted); font-size: 14px; }
.foot-links a:hover, .foot-links a:focus-visible { color: var(--text); }
.foot-legal { font: 12.5px var(--font-mono); color: var(--quiet); margin: 0; letter-spacing: 0.02em; }

/* ── parámetros panel: stack-level polish over the vendored kit ─────
   params-panel.css is vendored and must not be edited in place; its own
   header says a stack should override in ITS OWN stylesheet, which is
   what this is. Only surface treatment — no behavior, no colour that
   isn't already a token. */
.params-panel { padding: 26px 24px; }
.params-note { max-width: 62ch; }

/* ── reveal stagger helper (unitless --d, per motion.css's calc) ─── */
.reveal[style*="--d"] { /* no-op selector kept for clarity; --d is read by motion.css */ }

/* ── 404 ───────────────────────────────────────────────────────── */
.error-wrap {
  min-height: calc(100svh - var(--header-height));
  display: flex;
  align-items: center;
}
.error-grid { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 44px; align-items: center; }
.error-code { font: 700 12px var(--font-mono); letter-spacing: 0.2em; color: var(--muted); text-transform: uppercase; }
.error-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 22px; }
.error-mark { width: min(30vw, 180px); }
@media (max-width: 640px) { .error-grid { grid-template-columns: 1fr; } .error-mark { width: min(48vw, 150px); } }

/* ── reduced motion ────────────────────────────────────────────────
   motion.css already carries a global guard, but 404.html links only
   tokens.css + styles.css — so without this, the transitions declared
   above would run unguarded on exactly the page that has no motion kit.
   Scoped to this file's own properties rather than a blanket `*`, so it
   never fights motion.css's rules on the pages that load both. */
@media (prefers-reduced-motion: reduce) {
  html:not(.force-motion) .skip-link,
  html:not(.force-motion) .btn,
  html:not(.force-motion) .cap-card {
    transition-duration: 0.01ms !important;
  }
}

/* ── small-screen: 375px, and the LONGER language half ───────────────
   Overflow is not the only way a header fails at 375px. This nav wraps, and
   Spanish labels run materially longer than English — measured on the showcase
   POC's own twin, the header grew 79px -> 117px and pushed the headline to 50%
   of the first screen. A bilingual template has to hold its longer half, so
   these rules are sized against the Spanish nav, not the English one.
   Check the ES twin at 375px, not just the EN page. */
@media (max-width: 480px) {
  /* ── the theme toggle does not fit here, and that is measured ───────
     The toggle mounts inside the language flipper, which is a flex item
     in a nav that is already at its limit on a phone. Measured at 375px,
     with the button in place:

       casa-estatica ES   nav row 2 needs 268px of 246 -> the "get in
                          touch" button wraps to a third row and the
                          headline drops 115px -> 150px. Baseline had
                          THREE pixels of slack.
       casa-viva EN       header row needs 357px of 343 -> the nav wraps
                          under the wordmark, header 48px -> 76px,
                          headline 162px -> 190px.

     casa-datos and taller-abierto happen to have room, but the rule is
     uniform across the palette on purpose: these are templates, and a
     stack copied from one of them should not behave differently from a
     stack copied from another because of a few pixels of nav copy.

     Hiding is the honest resolution rather than shrinking the control to
     11px or spending the nav's remaining slack, because nothing is lost
     that a phone user did not already have: `prefers-color-scheme` still
     governs, which is the baseline this whole layer is built on, and a
     phone's own theme switch is the control people actually reach for.
     The toggle was always the enhancement half. */
  .theme-toggle { display: none; }

  .wrap, .wrap-narrow { padding-inline: 16px; }
  .nav { gap: 12px; }
  .nav-links { gap: 13px; }
  .nav-links a { font-size: 13.5px; }
  .brand { font-size: 16px; white-space: nowrap; }
  .btn-nav { padding: 8px 14px; font-size: 13.5px; }
  .btn { padding: 12px 20px; }
  /* The ribbon keeps real presence on a phone but not desktop presence:
     four dividers at the desktop 54px floor would spend 216px of a short
     screen on decoration. One variable, so the SVG follows automatically. */
  .thread { --ribbon-h: 44px; }
  /* The eyebrow is the ONLY thing between the sticky header and the
     headline, so at 375px it is measured, not styled by feel. Desktop
     values (11.5px / 0.16em / 1.5 / 16px) put the EN headline at 125px
     and pushed past the 117px the content-first fix had won back; these
     bring it to ~110px EN and ~144px ES, both inside the budget, while
     still reading as a tracked caps label. Re-measure BOTH halves after
     any change here — Spanish is the binding case. */
  .eyebrow {
    font-size: 10.5px;
    letter-spacing: 0.12em;
    line-height: 1.35;
    margin-bottom: 11px;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   THE THEME LAYER — light
   ──────────────────────────────────────────────────────────────────────
   tokens.css re-binds the CANONICAL tokens (--bg, --text, --accent …).
   This block re-derives what this stack composes ON TOP of them — the
   elevation ladder, the edges, the shadows, the light rig, the grain
   plate and the four painted gradients. Those are stack-local names, so
   they belong here and not in the kit; the split is exactly the one the
   vendoring contract already draws.

   Dark needs no counterpart block: forcing dark removes these rules
   (:not([data-theme="dark"])) and the file falls back through to the
   base values above, which is the same result with one copy of the
   numbers instead of two.

   Gated on the same color-mix() probe as the rest of the file, for the
   reason tokens.css states: --tp-paper is the lightest value the design
   system has, so a light elevation ladder cannot be built out of plain
   token references — the ground has to come down, and coming down means
   mixing. An engine without color-mix() gets the dark theme in light
   mode: a complete, measured page rather than a broken approximation.

   ── WHAT ACTUALLY HAD TO INVERT, AND WHAT THE NUMBERS SAY ────────────

   1 · THE LADDER'S SIGN. Dark lifts the card off the ground by mixing
       --tp-peach INTO a dark base. Paper has nothing above it to mix
       toward, so the GROUND drops instead and the card stays near paper:
       ground = ink 7% into paper (L* 91.94), card = ink 1% (L* 96.50),
       hover = paper itself (L* 97.26). dL* card−ground = +4.57, and
       hover finally lifts for real (+0.76 over the card) where on the
       dark page it could not move at all.

   2 · THE CEILING, WHICH IS THE MIRROR OF THE DARK ONE AND WORSE.
       The base layer above records that near black, --text ≥ 15:1 and a
       visible card fight. Near white they fight harder:

         --text (--tp-ink) on ground ≥ 15:1  =>  ground Y ≥ 0.90067
                                             =>  ground L* ≥ 96.00
                                             =>  dL* card−ground ≤ 1.26

       So on paper, "text at 15:1" and "a 4.5 dL* step" are not merely
       tight, they are mutually exclusive — WCAG's +0.05 is negligible up
       here, but L* is compressed instead (dL* per unit Y ≈ 40 near white vs 903
       in the near-black linear segment), which is the same trap wearing
       the other face. The ratified pairing is --tp-paper over --tp-ink
       (PIP-114, 2026-07-22) and it is held: ink stays ink. What gives is
       the 15:1 floor on OPEN GROUND, where it lands at 13.50:1. On the
       card — where the page's dense text actually sits — it is 15.18:1.
       Flagged rather than quietly traded: see the build report.

   3 · THE RIG. 26–32% alpha over near-black is a whisper; the same
       alpha over paper is mud, and it eats the ladder from the other
       end (ocean at 16% costs the ground 8.34 L*). Cut to 4–7%, which
       holds the worst overlap to about −1.5 L* — the rig still tints the
       corners and the ground stays inside the budget above.

   4 · THE SHADOWS. --tp-black at 96–98% alpha is soot on paper. Re-tinted
       to --tp-ink and softened to 20–46%, and the inset top highlight is
       dropped: a 1px light line reads as a lit edge on a dark ground and
       as nothing at all on a white one. On paper the ambient shadow does
       the whole job, which is the reverse of the dark page.

   5 · THE PAINTS. --grad-title is white→lavender (1.06:1 on paper),
       --grad runs through a 1.56:1 amber, --grad-cta through a 3.91:1
       purple. None survives. Each is re-aimed at the `*-deep` family,
       which is the design system's own answer for a light ground: the
       eyebrow reads red-deep→teal-deep→ocean-deep (5.44 / 6.46 / 7.56:1
       on ground), the h1 ink→teal-deep→ocean-deep, and the CTA becomes a
       DARK fill with paper text (4.86 / 6.23 / 8.67:1) instead of a
       bright fill with dark text. No raw warm hue carries text anywhere
       in this theme — that is the colors.md rule the a11y gate hard-fails
       on, and it is held by construction, not by luck.
   ══════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════
   LIGHT THEME — SAME VALUES appear TWICE, by CSS limitation
   ──────────────────────────────────────────────────────────────────
   The declarations below are IDENTICAL to the `:root[data-theme="light"]`
   block at the foot of this file. They must be kept in sync by hand.
   
   Why two copies: a selector list cannot cross an `@media` boundary,
   so this `@media (prefers-color-scheme) { :root:not([data-theme]) }`
   path (JS-off fallback) and the `:root[data-theme="light"]` path
   (explicit toggle + JS-stored choice) must be separate rules even
   though every declaration is identical. There is no pure-CSS way to
   share a declaration block across these two conditions without a
   preprocessor, which this palette explicitly does not use.
   
   Edit ONE, grep for the identically-named properties in the other
   and mirror the change. The gate will catch a drifted hex value
   (via check-token-fidelity.mjs) but NOT a drifted color-mix() —
   that check is on you.  */

@supports (color: color-mix(in srgb, currentColor 50%, transparent)) {
  @media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
      --plane-base: var(--surface);
      --plane-band: var(--bg-alt);
      --plane-1:    var(--raised);
      --plane-2:    var(--tp-paper);
      --plane-sunk: color-mix(in srgb, var(--tp-ember) 4%, var(--bg));

      --edge-warm:        color-mix(in srgb, var(--tp-ember) 52%, var(--plane-1));
      --edge-neutral:     color-mix(in srgb, var(--tp-ink)   22%, var(--plane-1));
      --edge-cool:        color-mix(in srgb, var(--tp-ocean) 32%, var(--plane-1));
      --edge-warm-hot:    color-mix(in srgb, var(--tp-ember) 62%, var(--plane-2));
      --edge-neutral-hot: color-mix(in srgb, var(--tp-ink)   34%, var(--plane-2));
      --edge-cool-hot:    color-mix(in srgb, var(--tp-ocean) 48%, var(--plane-2));

      --band-line: color-mix(in srgb, var(--tp-ink) 20%, var(--plane-band));
      --band-atmos: color-mix(in srgb, var(--tp-peach) 18%, transparent);
      --quiet:     color-mix(in srgb, var(--tp-ink) 76%, var(--tp-paper));

      --lift:
        0 1px 2px -1px   color-mix(in srgb, var(--tp-ink) 20%, transparent),
        0 16px 30px -20px color-mix(in srgb, var(--tp-ink) 38%, transparent);
      --lift-hover:
        0 2px 5px -2px   color-mix(in srgb, var(--tp-ink) 26%, transparent),
        0 24px 44px -20px color-mix(in srgb, var(--tp-ink) 46%, transparent);

      --rig-1: color-mix(in srgb, var(--tp-orange) 7%, transparent);
      --rig-2: color-mix(in srgb, var(--tp-cyan)   6%, transparent);
      --rig-3: color-mix(in srgb, var(--tp-ocean)  7%, transparent);
      --rig-4: color-mix(in srgb, var(--tp-ember)  4%, transparent);
      --grain-op: 0.065;
      --bloom: color-mix(in srgb, var(--tp-ember) 16%, transparent);
      --bloom-cool: color-mix(in srgb, var(--tp-ocean) 13%, transparent);

      --brand-paint: linear-gradient(135deg, var(--tp-red-deep) 0%, var(--tp-teal-deep) 52%, var(--tp-ocean-deep) 100%);
      --title-paint: linear-gradient(120deg, var(--tp-ink) 0%, var(--tp-teal-deep) 46%, var(--tp-ocean-deep) 100%);
      --cta-fill:    linear-gradient(135deg, var(--tp-ember-deep) 0%, var(--tp-red-deep) 45%, var(--tp-ocean-deep) 100%);
      --cta-ink:     var(--tp-paper);
      --cta-glow:    color-mix(in srgb, var(--tp-ink) 34%, transparent);
    }
  }
  /* Same declarations, no media condition: the reader's explicit choice
     wins over the OS. Duplicated because a selector list cannot cross an
     @media boundary — keep the two halves identical. */
  :root[data-theme="light"] {
    --plane-base: var(--surface);
    --plane-band: var(--bg-alt);
    --plane-1:    var(--raised);
    --plane-2:    var(--tp-paper);
    --plane-sunk: color-mix(in srgb, var(--tp-ember) 4%, var(--bg));

    --edge-warm:        color-mix(in srgb, var(--tp-ember) 52%, var(--plane-1));
    --edge-neutral:     color-mix(in srgb, var(--tp-ink)   22%, var(--plane-1));
    --edge-cool:        color-mix(in srgb, var(--tp-ocean) 32%, var(--plane-1));
    --edge-warm-hot:    color-mix(in srgb, var(--tp-ember) 62%, var(--plane-2));
    --edge-neutral-hot: color-mix(in srgb, var(--tp-ink)   34%, var(--plane-2));
    --edge-cool-hot:    color-mix(in srgb, var(--tp-ocean) 48%, var(--plane-2));

    --band-line: color-mix(in srgb, var(--tp-ink) 20%, var(--plane-band));
    --band-atmos: color-mix(in srgb, var(--tp-peach) 18%, transparent);
    --quiet:     color-mix(in srgb, var(--tp-ink) 76%, var(--tp-paper));

    --lift:
      0 1px 2px -1px   color-mix(in srgb, var(--tp-ink) 20%, transparent),
      0 16px 30px -20px color-mix(in srgb, var(--tp-ink) 38%, transparent);
    --lift-hover:
      0 2px 5px -2px   color-mix(in srgb, var(--tp-ink) 26%, transparent),
      0 24px 44px -20px color-mix(in srgb, var(--tp-ink) 46%, transparent);

    --rig-1: color-mix(in srgb, var(--tp-orange) 7%, transparent);
    --rig-2: color-mix(in srgb, var(--tp-cyan)   6%, transparent);
    --rig-3: color-mix(in srgb, var(--tp-ocean)  7%, transparent);
    --rig-4: color-mix(in srgb, var(--tp-ember)  4%, transparent);
    --grain-op: 0.065;
    --bloom: color-mix(in srgb, var(--tp-ember) 16%, transparent);
    --bloom-cool: color-mix(in srgb, var(--tp-ocean) 13%, transparent);

    --brand-paint: linear-gradient(135deg, var(--tp-red-deep) 0%, var(--tp-teal-deep) 52%, var(--tp-ocean-deep) 100%);
    --title-paint: linear-gradient(120deg, var(--tp-ink) 0%, var(--tp-teal-deep) 46%, var(--tp-ocean-deep) 100%);
    --cta-fill:    linear-gradient(135deg, var(--tp-ember-deep) 0%, var(--tp-red-deep) 45%, var(--tp-ocean-deep) 100%);
    --cta-ink:     var(--tp-paper);
    --cta-glow:    color-mix(in srgb, var(--tp-ink) 34%, transparent);
  }

  /* The ribbon is the one ORNAMENT that does not survive a re-binding,
     because its problem is compositing rather than colour. Its halo and
     body washes are the `-soft` tokens — 0.15 alpha — and a colour
     LIGHTER than the ground laid over it at 0.15 alpha is, on paper,
     almost exactly nothing: --tp-sun at 0.15 over --tp-paper moves the
     pixel 1.06:1. Multiply is the fix and it is the physically right
     one: it is what translucent ink on paper actually does, subtracting
     the channels the hue lacks (sun's blue) instead of averaging toward
     white. Same three depths, same section palettes, same seeded SVG —
     the element just stops washing out. Decoration only; nothing here is
     load-bearing with JS off or with motion disabled. */
  @media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .thread { mix-blend-mode: multiply; }
  }
  :root[data-theme="light"] .thread { mix-blend-mode: multiply; }
}
