/* ═══════════════════════════════════════════════════════════════════════
   VINCTURA — production stylesheet
   Doctrine: darkness contains, light reveals.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════
   THE TYPEFACES — served from this origin

   These were loaded from fonts.googleapis.com until it was noticed that
   /privacy says "no third-party scripts, no fingerprinting" and the README
   says "the CSP forbids them", while every page shipped two preconnects and
   a render-blocking stylesheet to Google — handing them the IP address of
   everybody who ever opened a verification record. A privacy claim you
   contradict in your own <head> is worse than no claim.

   They are also faster here. The shared-cross-site-font-cache argument for a
   CDN has been false since browsers began partitioning the HTTP cache by
   top-level site; what remains of it is two extra DNS lookups and two extra
   TLS handshakes on the critical path of a page designed to open on a slow
   connection in another country.

   Four families, one variable file each per subset. `latin-ext` is carried
   because holder names have accents in them and a name rendered in a
   fallback face on their own record is not acceptable.

   All four are SIL Open Font Licence 1.1. See fonts/OFL.txt.
   ═══════════════════════════════════════════════════════════════════════ */
@font-face{font-family:'Cinzel';font-style:normal;font-weight:400 600;font-display:swap;
  src:url(/fonts/cinzel-normal-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Cinzel';font-style:normal;font-weight:400 600;font-display:swap;
  src:url(/fonts/cinzel-normal-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}

@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:300 500;font-display:swap;
  src:url(/fonts/cormorant-garamond-normal-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:300 500;font-display:swap;
  src:url(/fonts/cormorant-garamond-normal-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'Cormorant Garamond';font-style:italic;font-weight:300 500;font-display:swap;
  src:url(/fonts/cormorant-garamond-italic-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Cormorant Garamond';font-style:italic;font-weight:300 500;font-display:swap;
  src:url(/fonts/cormorant-garamond-italic-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}

@font-face{font-family:'Inter';font-style:normal;font-weight:300 500;font-display:swap;
  src:url(/fonts/inter-normal-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:300 500;font-display:swap;
  src:url(/fonts/inter-normal-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}

@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:300 500;font-display:swap;
  src:url(/fonts/jetbrains-mono-normal-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:300 500;font-display:swap;
  src:url(/fonts/jetbrains-mono-normal-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}

/* ── metric-matched fallbacks ──────────────────────────────────────────────
   `font-display:swap` means the first paint uses a fallback and the real face
   replaces it. Without these the replacement reflows the paragraph under the
   reader's eye, which on the verification page means the hour count moves
   while somebody is reading it.

   Every number below is MEASURED, not estimated. size-adjust is the ratio of
   a specimen string's advance width in the real face to its width in the
   fallback; the ascent and descent overrides are the real face's own metrics
   divided by that ratio, because Chromium applies size-adjust on top of the
   overrides rather than instead of them. `scripts/verify-frontend.js` renders
   the specimen in both faces on every run and fails the build if they drift.

   The first draft of this block was written from memory and was wrong by
   15.7% on Cinzel, which is a headline moving half a line height. That is the
   whole argument for measuring: these are not values anybody can eyeball.
   ──────────────────────────────────────────────────────────────────────── */
@font-face{font-family:'Cinzel Fallback';src:local('Georgia'),local('Times New Roman');
  size-adjust:114.4%;ascent-override:85.3%;descent-override:32.5%;line-gap-override:0%}
@font-face{font-family:'Cormorant Fallback';src:local('Georgia'),local('Times New Roman');
  size-adjust:87.3%;ascent-override:105.8%;descent-override:32.9%;line-gap-override:0%}
@font-face{font-family:'Inter Fallback';src:local('Helvetica Neue'),local('Arial');
  size-adjust:105.2%;ascent-override:92.1%;descent-override:22.9%;line-gap-override:0%}
@font-face{font-family:'Mono Fallback';src:local('Menlo'),local('Consolas'),local('Courier New');
  size-adjust:99.7%;ascent-override:102.3%;descent-override:30.1%;line-gap-override:0%}

:root{
  /* ── Alabaster core ── */
  --core-bg:#F5F6F9;
  --core-paper:#FAF8F3;
  --core-surface:#FFFFFF;
  --core-border:rgba(0,0,0,.06);
  --core-rule:rgba(0,0,0,.12);
  --core-text:#0A0C10;      /* 18.11:1 on core-bg */
  --core-text-2:#4B5563;    /*  6.99:1 */
  --core-text-3:#646C7A;    /*  4.90:1 — least headroom in the system */

  /* ── Obsidian shell ── */
  --shell-bg:#07080A;
  --shell-surface:#0F1117;
  --shell-border:rgba(255,255,255,.08);
  --shell-hairline:rgba(255,255,255,.045);
  --shell-text:#F5F6F9;     /* 18.54:1 on shell-bg */
  --shell-text-mid:#9EA3B0; /*  7.94:1 */
  /* CORRECTED. The supplied value #646C7A measures 3.79:1 on obsidian —
     below AA for body text. It is the token tuned for the LIGHT surface
     (4.90:1 there); reusing one hex on both materials is exactly the error
     the two-gold system exists to avoid. #7C8493 gives 5.32:1. */
  --shell-text-muted:#7C8493;

  /* ── Accent ── */
  --gold-leaf:#D4AF37;      /*  9.53:1 on obsidian */
  --gold-lume:#E8C766;      /* 12.20:1 */
  --gold-deep:#7A5E12;      /*  5.65:1 on alabaster, 5.76:1 on paper */
  --gold-wash:rgba(212,175,55,.10);

  /* ── Verification signals ── */
  --sig-verified:#17754F;   /*  5.26:1 on alabaster */
  /* CORRECTED. #966D1B measures 4.32:1 — under AA as a status word.
     #7F5C15 gives 5.32:1. The brighter tone survives as a decorative
     sweep only, where contrast rules do not apply. */
  --sig-attesting:#7F5C15;
  --sig-attesting-hi:#C9922B;
  --sig-revoked:#9B2C2C;    /*  6.96:1 */

  /* ── Type ── */
  --font-display:'Cinzel','Cinzel Fallback',Georgia,serif;
  --font-editorial:'Cormorant Garamond','Cormorant Fallback',Georgia,serif;
  --font-ui:'Inter','Inter Fallback',-apple-system,system-ui,sans-serif;
  --font-mono:'JetBrains Mono','Mono Fallback','SF Mono',ui-monospace,monospace;

  /* ── Spring physics — GENERATED, see scripts/verify-motion-parity.js ──
     Sampled from the step response of the SAME damped oscillator SwiftUI
     integrates, so both platforms trace one curve. Do not hand-edit: run
     `node scripts/verify-motion-parity.js --write`. CI fails on drift.

     TRANSFORM ONLY. These overshoot past 1 on purpose — that is the whole
     point of a spring — and a value above 1 is meaningful for translate and
     scale, clamped and therefore invisible for opacity, and out of gamut for
     colour. Use --ease-luxury for anything that is not a transform. */
  /* fluid: ζ=0.6928 · overshoot 4.89% · settles 0.596s */
  --spring-fluid:linear(0,0.0121,0.0447,0.0931,0.1529,0.2204,0.2926,0.3668,0.4408,0.513,0.5821,0.6471,0.7073,0.7622,0.8117,0.8557,0.8942,0.9274,0.9557,0.9793,0.9987,1.0142,1.0263,1.0354,1.0418,1.0459,1.0482,1.0489,1.0483,1.0467,1.0443,1.0414,1.0381,1.0346,1.0309,1.0273,1.0238,1.0204,1.0172,1.0143,1.0116,1.0092,1.007,1.0052,1.0035,1.0022,1.001,1.0001,0.9993,0.9987,0.9983,0.998,0.9978,0.9976,0.9976,0.9976,0.9977,0.9978,0.998,0.9981,0.9983,0.9985,0.9987,0.9988,1);
  --dur-spring-fluid:0.596s;
  /* pop: ζ=0.8200 · overshoot 1.11% · settles 0.384s */
  --spring-pop:linear(0,0.0084,0.0313,0.0654,0.108,0.1566,0.2093,0.2644,0.3205,0.3766,0.4317,0.4851,0.5363,0.5849,0.6306,0.6732,0.7128,0.7491,0.7823,0.8125,0.8398,0.8642,0.886,0.9053,0.9224,0.9373,0.9503,0.9615,0.9712,0.9793,0.9862,0.992,0.9967,1.0006,1.0037,1.0061,1.0079,1.0093,1.0102,1.0108,1.011,1.0111,1.011,1.0107,1.0103,1.0098,1.0092,1.0086,1.008,1.0074,1.0068,1.0061,1.0056,1.005,1.0045,1.004,1.0035,1.0031,1.0027,1.0023,1.002,1.0017,1.0014,1.0012,1);
  --dur-spring-pop:0.384s;
  /* ── end generated springs ── */
  /* ── Motion ── */
  --ease-luxury:cubic-bezier(.16,1,.3,1);
  --ease-material:cubic-bezier(.65,0,.35,1);
  --dur-fast:.28s; --dur-base:.62s;

  /* ── Space ── */
  --gutter:clamp(24px,6vw,120px);
  --measure:62ch;
  --s1:8px;--s2:16px;--s3:24px;--s4:40px;--s5:64px;--s6:104px;--s7:168px;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--shell-bg);color:var(--shell-text);
  font-family:var(--font-ui);font-weight:300;font-size:16px;line-height:1.65;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
  overflow-x:hidden;
}
::selection{background:var(--gold-leaf);color:#07080A}
a{color:inherit}
img{max-width:100%;height:auto}

/* ── focus ─────────────────────────────────────────────────────────────────
   Everything except .btn was falling through to Chromium's own ring, which is
   #005FCC — 1.6:1 on obsidian, so a keyboard user navigating the header could
   not see where they were. Buttons had a gold ring; nothing else had anything.

   Two rings, always: a dark one and a light one, offset from each other. That
   is what makes the indicator survive an unknown backdrop — over obsidian the
   light ring carries it, over alabaster the dark one does, and over the plate
   or a photograph one of them still lands. WCAG 2.4.11 asks for 3:1 against
   adjacent colour; a single-colour ring can only promise that against a
   surface you already know.
   ──────────────────────────────────────────────────────────────────────── */
:where(a,button,input,select,textarea,summary,[tabindex]):focus-visible{
  outline:2px solid var(--gold-lume);
  outline-offset:3px;
  box-shadow:0 0 0 5px rgba(7,8,10,.85);
  border-radius:1px}
.core :where(a,button,input,select,textarea,summary,[tabindex]):focus-visible,
.verify :where(a,button,input,select,textarea,summary,[tabindex]):focus-visible{
  outline-color:var(--gold-deep);
  box-shadow:0 0 0 5px rgba(245,246,249,.92)}
/* Nothing may remove it. This is the rule that stops the next component from
   quietly reintroducing outline:none. */
:focus-visible{outline-style:solid}

/* ── skip link ── */
.skip{position:absolute;left:-9999px;top:0;z-index:200;padding:14px 20px;
  background:var(--gold-leaf);color:#07080A;font-size:13px;font-weight:500}
.skip:focus{left:0}

/* ── the field ── */
#field{position:fixed;inset:0;z-index:0;pointer-events:none;
  transition:opacity 1.1s var(--ease-luxury)}
#field canvas{width:100%;height:100%;display:block}
body.in-core #field{opacity:.05}

/* ── surfaces ─────────────────────────────────────────────────────────────
   `display:flow-root` is doing real work, not tidiness. Sections that set
   padding-top:0 open with `<hr class="rule">`, whose 64px top margin used to
   collapse out through .wrap and out through the section itself — landing as
   64px of exposed obsidian between two alabaster panels. Three of them cut
   across the light passage on the homepage. flow-root establishes a block
   formatting context, so a child margin can no longer escape its section.
   ──────────────────────────────────────────────────────────────────────── */
section{position:relative;z-index:1;padding:var(--s7) var(--gutter);display:flow-root}
.shell{background:transparent}
.shell.solid{background:var(--shell-bg)}
.core{background:var(--core-bg);color:var(--core-text)}
/* The lift is drawn only where alabaster actually meets obsidian. Applied to
   every .core it also fired between two adjacent alabaster sections, where
   there is no edge to shade, and — worse — cast upward into the foot of the
   seam, darkening the gradient exactly where it was meant to arrive at white.
   That is what made the descent read as a fog bank with a cut across it. */
.shell + .core{box-shadow:0 -40px 80px -20px rgba(0,0,0,.55)}
.core:has(+ .shell),.core:has(+ footer){box-shadow:0 40px 80px -20px rgba(0,0,0,.55)}
.shell + .core:has(+ .shell){box-shadow:0 -40px 80px -20px rgba(0,0,0,.55),
                                        0 40px 80px -20px rgba(0,0,0,.55)}
.core h1,.core h2,.core h3{color:var(--core-text)}
.wrap{max-width:1240px;margin:0 auto}
.narrow{max-width:760px;margin:0 auto}

/* ── type ── */
h1,h2,h3,h4{font-family:var(--font-display);font-weight:400;margin:0;
  letter-spacing:.01em;line-height:1.08;text-wrap:balance}
/* Cinzel is a capitals face: it has no true lowercase, so its "lowercase" is a
   small-cap and the optical colour of a line is set almost entirely by
   tracking. These values are tuned per size, not scaled from one. */
.d1{font-size:clamp(38px,7vw,96px);letter-spacing:.055em;line-height:1.04}
.d2{font-size:clamp(29px,4vw,54px);letter-spacing:.045em}
.d3{font-size:clamp(22px,2.5vw,32px);letter-spacing:.04em}
.d4{font-size:clamp(17px,1.5vw,21px);letter-spacing:.05em}
.lede{font-family:var(--font-editorial);font-size:clamp(19px,2vw,26px);
  line-height:1.55;font-weight:300;max-width:var(--measure);
  color:var(--shell-text-mid);text-wrap:pretty}
.core .lede{color:var(--core-text-2)}
p{max-width:var(--measure);margin:0 0 var(--s2);text-wrap:pretty}
/* The 62ch measure is for running prose. Applied to an all-caps line tracked
   at .34em it produced a two-line eyebrow broken mid-phrase — "STATE COLLEGE,"
   / "PENNSYLVANIA" — as the first typographic event on the homepage. A label
   is not a paragraph and does not want a paragraph's measure. */
.eyebrow{font-size:10.5px;font-weight:500;letter-spacing:.34em;
  text-transform:uppercase;color:var(--gold-leaf);margin:0 0 var(--s3);
  max-width:none;text-wrap:balance}
.core .eyebrow{color:var(--gold-deep)}
.meta{font-family:var(--font-mono);font-size:11px;letter-spacing:.13em;
  text-transform:uppercase;color:var(--shell-text-muted);margin:0;max-width:none}
.core .meta{color:var(--core-text-3)}
.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;letter-spacing:.01em}
/* A record number is read aloud down a telephone and typed from paper. It gets
   the widest tracking on the site so a 0 and an O are never in question. */
.num-id{font-family:var(--font-mono);font-variant-numeric:tabular-nums;
  letter-spacing:.06em;word-break:break-word}
.rule{height:1px;background:var(--shell-border);margin:var(--s5) 0;border:0}
.core .rule{background:var(--core-rule)}
.rule-gold{height:1px;width:64px;background:var(--gold-leaf);margin:var(--s3) 0;border:0}
.core .rule-gold{background:var(--gold-deep)}

/* ── nav ───────────────────────────────────────────────────────────────────
   This bar used to be `backdrop-filter: blur(24px) brightness(.72)` over a
   transparent background, with obsidian-surface text colours nailed on. Over
   the hero that is beautiful. Over an alabaster page it filters white down to
   a light grey and then writes #9EA3B0 on it.

   Measured from rendered pixels on /v/[id] and /verify — the two routes a
   sceptic actually lands on, holding a printed record — the links came out at
   1.12:1 and the wordmark at 1.73:1. AA wants 4.5. The navigation was, in
   practice, invisible on the most important page in the product.

   verify-contrast.js could not see it, and that is the more interesting half:
   it checks declared token-on-surface pairs, and nobody had declared "muted
   obsidian text over a brightness-filtered alabaster backdrop", because
   nobody intended it. The fix is to stop deriving the bar's colour from
   whatever happens to be behind it. Both modes are now stated outright, and
   scripts/verify-frontend.js reads the composited pixels back.
   ──────────────────────────────────────────────────────────────────────── */
#nav{
  --nav-ink:var(--shell-text);
  --nav-ink-2:var(--shell-text-mid);
  --nav-key:var(--gold-leaf);
  --nav-veil:rgba(7,8,10,.86);
  --nav-edge:var(--shell-border);
  --nav-tail:rgba(7,8,10,.55);
  position:fixed;top:0;left:0;right:0;z-index:50;padding:16px var(--gutter);
  display:flex;justify-content:space-between;align-items:center;gap:16px;
  color:var(--nav-ink);background:var(--nav-veil);
  -webkit-backdrop-filter:blur(20px) saturate(125%);
  backdrop-filter:blur(20px) saturate(125%);
  border-bottom:1px solid transparent;
  transition:background var(--dur-base) var(--ease-luxury),
             border-color var(--dur-base) var(--ease-luxury)}

/* `nav-core` means alabaster is behind THIS BAR, not merely somewhere in the
   viewport — see the two observers in app.js. Keyed to the viewport-wide class
   instead, the bar turned light while it was still sitting over the black
   hero, because a light section had come into view further down the screen.

   Pages that are alabaster throughout carry the class from the server, so this
   is correct with JavaScript switched off. Where it is absent the bar stays
   obsidian, which measures 5.78:1 over alabaster — not the intended treatment,
   but never an illegible one. */
body.nav-core #nav{
  --nav-ink:var(--core-text);
  --nav-ink-2:var(--core-text-2);
  --nav-key:var(--gold-deep);
  --nav-veil:rgba(245,246,249,.88);
  --nav-edge:var(--core-rule);
  --nav-tail:rgba(245,246,249,.5)}

/* Softens the lower edge so a legible bar still does not read as a slab.
   The veil itself is never faded — that would put the gradient through the
   text, which is how this went wrong the first time. */
#nav::after{content:'';position:absolute;left:0;right:0;top:100%;height:26px;
  background:linear-gradient(var(--nav-tail),transparent);pointer-events:none}
#nav.stuck{border-bottom-color:var(--nav-edge)}
#nav a{text-decoration:none}
.lockup{display:flex;align-items:center;gap:10px;font-family:var(--font-display);
  font-size:14px;letter-spacing:.28em;color:var(--nav-ink)}
.lockup svg{flex:none}
.navlinks{display:flex;gap:26px;align-items:center;font-size:11.5px;
  letter-spacing:.18em;text-transform:uppercase;color:var(--nav-ink-2)}
/* 19px tall was under the 24px floor of WCAG 2.5.8, and these are the primary
   controls on a phone. The box is brought up to 27px, and a pseudo-element
   widens the touch area to a comfortable 43px without moving anything. */
.navlinks a{position:relative;display:inline-flex;align-items:center;
  min-height:27px;transition:color var(--dur-fast)}
.navlinks a::before{content:'';position:absolute;inset:-8px -10px}
.lockup{min-height:27px}
.navlinks a:hover,.navlinks a:focus-visible{color:var(--nav-key)}
/* ── the bar on a phone ────────────────────────────────────────────────────
   The chapter link earns its place on mobile (see the note in lib/html.js),
   and adding it immediately overflowed the bar: at 375px the lockup is 139px,
   the three links 217px, and 24px of gutter each side — 420px of content in a
   375px window. The wordmark and "State College" collided and "Apply" was
   clipped off the right edge.

   Nothing is dropped to fix it. The wordmark keeps its letters and loses some
   of its tracking, the links close up, and the gutter narrows — which together
   buy back the 45px. Measured at 320, 375 and 390 by verify-frontend.js, which
   fails the build on horizontal overflow, and which is how this was caught.

   The tracking is the interesting one. .28em is a display setting on a 14px
   word and it reads as luxury on a laptop; on a phone it is 40px of air inside
   a component that has run out of room. .16em still reads as a wordmark. */
@media(max-width:720px){
  .navlinks .hide-sm{display:none}
  #nav{padding-left:16px;padding-right:16px;gap:10px}
  .navlinks{gap:16px}
  .lockup{letter-spacing:.16em}
}
/* Below 400px the wordmark gives up the rest of its tracking rather than let
   a primary control leave the screen. */
@media(max-width:400px){
  .lockup{letter-spacing:.08em;font-size:13px}
  .navlinks{gap:13px;font-size:11px}
}
/* 320px is the narrowest phone still in real use, and the width somebody is
   most likely to be holding while checking a printed record. At that size the
   bar was still one pixel over: the links ended exactly on the right bezel,
   with no gutter, which reads as a broken layout rather than a tight one.

   The wordmark takes the cut instead of the controls. It is the one element
   here that is decoration — the mark beside it carries the identity, and a
   verifier who has already typed the URL does not need the name spelled at
   14px. Everything that DOES something keeps its size and its target. */
@media(max-width:360px){
  .lockup{font-size:11px;letter-spacing:.04em;gap:7px}
  .navlinks{gap:11px}
}

/* ── chip ── */
.chip{display:inline-flex;align-items:center;gap:10px;padding:9px 16px 9px 13px;
  border:1px solid var(--shell-border);border-radius:999px;background:rgba(255,255,255,.025);
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--shell-text-mid)}
.core .chip{border-color:var(--core-border);background:var(--core-surface);
  color:var(--core-text-2);box-shadow:0 2px 12px rgba(0,0,0,.05)}
.dot{width:6px;height:6px;border-radius:50%;background:#2FA477;
  box-shadow:0 0 0 3px rgba(47,164,119,.18);animation:breathe 3.4s var(--ease-material) infinite}
@keyframes breathe{0%,100%{opacity:1}50%{opacity:.42}}

/* ── buttons ── */
.btn{display:inline-flex;align-items:center;gap:12px;padding:17px 30px;min-height:52px;
  font-family:var(--font-ui);font-size:11.5px;font-weight:500;letter-spacing:.22em;
  text-transform:uppercase;text-decoration:none;border:1px solid var(--gold-leaf);
  color:var(--gold-leaf);background:transparent;cursor:pointer;position:relative;
  overflow:hidden;transition:color var(--dur-base) var(--ease-luxury),
  border-color var(--dur-base) var(--ease-luxury)}
.btn::after{content:'';position:absolute;inset:0;background:var(--gold-leaf);z-index:-1;
  transform:scaleX(0);transform-origin:left;transition:transform var(--dur-base) var(--ease-luxury)}
.btn:hover{color:var(--shell-bg);border-color:var(--gold-lume)}
.btn:hover::after{transform:scaleX(1)}
.btn:focus-visible{outline:2px solid var(--gold-lume);outline-offset:4px}
.btn-solid{background:var(--gold-leaf);color:#07080A;border-color:var(--gold-leaf)}
.btn-solid:hover{background:var(--gold-lume);color:#07080A}
.btn-quiet{border-color:var(--shell-border);color:var(--shell-text-mid)}
.btn-quiet:hover{color:var(--shell-bg);border-color:var(--gold-leaf)}
/* `.verify` is alabaster too. It was left out of this pair, so every button on
   a verification record and on the lookup form kept the obsidian treatment:
   gold-leaf on #F5F6F9 measures 1.95:1. "Check another record" and "What is
   Vinctura" — the two ways off that page — were close to unreadable, on the
   two routes a stranger reaches first. Now measured, on both materials. */
.core .btn,.verify .btn{border-color:var(--core-text);color:var(--core-text)}
.core .btn::after,.verify .btn::after{background:var(--core-text)}
.core .btn:hover,.verify .btn:hover{color:var(--core-surface)}
.actions{display:flex;gap:var(--s2);flex-wrap:wrap;margin-top:var(--s4)}

/* ── layout ─────────────────────────────────────────────────────────────────
   `minmax(320px,1fr)` is a floor, not a request: below 320px of available
   width the track keeps its 320 and the page grows sideways. At 320px — still
   a real phone, and the width a verifier is most likely to be holding — that
   pushed 24px of the homepage off the right edge, hidden from view by the
   body's overflow-x:hidden and visible only to anyone trying to read it.
   `min(320px, 100%)` lets the floor collapse when the floor is the problem.
   ────────────────────────────────────────────────────────────────────────── */
.grid{display:grid;gap:var(--s3)}
.g2{grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr))}
.g3{grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr))}
.g4{grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr))}
.split{display:grid;grid-template-columns:minmax(0,.44fr) minmax(0,.56fr);
  gap:var(--s5);align-items:start}
@media(max-width:900px){.split{grid-template-columns:1fr;gap:var(--s4)}}
.plate{background:var(--shell-surface);border:1px solid var(--shell-border);padding:var(--s3);
  transition:border-color var(--dur-base) var(--ease-luxury),transform var(--dur-base) var(--ease-luxury)}
.plate:hover{border-color:rgba(212,175,55,.3);transform:translateY(-2px)}
.core .plate{background:var(--core-surface);border-color:var(--core-border);
  box-shadow:0 1px 3px rgba(0,0,0,.04)}
.core .plate:hover{box-shadow:0 12px 32px -12px rgba(0,0,0,.16);border-color:rgba(0,0,0,.12)}

/* ── hero ── */
#hero{min-height:100svh;display:flex;flex-direction:column;justify-content:center;
  padding-top:var(--s6);padding-bottom:var(--s6)}
.scrollcue{width:1px;height:46px;background:linear-gradient(var(--gold-leaf),transparent);
  margin-top:var(--s5);animation:cue 3.2s var(--ease-material) infinite}
@keyframes cue{0%,100%{opacity:.25;transform:scaleY(.7)}50%{opacity:1;transform:scaleY(1)}}

/* ── the seam ───────────────────────────────────────────────────────────────
   The descent. Obsidian to alabaster, and the one moment where the doctrine —
   darkness contains, light reveals — is the interface rather than a note about
   it. It is worth getting right, and it was not right.

   What shipped was `linear-gradient(#07080A, #1A1C22 55%, #F5F6F9)`. Sampled
   down its own column, that ramp crawled through near-black for 160px, climbed
   to a muddy #87888B, REVERSED (the .core box-shadow above was casting up into
   it), and then cut to white in a single 10px step — a luminance jump of 0.71.
   Not a transition. A join, with a grey smear on one side of it.

   Two things are wrong with a two-stop dark-to-light gradient, and both are
   arithmetic rather than taste:

     A gradient interpolates linearly in sRGB, but the eye responds to L*.
     Half the sRGB distance between #07080A and #F5F6F9 is L* 74 of 97 — so
     three quarters of the perceived brightening happens in the last quarter
     of the ramp. It always looks like a cliff, because it is one.

     Across a range this wide, 8-bit sRGB has fewer distinct steps than the
     band is tall, so the flat part visibly quantises into stripes.

   The stops below are computed, not chosen: eleven samples at even intervals
   of CIE L*, converted back to sRGB, holding the blue cast of both endpoints.
   The grain is a 1.6% fractal-noise tile that dithers away the banding — the
   same trick as a mezzotint, and cheap because it is one inlined SVG.
   ────────────────────────────────────────────────────────────────────────── */
/* Asymmetric padding on purpose. Centred in the old 253px band, the line sat
   at the ramp's midpoint and measured 3.69:1 — under AA. Deepening the foot
   puts it at roughly a third of the way down, where the ground is still dark
   enough for 7.5:1, and it also gives the descent somewhere to happen. The
   fall reads slower, which is the correct speed for it. */
.seam{position:relative;text-align:center;
  padding:var(--s6) var(--gutter) calc(var(--s7) + var(--s5));
  background:linear-gradient(180deg,
    #07080a 0%,#1d1f23 10%,#303338 20%,#45484e 30%,#5b5f66 40%,#72767d 50%,
    #8a8e96 60%,#a4a7ae 70%,#bec1c7 80%,#d9dbe0 90%,#f5f6f9 100%)}
.seam::before{content:'';position:absolute;inset:0;pointer-events:none;opacity:.016;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E")}
.seam .wrap,.seam p{position:relative;z-index:1}
/* The line sits in the upper third, where the ramp is still dark enough to
   carry light text at AA. Pinned by scripts/verify-frontend.js, which samples
   the pixels immediately behind it. */
.seam p{margin:0 auto;max-width:36ch;font-family:var(--font-editorial);font-style:italic;
  font-size:clamp(20px,2.4vw,30px);line-height:1.5;color:#E4E6EC;
  text-shadow:0 1px 18px rgba(7,8,10,.55)}

/* ── metric wall ── */
.metric{font-family:var(--font-display);font-size:clamp(34px,4vw,46px);
  letter-spacing:.02em;margin:14px 0 6px;color:var(--core-text)}
.prov{font-size:11.5px;color:var(--core-text-3);margin:0}

/* ── credential specimen ── */
.cert-frame{background:linear-gradient(160deg,#101319,#07080A);padding:clamp(14px,3vw,40px);
  border:1px solid var(--shell-border);display:flex;justify-content:center}
.core .cert-frame{background:linear-gradient(160deg,#EFEFF3,#E4E5EA);border-color:var(--core-border)}
.cert{width:100%;max-width:820px;aspect-ratio:1.414/1;background:var(--core-paper);
  position:relative;overflow:hidden;color:#141210;display:flex;flex-direction:column;
  padding:clamp(16px,3.2vw,42px);
  box-shadow:0 40px 90px -30px rgba(0,0,0,.55),0 0 0 1px rgba(0,0,0,.1)}
.cert-plate{position:absolute;inset:0;pointer-events:none}
.cert-plate svg{width:100%;height:100%;display:block}
.cert-frame-rule{position:absolute;inset:clamp(10px,2vw,20px);
  border:1px solid rgba(122,94,18,.34);pointer-events:none}
.cert-frame-rule::after{content:'';position:absolute;inset:5px;border:1px solid rgba(122,94,18,.15)}
.cert-head,.cert-foot{display:flex;justify-content:space-between;position:relative;z-index:2}
.cert-head{align-items:flex-start;gap:12px}
.cert-foot{align-items:flex-end;gap:clamp(8px,2vw,14px)}
.cert-body{flex:1;display:flex;flex-direction:column;justify-content:center;
  text-align:center;position:relative;z-index:2}
.cert-label{font-family:var(--font-mono);font-size:clamp(6.5px,.8vw,9px);
  letter-spacing:.26em;text-transform:uppercase;color:#7A5E12;margin:0}
.cert-name{font-family:var(--font-display);font-size:clamp(19px,3.4vw,40px);
  letter-spacing:.06em;margin:clamp(6px,1.3vw,13px) 0;color:#141210}
.cert-line{font-family:var(--font-editorial);font-size:clamp(9px,1.25vw,15px);
  color:#3A342A;line-height:1.6;margin:0}
.cert-rank{font-family:var(--font-display);font-size:clamp(10px,1.4vw,17px);
  letter-spacing:.3em;color:#7A5E12;margin:clamp(5px,1.1vw,12px) 0}
/* A fixed 130px floor on both signature rules overflowed the page by 24px at
   320px wide — and the thing sticking out was the verification URL, which is
   the only part of the document that can be checked. It shrinks now, and the
   URL is allowed to break rather than to push. */
.sigline{border-top:1px solid rgba(20,18,16,.42);padding-top:5px;
  min-width:min(130px,40%)}
.cert-foot .cert-label{overflow-wrap:anywhere}

/* ═══ VERIFICATION — the three-phase state machine ═══════════════════════
   Contrast rises as certainty rises. Phase 00 withholds; Phase 02 commits. */
.verify{min-height:100vh;background:var(--core-bg);color:var(--core-text);
  padding:calc(var(--s6) + 40px) var(--gutter) var(--s6)}
/* One left edge for the whole page. The wrap was 1240px while the record card
   was 900px and centred inside it, so the record number — the heading the
   reader is checking against the paper in their hand — hung 230px to the left
   of the card that answers it. */
.verify .wrap{max-width:900px}
.record{margin:0 auto;background:var(--core-surface);
  border:1px solid var(--core-border);padding:var(--s4);position:relative;overflow:hidden;
  transition:box-shadow .8s var(--ease-luxury),border-color .8s var(--ease-luxury)}

/* Phase 00 — zero trust. Recessed, muted, nothing asserted. */
.record[data-phase="0"]{border-color:var(--core-border);box-shadow:none}
.record[data-phase="0"] .reveal{opacity:.18;filter:blur(2px);
  transition:opacity .8s var(--ease-luxury),filter .8s var(--ease-luxury)}
.record[data-phase="0"] .holder{visibility:hidden}

/* Phase 01 — attesting. One gold sweep. No spinner, no percentage. */
.record[data-phase="1"]{border-color:var(--sig-attesting)}
.record[data-phase="1"]::before{content:'';position:absolute;top:0;left:-40%;
  width:40%;height:2px;background:linear-gradient(90deg,transparent,
  var(--sig-attesting-hi),transparent);animation:sweep 1.05s linear infinite}
@keyframes sweep{to{left:100%}}
.record[data-phase="1"] .reveal{opacity:.35;filter:blur(1px)}
.record[data-phase="1"] .holder{visibility:hidden}

/* Phase 02 — verified. Full luminance, ink-black, the record commits. */
.record[data-phase="2"]{border-color:rgba(23,117,79,.42);
  box-shadow:0 20px 48px -22px rgba(23,117,79,.34)}
.record[data-phase="2"] .reveal{opacity:1;filter:none}

/* Phase 03 — revoked or withdrawn. Published, never deleted. */
.record[data-phase="3"]{border-color:rgba(155,44,44,.42)}
.record[data-phase="3"] .reveal{opacity:1;filter:none}

.status{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono);
  font-size:11px;letter-spacing:.16em;text-transform:uppercase;margin:0 0 var(--s3)}
.status--valid{color:var(--sig-verified)}
.status--attesting{color:var(--sig-attesting)}
.status--revoked{color:var(--sig-revoked)}
.status--zero{color:var(--core-text-3)}
/* ── the record, in the order it gets read ─────────────────────────────────
   Whose · what it claims · on whose authority. Three bands, decreasing in
   weight, because a verifier holding the printed document is matching fields
   against it and the first match they need is the name.
   ──────────────────────────────────────────────────────────────────────── */
.record-head{margin-top:var(--s4)}
.record-label{font-family:var(--font-mono);font-size:10px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--core-text-3);margin:0 0 10px}
.record-holder{font-family:var(--font-display);font-weight:400;
  font-size:clamp(27px,3.6vw,44px);line-height:1.12;letter-spacing:.03em;
  color:var(--core-text);margin:0;max-width:22ch;text-wrap:balance}
.record-rule{height:1px;background:var(--core-border);margin:var(--s4) 0 0;
  max-width:none}

.fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr));
  gap:var(--s3);margin:var(--s4) 0 0}
/* The four facts the certificate itself asserts. They stay on one row on a
   laptop and pair up on a phone, so the rank and the hour count are never
   separated from each other. */
.fields--claim{grid-template-columns:repeat(auto-fit,minmax(min(150px,45%),1fr));
  gap:var(--s3) var(--s4)}
/* Administrative provenance. Present, complete, and quieter than the claim. */
.fields--register{margin-top:var(--s3);
  grid-template-columns:repeat(auto-fit,minmax(min(170px,45%),1fr));
  gap:var(--s3) var(--s4)}
.fields--register .field dd{font-size:14.5px;color:var(--core-text-2)}

.field dt{font-family:var(--font-mono);font-size:10px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--core-text-3);margin:0 0 8px}
.field dd{margin:0;font-size:16px;color:var(--core-text)}
.field dd.display{font-family:var(--font-display);font-size:clamp(20px,2vw,26px);
  letter-spacing:.04em;line-height:1.2}
.disclaimer{font-size:13px;color:var(--core-text-3);max-width:70ch;
  border-left:2px solid var(--core-rule);padding-left:var(--s2);margin-top:var(--s4)}

/* ── forms ─────────────────────────────────────────────────────────────────
   `outline:none` used to be substituted by a 1px border-bottom tint. A hairline
   changing hue is not a focus indicator: it fails the 2px-minimum-thickness
   reading of WCAG 2.4.11, and on the verification lookup — a form typed by
   somebody holding a piece of paper, often on a phone in bright sun — it is
   simply not perceivable. The rule above supplies the ring; the border weight
   below supplies the emphasis. Both, not either.
   ──────────────────────────────────────────────────────────────────────── */
.field-input{width:100%;padding:16px 0;border:0;border-bottom:1px solid var(--core-rule);
  background:transparent;font-family:var(--font-ui);font-size:17px;font-weight:300;
  color:var(--core-text);
  transition:border-color var(--dur-fast) var(--ease-material)}
.field-input:focus{border-bottom-color:var(--gold-deep);border-bottom-width:2px;
  padding-bottom:15px}
.field-input::placeholder{color:var(--core-text-3);opacity:1}
.field-input[aria-invalid="true"]{border-bottom-color:var(--sig-revoked)}
textarea.field-input{font-family:var(--font-editorial);font-size:19px;resize:vertical}
/* Select needs its own affordance; the shared input rule strips the platform
   one and a bare caret-less select reads as a dead text field. */
select.field-input{-webkit-appearance:none;appearance:none;cursor:pointer;
  padding-right:28px;
  background-image:linear-gradient(45deg,transparent 50%,var(--core-text-3) 50%),
                   linear-gradient(135deg,var(--core-text-3) 50%,transparent 50%);
  background-size:6px 6px,6px 6px;
  background-position:calc(100% - 12px) calc(50% + 1px),calc(100% - 6px) calc(50% + 1px);
  background-repeat:no-repeat}
label{display:block;margin-bottom:8px}
input[type="checkbox"]{accent-color:var(--gold-deep);width:17px;height:17px}

/* ── tables ────────────────────────────────────────────────────────────────
   A member's own record renders its shift history as a table and the
   stylesheet had no opinion about tables at all, so the one page a volunteer
   actually looks at fell back to the browser's 1996 defaults. Numbers align
   on the decimal, because the column is hours and the whole argument of this
   organisation is that somebody counted them.
   ──────────────────────────────────────────────────────────────────────── */
table{width:100%;border-collapse:collapse;font-size:14.5px;
  font-variant-numeric:tabular-nums}
thead th{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--core-text-3);font-weight:400;
  text-align:left;padding:0 var(--s2) 10px 0;border-bottom:1px solid var(--core-rule);
  white-space:nowrap}
tbody td{padding:13px var(--s2) 13px 0;border-bottom:1px solid var(--core-border);
  color:var(--core-text-2);vertical-align:baseline}
tbody td:first-child{color:var(--core-text)}
tbody td.num,thead th.num{text-align:right;padding-right:var(--s3)}
tbody tr:last-child td{border-bottom:0}
@media(max-width:560px){
  thead th,tbody td{padding-right:var(--s1)}
  table{font-size:13.5px}
}

/* ── footer ── */
footer{background:var(--shell-bg);padding:var(--s6) var(--gutter) var(--s4);
  border-top:1px solid var(--shell-border);position:relative;z-index:1}
.foot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(210px,100%),1fr));
  gap:var(--s4);max-width:1240px;margin:0 auto}
footer a{color:var(--shell-text-mid);text-decoration:none;
  border-bottom:1px solid transparent;transition:color var(--dur-fast)}
footer a:hover,footer a:focus-visible{color:var(--gold-leaf);border-bottom-color:var(--gold-leaf)}
.foot-note{max-width:1240px;margin:var(--s5) auto 0;padding-top:var(--s3);
  border-top:1px solid var(--shell-hairline);font-size:12px;color:var(--shell-text-muted)}

/* ── reveal ─────────────────────────────────────────────────────────────────
   NOTE THE SELECTOR. The hiding rule is scoped to `.js`, which /boot.js sets on
   <html> before first paint. Content is therefore visible by default and the
   animation is the thing that has to be opted into — not the other way round.

   This was `.rv{opacity:0}` unscoped. With JavaScript blocked, or merely
   failing to execute, the homepage rendered 23 of 23 revealed blocks at zero
   opacity and /credential 17 of 17: roughly five thousand pixels of blank
   white where the standard, the specimen, the chapters and the lookup form
   should have been. app.js opened with the sentence "with this file blocked,
   the site still renders", and the site did not.

   A progressive enhancement that is load-bearing is not an enhancement. The
   no-JS render is now measured on every route by scripts/verify-frontend.js.
   ────────────────────────────────────────────────────────────────────────── */
.js .rv{opacity:0;transform:translateY(24px);
  transition:opacity 1s var(--ease-luxury),transform 1s var(--ease-luxury)}
.js .rv.seen{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){
  *{animation-duration:.01ms!important;animation-iteration-count:1!important;
    transition-duration:.01ms!important;scroll-behavior:auto!important}
  .js .rv{opacity:1;transform:none}
}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ═══════════════════════════════════════════════════════════════════════════
   THE COMPOSITOR
   The web half of the spring system. `--spring-*` above supplies the curve;
   these supply the two things a curve alone does not: a layer to run on, and a
   fixed box to run inside.

   ── On will-change ────────────────────────────────────────────────────────
   The instruction "put will-change:transform on all dynamic cards" is the most
   commonly repeated performance advice that makes things slower. Every element
   carrying it is promoted to its own compositor layer AND HELD THERE — on the
   verification page that is the record, every field, and the inspector at once,
   each with its own backing store in GPU memory, permanently, including the
   entire time nothing is animating. On the phone this product is actually used
   on, that is the memory that gets the tab evicted while somebody is reading a
   record in a doorway.

   The property is a HINT WITH A COST, and the cost is only worth paying in the
   moments before an element actually moves. So it is scoped to the states that
   precede movement — a pointer already on the control, a keyboard focus inside
   it, or an explicit [data-animating] the script sets for the duration of a
   transition — and it is dropped again afterwards.

   translate3d is used rather than translateY for the opposite reason: it costs
   nothing to write, and it keeps the transform on the compositor rather than
   letting a 2D transform be rasterised on the main thread on older WebKit.
   ═══════════════════════════════════════════════════════════════════════════ */
.spring-move{transition:transform var(--dur-spring-fluid) var(--spring-fluid)}
.spring-pop{transition:transform var(--dur-spring-pop) var(--spring-pop)}
/* Earned, not granted. See above. */
.spring-move:hover,.spring-move:focus-within,.spring-move[data-animating],
.spring-pop:hover,.spring-pop:focus-within,.spring-pop[data-animating]{
  will-change:transform}

/* ── the progress line ─────────────────────────────────────────────────────
   The web half of the app's pull-to-refresh. A shift lead submitting the log
   form is on the same connection the phone client is offline on, so the gap
   between pressing the button and the page changing can be seconds — and a
   form that looks inert for seconds gets pressed again, which is how a
   duplicate entry is born. (The ledger refuses it by name, but a refusal the
   lead has to read is worse than a wait they can see.)

   Top-anchored and 2px, because the alternative — a spinner on the button —
   moves the thing under the finger that was just pressed. This is out of the
   way of every control on the page and visible from anywhere on it.

   It animates transform, never width: a width animation relayouts the element
   on every frame, and this fires at exactly the moment the main thread is busy
   posting a form.
   ──────────────────────────────────────────────────────────────────────── */
#progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:100;
  transform:scaleX(0);transform-origin:0 50%;
  background:linear-gradient(90deg,var(--gold-deep),var(--gold-leaf));
  opacity:0;pointer-events:none}
#progress[data-state="running"]{opacity:1;
  /* Creeps to 90% and waits. It cannot know the real fraction, so it never
     claims one — the last 10% is the response actually arriving. A bar that
     completes on a guess and then sits at 100% while nothing happens has told
     a lie the reader will remember next time. */
  transform:scaleX(.9);
  transition:transform 8s cubic-bezier(0,.6,.2,1),opacity .2s}
#progress[data-state="done"]{opacity:0;transform:scaleX(1);
  transition:transform .2s var(--ease-material),opacity .4s .2s}
.progress-note{position:fixed;top:10px;left:50%;transform:translate3d(-50%,0,0);
  z-index:100;padding:7px 14px;border-radius:999px;
  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.14em;
  text-transform:uppercase;background:var(--core-surface);color:var(--core-text-2);
  border:1px solid var(--core-rule);box-shadow:0 6px 24px -10px rgba(0,0,0,.28);
  opacity:0;transition:opacity .25s var(--ease-material)}
.progress-note[data-visible="1"]{opacity:1}

/* ── skeletons ─────────────────────────────────────────────────────────────
   A placeholder exists to hold a box open, not to entertain. The shimmer is
   secondary; the FIXED GEOMETRY is the entire point, and it is why these carry
   explicit heights rather than wrapping whatever the server has not sent yet.

   Cumulative Layout Shift on a verification page is not a metric problem. It is
   somebody reading a record number off a phone while a field they were halfway
   through loads above it and pushes it under their thumb.

   The shimmer translates a gradient across a fixed box — transform only, so it
   composites — rather than animating background-position, which repaints. */
.skel{position:relative;overflow:hidden;background:var(--core-border);
  border-radius:1px}
.shell .skel{background:rgba(255,255,255,.06)}
.skel::after{content:'';position:absolute;inset:0;transform:translate3d(-100%,0,0);
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.55),transparent);
  animation:skel-sweep 1.4s var(--ease-material) infinite}
.shell .skel::after{background:linear-gradient(90deg,transparent,
  rgba(255,255,255,.09),transparent)}
@keyframes skel-sweep{to{transform:translate3d(100%,0,0)}}
/* The heights are the rendered heights of the real content, so the swap moves
   nothing. Changing a field's type scale means changing its skeleton too —
   scripts/verify-frontend.js measures the shift and fails if it grows. */
.skel-label{height:10px;width:8ch;margin-bottom:8px}
.skel-value{height:16px;width:14ch}
.skel-holder{height:clamp(27px,3.6vw,44px);width:min(22ch,100%)}

@media(prefers-reduced-motion:reduce){
  /* The box still has to be held. Only the sweep goes. */
  .skel::after{display:none}
}

/* ═══════════════════════════════════════════════════════════════════════
   THE EPISTEMIC INSPECTOR
   Dormant until invoked. Nothing below costs a visitor anything until they
   press the key, which is the correct price for a feature most people will
   never use and the few who matter will use immediately.
   ═══════════════════════════════════════════════════════════════════════ */
.insp-bar{position:fixed;right:var(--s2);bottom:var(--s2);z-index:60}
/* Surface-aware for the same reason the nav is. On a verification record this
   control floats over alabaster, and an obsidian chip there is not restraint,
   it is a component that was only ever looked at on one background. */
.insp-toggle{display:inline-flex;align-items:center;gap:10px;
  padding:12px 15px;min-height:44px;
  border:1px solid var(--shell-border);background:rgba(7,8,10,.86);
  -webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);
  color:var(--shell-text-mid);cursor:pointer;
  font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;
  transition:color var(--dur-fast) var(--ease-material),
             border-color var(--dur-fast) var(--ease-material)}
body.in-core .insp-toggle{border-color:var(--core-rule);
  background:rgba(255,255,255,.9);color:var(--core-text-2);
  box-shadow:0 6px 24px -10px rgba(0,0,0,.28)}
.insp-toggle:hover{border-color:var(--gold-leaf);color:var(--gold-leaf)}
body.in-core .insp-toggle:hover{border-color:var(--gold-deep);color:var(--gold-deep)}
.insp-toggle kbd{font-family:var(--font-mono);font-size:9px;padding:2px 6px;
  border:1px solid var(--shell-border);border-radius:2px;color:var(--shell-text-muted)}
body.in-core .insp-toggle kbd{border-color:var(--core-rule);color:var(--core-text-3)}
.insp-dot{width:6px;height:6px;border-radius:50%;background:var(--shell-text-muted);
  transition:background var(--dur-fast),box-shadow var(--dur-fast)}
.inspecting .insp-dot{background:var(--gold-leaf);box-shadow:0 0 0 3px var(--gold-wash)}
.inspecting .insp-toggle{border-color:var(--gold-leaf);color:var(--gold-leaf)}
.inspecting body.in-core .insp-toggle,
body.in-core .inspecting .insp-toggle{border-color:var(--gold-deep);color:var(--gold-deep)}
.inspecting body.in-core .insp-dot{background:var(--gold-deep)}
@media(max-width:720px){.insp-toggle kbd{display:none}}

/* annotated targets — marked only while inspecting */
.inspecting .insp-target{position:relative;
  box-shadow:inset 0 -1px 0 rgba(212,175,55,.55)}
.inspecting .core .insp-target,.inspecting .verify .insp-target{
  box-shadow:inset 0 -1px 0 rgba(122,94,18,.5)}
.inspecting .insp-held{position:relative;outline:1px dashed rgba(212,175,55,.4);
  outline-offset:6px}
.inspecting .core .insp-held{outline-color:rgba(122,94,18,.4)}

/* A note is inserted directly after the value it explains, which inside a
   four-up grid means it inherits a 150px column and the source line becomes a
   vertical ribbon one or two words wide. While inspecting, a field carrying an
   open note takes the whole row instead.

   The page reflowing to make room for its own evidence is the correct
   behaviour for this feature, not a compromise: the mode exists to turn the
   page inside out, and cramming the justification into the gutter would be
   the display saying it did not really mean it. */
.inspecting .fields .field:has(> .insp-note:not([hidden])){grid-column:1 / -1}

.insp-note{margin:12px 0 18px;padding:14px 16px;max-width:64ch;
  border-left:2px solid var(--gold-leaf);background:rgba(212,175,55,.05);
  opacity:0;transform:translateY(-4px);
  transition:opacity .5s var(--ease-luxury),transform .5s var(--ease-luxury)}
.inspecting .insp-note{opacity:1;transform:none}
.core .insp-note,.verify .insp-note{border-left-color:var(--gold-deep);
  background:rgba(122,94,18,.045)}
.insp-note--held{border-left-style:dashed}
.insp-heldhead{margin:0 0 10px;font-family:var(--font-mono);font-size:9.5px;
  letter-spacing:.2em;text-transform:uppercase;color:var(--gold-leaf)}
.core .insp-heldhead,.verify .insp-heldhead{color:var(--gold-deep)}
.insp-dl{margin:0;display:grid;gap:7px}
.insp-row{display:grid;grid-template-columns:82px 1fr;gap:12px;align-items:baseline}
.insp-row dt{margin:0;font-family:var(--font-mono);font-size:9px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--shell-text-muted)}
.core .insp-row dt,.verify .insp-row dt{color:var(--core-text-3)}
.insp-row dd{margin:0;font-size:12.5px;line-height:1.5;color:var(--shell-text-mid)}
.core .insp-row dd,.verify .insp-row dd{color:var(--core-text-2)}
.insp-fresh{color:var(--sig-verified)!important}
.core .insp-fresh,.verify .insp-fresh{color:var(--sig-verified)!important}
.insp-stale{color:var(--sig-revoked)!important}
@media(max-width:560px){.insp-row{grid-template-columns:1fr;gap:2px}}

/* ═══════════════════════════════════════════════════════════════════════
   THE PLATE FORGE
   ═══════════════════════════════════════════════════════════════════════ */
.forge{border:1px solid var(--core-border);background:var(--core-surface);
  padding:var(--s4);margin-top:var(--s3)}
.forge-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);
  gap:var(--s4);align-items:start}
@media(max-width:860px){.forge-grid{grid-template-columns:1fr}}
.forge-stage{position:relative;aspect-ratio:1;background:#0B0C10;
  border:1px solid rgba(212,175,55,.18);overflow:hidden}
.forge-stage canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.forge-caption{position:absolute;left:10px;bottom:8px;z-index:2;
  font-family:var(--font-mono);font-size:9px;letter-spacing:.16em;
  text-transform:uppercase;color:rgba(212,175,55,.75)}
.forge-pair{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.forge-pair .forge-stage{aspect-ratio:1}
.forge-bytes{font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;
  display:flex;flex-wrap:wrap;gap:5px;margin:0}
.fb{padding:3px 5px;background:rgba(0,0,0,.04);color:var(--core-text-3);border-radius:2px}
.fb--used{background:var(--gold-wash);color:var(--gold-deep);font-weight:500}
.fb--rest{background:none;color:var(--core-text-3)}
.forge-params{display:flex;flex-wrap:wrap;gap:10px;font-family:var(--font-mono);
  font-size:11px;color:var(--core-text-2);margin:0}
.forge-params span{padding:3px 8px;border:1px solid var(--core-border);border-radius:2px}
.forge-examples{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.forge-examples button{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;
  padding:7px 11px;border:1px solid var(--core-rule);background:transparent;
  color:var(--core-text-2);cursor:pointer;transition:all var(--dur-fast)}
.forge-examples button:hover{border-color:var(--gold-deep);color:var(--gold-deep)}
.forge-readout{display:grid;gap:14px;margin-top:var(--s3)}
.forge-readout > div{display:grid;grid-template-columns:104px 1fr;gap:12px;align-items:baseline}
.forge-readout dt,.forge-readout .k{font-family:var(--font-mono);font-size:9px;
  letter-spacing:.18em;text-transform:uppercase;color:var(--core-text-3);margin:0}
@media(max-width:560px){.forge-readout > div{grid-template-columns:1fr;gap:3px}}
.forge noscript{display:block;font-size:13px;color:var(--core-text-2)}

/* ═══ THE COVERAGE TAPESTRY ═══════════════════════════════════════════════
   A gap is drawn as an empty cell, never omitted. It is the only part of this
   chart that is difficult to publish, and therefore the only part worth
   looking at. */
.tapestry{margin:0;overflow-x:auto;padding-bottom:var(--s2)}
.tapestry svg{min-width:520px;display:block}
.tapestry rect{transition:fill-opacity var(--dur-fast) var(--ease-material)}
.tapestry rect:hover{fill-opacity:1}
.tapestry figcaption{margin-top:var(--s2);max-width:74ch}
.tapestry figcaption strong{color:var(--core-text);font-weight:500}

/* ═══════════════════════════════════════════════════════════════════════════
   PRINT
   A registrar prints a verification page and attaches it to a file. That is a
   real workflow and it is the last thing anyone tests. Without this the paper
   carries navigation, a footer, and a floating inspector button — and loses the
   URL, which is the only part that mattered.
   ═══════════════════════════════════════════════════════════════════════════ */
@media print {
  @page { margin: 18mm; }

  #nav, #field, footer, .insp-bar, .actions, .scrollcue,
  .skip, form, .forge, .seam { display: none !important; }

  html, body { background: #fff !important; color: #000 !important; }
  body { font-size: 11pt; line-height: 1.5; }
  main { padding: 0 !important; }
  section { padding: 0 !important; background: transparent !important;
            box-shadow: none !important; }

  .record { border: 1pt solid #000 !important; box-shadow: none !important;
            padding: 8mm !important; break-inside: avoid; }
  .record[data-phase] .reveal { opacity: 1 !important; filter: none !important; }
  .record[data-phase="0"] .holder,
  .record[data-phase="1"] .holder { visibility: visible !important; }

  /* The record head, on paper. The holder's name is the line a registrar
     attaches to a file, so it keeps its weight and loses only its colour. */
  .record-head { break-inside: avoid; margin-bottom: 5mm; }
  .record-label { color: #444 !important; }
  .record-holder { color: #000 !important; font-size: 20pt !important;
                   max-width: none !important; }
  .record[data-phase="0"] .record-holder,
  .record[data-phase="1"] .record-holder { visibility: visible !important; }
  .record-rule { background: #999 !important; margin: 4mm 0 !important; }

  .fields { display: block !important; }
  .fields--claim, .fields--register { display: block !important; }
  .field { break-inside: avoid; margin-bottom: 4mm; }
  .field dt { color: #444 !important; }
  .field dd, .field dd.display { color: #000 !important; }
  .fields--register .field dd { color: #000 !important; }

  table { font-size: 9pt; }
  thead th { color: #444 !important; }
  tbody td { color: #000 !important; border-bottom-color: #ccc !important; }

  /* The URL is the record. Print it, always, whether or not it was a link. */
  main::after {
    content: "Verify at " attr(data-verify-url);
    display: block; margin-top: 8mm; padding-top: 3mm;
    border-top: 0.5pt solid #999;
    font-family: var(--font-mono); font-size: 9pt; color: #000;
  }

  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 8pt; color: #444; }
  a[href^="/"]::after { content: ""; }

  .insp-note { display: none !important; }
  .tapestry { break-inside: avoid; }
  .status--valid { color: #000 !important; font-weight: 700; }
  .status--revoked { color: #000 !important; font-weight: 700;
                     text-decoration: underline; }
}
