/* ==========================================================================
   Pingo — pingo.tools
   Styles harvested from the app's own token file, lib/theme/tokens.dart
   ("STEEL" design system). Same graphite canvas, same glass cards, same
   three-hues-only rule: mint = pass, amber = caution, red = fail, and
   NOTHING is tinted decoratively. Emphasis is created with light, never hue.
   ========================================================================== */

:root {
  color-scheme: dark;

  /* Canvas — the graphite -> black vertical ramp (tokens.dart P.bgGradient) */
  --bg-top:  #22303F;
  --bg-mid1: #1A2634;
  --bg-mid2: #111A25;
  --bg-mid3: #0A0F16;
  --bg:      #04070B;

  /* Surfaces — glass. Depth is a 1px hairline, never a shadow. */
  --sur:         rgba(24, 35, 48, 0.82);
  --sur-cell:    rgba(255, 255, 255, 0.07);
  --sur-cell-hi: rgba(255, 255, 255, 0.09);
  --sur-soft:    rgba(255, 255, 255, 0.06);
  --line:        rgba(255, 255, 255, 0.11);
  --line-soft:   rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.14);

  /* Text */
  --ink:      #F2F6FC;
  --ink2:     #D7E0EC;
  --mut:      #B7C4D6;
  --mut2:     #A3B1C3;
  --faint:    #94A2B6;
  --faint2:   #7F8EA4;
  --on-light: #0B121C;

  /* The only three hues in Pingo. Semantics, not decoration. */
  --pass:        #35D6A0;
  --pass-bright: #4BEDBA;
  --pass-text:   #6FE0B6;
  --pass-tint:   rgba(53, 214, 160, 0.12);
  --pass-border: rgba(53, 214, 160, 0.34);
  --warn:        #FFB454;
  --warn-text:   #E7C08A;
  --warn-tint:   rgba(255, 180, 84, 0.14);
  --warn-border: rgba(255, 180, 84, 0.34);
  --fail:        #FF5C6E;
  --fail-text:   #FF8A96;
  --fail-tint:   rgba(255, 92, 110, 0.14);
  --fail-border: rgba(255, 92, 110, 0.40);

  /* Geometry */
  --r-card: 20px;
  --r-hero: 28px;
  --r-inset: 12px;
  --r-chip: 14px;
  --r-button: 18px;
  --r-pill: 99px;

  /* Both families are CDN-loaded (see index.html <head>). If that request fails
     the negative letter-spacing below lands on a fallback face, so the stacks
     are widened towards geometric sans / metric-sane mono rather than stopping
     at system-ui. The real fix is self-hosting both as woff2; note that the
     mock's own stack is declared inside mock.css and is not reachable here. */
  --font: 'Sora', 'Segoe UI Variable Display', system-ui, -apple-system,
          'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', 'SF Mono',
          'Roboto Mono', Menlo, Consolas, monospace;

  --gutter: clamp(20px, 5vw, 48px);
  --maxw: 1120px;
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
  color: var(--mut);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* The canvas: the ramp, plus the cool light bloom over the top 46% and its
   hotter radial core over the top 22%. Fixed so it does not scroll away. */
body::before,
body::after {
  content: '';
  position: fixed;
  inset: 0 0 auto 0;
  pointer-events: none;
  z-index: -1;
}
body::before {
  height: 46vh;
  background: linear-gradient(180deg,
    rgba(163, 187, 214, 0.20) 0%,
    rgba(163, 187, 214, 0.06) 46%,
    rgba(163, 187, 214, 0) 100%);
}
body::after {
  height: 22vh;
  background: radial-gradient(120% 100% at 50% 0%,
    rgba(190, 210, 232, 0.16) 0%, transparent 72%);
}

/* The ramp itself sits on a fixed layer behind both blooms. */
.canvas {
  position: fixed;
  inset: 0;
  z-index: -2;
  background: linear-gradient(180deg,
    var(--bg-top) 0%, var(--bg-mid1) 18%, var(--bg-mid2) 50%,
    var(--bg-mid3) 80%, var(--bg) 100%);
}

/* -------------------------------------------------------------------------
   Type
   ------------------------------------------------------------------------- */

h1, h2, h3 { color: var(--ink); font-weight: 800; margin: 0; letter-spacing: -0.02em; }
h1 { font-size: clamp(2.1rem, 6vw, 3.6rem); line-height: 1.05; letter-spacing: -0.035em; }
h2 { font-size: clamp(1.5rem, 3.4vw, 2.1rem); line-height: 1.15; }
h3 { font-size: 1.0625rem; font-weight: 700; letter-spacing: -0.01em; }
p { margin: 0; }
a { color: var(--ink2); text-decoration-color: var(--line-strong); text-underline-offset: 3px; }
a:hover { color: var(--ink); }

/* THE label: mono, bold, wide-tracked, quiet. tokens.dart lbl() */
.lbl {
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
}

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.lead { font-size: clamp(1.02rem, 2.1vw, 1.1875rem); color: var(--mut); }

/* -------------------------------------------------------------------------
   Layout
   ------------------------------------------------------------------------- */

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
section { padding: clamp(56px, 9vw, 104px) 0; }
.stack { display: grid; gap: 14px; }

/* -------------------------------------------------------------------------
   Glass card — a frosted pane of the field itself, lifted by a 1px hairline
   and a soft top-edge highlight over its top 12%.
   ------------------------------------------------------------------------- */

.card {
  position: relative;
  background: var(--sur);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-card);
  padding: 22px;
  overflow: hidden;
}
.card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 12%;
  background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0));
  pointer-events: none;
}
.card > * { position: relative; }

/* -------------------------------------------------------------------------
   Actions — there is no coloured CTA. Emphasis is white.
   ------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 24px;
  border-radius: var(--r-button);
  font-family: var(--font);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.16s ease, filter 0.16s ease;
}
.btn-primary {
  background: linear-gradient(150deg, #FFFFFF, #CFDBE9);
  color: var(--on-light);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.40);
}
.btn-primary:hover { transform: translateY(-1px); filter: brightness(1.04); }
.btn-secondary {
  background: linear-gradient(150deg, rgba(255,255,255,0.14), rgba(255,255,255,0.05));
  border-color: var(--line);
  color: var(--ink);
}
.btn-secondary:hover { border-color: var(--line-strong); color: var(--ink); }
.btn[aria-disabled='true'] { cursor: default; opacity: 0.92; }
.btn[aria-disabled='true']:hover { transform: none; filter: none; }
.btn small {
  display: block;
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.62;
  line-height: 1.3;
}

/* -------------------------------------------------------------------------
   Chips
   ------------------------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border-radius: var(--r-chip);
  border: 1px solid var(--line);
  background: var(--sur-cell);
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mut2);
}
.chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--faint); }
.chip-pass { background: var(--pass-tint); border-color: var(--pass-border); color: var(--pass-text); }
.chip-pass .dot { background: var(--pass); }
.chip-warn { background: var(--warn-tint); border-color: var(--warn-border); color: var(--warn-text); }
.chip-warn .dot { background: var(--warn); }
.chip-fail { background: var(--fail-tint); border-color: var(--fail-border); color: var(--fail-text); }
.chip-fail .dot { background: var(--fail); }

/* -------------------------------------------------------------------------
   THE HATCH — "there is no reading here". Never a zero, never a pass.
   This is the app's core vocabulary, so it is the site's too.
   ------------------------------------------------------------------------- */

.hatch {
  background-image: repeating-linear-gradient(45deg,
    transparent 0 4px, rgba(255, 255, 255, 0.06) 4px 8px);
  border: 1px dashed rgba(255, 255, 255, 0.20);
  border-radius: var(--r-inset);
}

/* -------------------------------------------------------------------------
   Header
   ------------------------------------------------------------------------- */

.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
  background: rgba(9, 13, 20, 0.72);
  border-bottom: 1px solid var(--line-soft);
}
.site-head .wrap {
  display: flex;
  align-items: center;
  gap: 18px;
  min-height: 66px;
}
.brand { display: flex; align-items: baseline; gap: 9px; text-decoration: none; }
.brand b { font-size: 1.1875rem; font-weight: 800; color: var(--ink); letter-spacing: -0.03em; }
.brand span {
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint2);
}
.site-nav { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 10px clamp(14px, 2.4vw, 28px); }
.site-nav a {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--mut2);
  text-decoration: none;
}
.site-nav a:hover { color: var(--ink); }
@media (max-width: 720px) { .site-nav .hide-sm { display: none; } }

/* THE HEADER MUST FIT A PHONE. Measured before this rule at 360x780:
   documentElement.scrollWidth 417 against clientWidth 360, with nav.site-nav
   right edge at 417. One row cannot hold the brand plus four links at that
   width, so under 560px the nav drops to a second row and wraps inside it.
   A wrapping row rather than a hamburger, deliberately: a hamburger needs
   script, and a header that only works with JavaScript on is a header that
   can fail. Every link stays reachable and nothing is hidden. flex-wrap on
   .site-nav above is the same safety net one breakpoint up, where the three
   .hide-sm links come back. */
@media (max-width: 560px) {
  .site-head .wrap {
    flex-wrap: wrap;
    gap: 6px 14px;
    min-height: 0;
    padding-top: 10px;
    padding-bottom: 10px;
  }
  .site-nav { margin-left: 0; width: 100%; gap: 8px 16px; }
  .site-nav a { font-size: 0.8125rem; }
}

/* -------------------------------------------------------------------------
   THE HEADER MARK. Since 12 September 2026 this is ONE static inline SVG of
   the T4 launcher artwork, carried in every page's markup, so the header mark
   and the Play Store icon are one drawing. It is the ONLY radar Pingo ships:
   there is no radar map with device blips anywhere on this site, because
   RadarField is mounted on no screen and a test forbids it on Home.

   This rule now only sizes and aligns that svg. The six animated child spans
   it replaced (.pg-mark-clip / -sweep / -outer / -inner / -dot / -blip /
   -ring), their three @keyframes, and their two final-frame blocks were
   deleted on 13 September: nothing in any page or script referenced them, and
   an svg has no children for them to style. Keep the mark at 26px.
   ------------------------------------------------------------------------- */

.pg-mark {
  width: 26px;
  height: 26px;
  flex: none;
  display: block;
  align-self: center;   /* .brand aligns on the baseline; the mark has no text */
}

/* -------------------------------------------------------------------------
   Hero
   ------------------------------------------------------------------------- */

.hero { padding: clamp(48px, 8vw, 88px) 0 clamp(40px, 6vw, 72px); }
.hero-grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(32px, 5vw, 64px);
  align-items: center;
}
@media (max-width: 900px) { .hero-grid { grid-template-columns: 1fr; } }

.hero h1 { margin-bottom: 20px; }
.hero .lead { max-width: 34ch; }
.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
.hero-proof {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 26px;
}

/* -------------------------------------------------------------------------
   The phone mock's HOST. The mock itself is mock.html + mock.css, which own
   every .pg-mock / .pgm-* selector; nothing below reaches inside it.

   Two hard rules from mock.css's public API, both load-bearing:
     1. --mock-scale must be set on an ANCESTOR (mock.css only ever reads
        var(--mock-scale, .88)), never on .pg-mock itself.
     2. NO ancestor of .pg-mock may carry filter, opacity < 1, mask or
        will-change. All five cards inside use backdrop-filter, and any of
        those would make the glass sample the wrong backdrop. That is exactly
        why the reveal transition below is on the CONTROLS and the section
        copy, and never on the mock or anything that contains it.
   ------------------------------------------------------------------------- */

.hero-mock {
  --mock-scale: 0.88;               /* 330 x 714.6 of real layout space */
  display: flex;
  flex-direction: column;
  align-items: center;   /* the mock is a fixed-size, non-shrinking box */
  gap: 16px;
  min-width: 0;
}
/* 0.88 IS THE FLOOR, and it is a legibility rule rather than a taste one.
   The design is authored at 375px with 8-9px mono labels, and the handover's
   type rule is "minimum size 9px for any mono unit/caption". 0.88 already puts
   the 8px labels at 7.04px, and it is the single rescale the locked spec
   sanctions (fidelity note 2, option a). The 0.76 and 0.68 steps that used to
   live here pushed those same labels to 6.08px and 5.44px, i.e. unreadable,
   which is precisely what that note exists to prevent.
   Below 370px the mock keeps its size and its own rail scrolls instead. */
@media (max-width: 370px) {
  .hero-mock { --mock-scale: 0.88; overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

/* The disclaimer. Deliberately NOT a [data-reveal] target: it is the one
   sentence on the page that must be present unconditionally. */
.mock-note {
  font-size: 0.75rem;
  line-height: 1.55;
  color: var(--faint2);
  width: 100%;
  max-width: 340px;         /* the same column as the controls, so it reads as one block */
  text-align: center;
}
.mock-note b { color: var(--mut2); font-weight: 700; }

/* -------------------------------------------------------------------------
   Mock controls — built from the page's existing vocabulary (.chip, .btn,
   .btn-secondary, --line, --sur-cell) so they read as part of the site.
   Selection is shown with LIGHT, never hue: mint/amber/red are reserved for
   pass/caution/fail, and a red "Poor" button would be a verdict on a control.
   ------------------------------------------------------------------------- */

.mock-controls {
  width: 100%;
  max-width: 340px;
  display: grid;
  gap: 10px;
  justify-items: center;
}
.mock-controls .lbl { display: block; }

/* The controls do nothing until motion.js has wired them, so until then they
   must not be reachable. Without this they render styled, focusable and inert
   whenever the script 404s, is blocked by CSP or throws: a keyboard visitor
   tabs into five buttons that answer nothing. motion.js adds .motion-ready to
   <html> as the LAST step of a successful init, so a throw leaves them hidden,
   and with JavaScript off the class can never be set. Recorded as robustness
   MED in RESUME-WEBSITE.md section 4b; closed 13 September 2026. */
html:not(.motion-ready) .mock-controls { display: none; }

/* One live region, read by assistive tech and by nobody else. It exists so a
   tier change is announced rather than silent; the page never renders text
   here, and with JavaScript off it stays empty. Not display:none and not
   visibility:hidden, because either removes it from the accessibility tree. */
.vis-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A segmented strip: one row of five at the default scale (measured 337px in a
   340px strip), with flex-wrap as the safety net if a fallback font widens the
   labels. Below 420px it becomes an explicit 2-up grid, see below. */
.tier-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  width: 100%;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--r-chip);
  background: var(--sur-cell);
}
.chip-btn {
  flex: 1 1 auto;
  justify-content: center;
  -webkit-appearance: none;
  appearance: none;
  padding: 7px 10px;
  font-size: 0.625rem;
  border-color: transparent;
  background: transparent;
  color: var(--mut2);
  cursor: pointer;
  transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease;
}
.chip-btn:hover { color: var(--ink2); background: var(--sur-soft); }
.chip-btn.is-selected {
  background: linear-gradient(150deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.06));
  border-color: var(--line-strong);
  color: var(--ink);
}

/* Narrow screens: the five chips will not fit one row, and letting them wrap
   leaves "No score" stretched alone across the bottom. A 2-up grid with the
   last chip spanning both columns is tidier, and it echoes the app's own 2x2
   quick-actions grid a few hundred pixels above it. */
@media (max-width: 420px) {
  .tier-switch { display: grid; grid-template-columns: 1fr 1fr; }
  .chip-btn:last-child { grid-column: 1 / -1; }
}

.mock-motion { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.btn-sm {
  padding: 9px 15px;
  font-size: 0.8125rem;
  border-radius: 13px;
}
/* The tool list as a scannable cloud rather than six columns of prose. Nobody
   reads a 40-item bulleted list on a phone; they scan it for the one word they
   came looking for, which is what chips are for. */
.tool-cloud { display: flex; flex-wrap: wrap; gap: 8px; }
.tool-cloud .chip { text-transform: none; letter-spacing: 0; font-family: var(--font); font-size: 0.8125rem; }
.chip-more { color: var(--faint2); border-style: dashed; }

/* THE MOTION OPT-IN. Hidden unless motion.js has detected the preference and
   set html.reduce-motion, so it never appears to someone who can already see
   the animation. It is NOT gated on .motion-opt-in: motion.js hides it after a
   click, which keeps it out of the way once it has done its job. */
.mock-optin { display: none; }
html.reduce-motion .mock-optin {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0;
  padding-top: 4px;
}
.mock-optin span {
  font-size: 0.8125rem;
  color: var(--faint2);
  max-width: 46ch;
}

/* motion.js disables these under prefers-reduced-motion, because with every
   animation already resting on its final frame there is nothing to pause or
   replay. A control that cannot act must not look like it can. */
.btn[data-motion-na] {
  opacity: 0.45;
  cursor: not-allowed;
}
.btn[data-motion-na]:hover { transform: none; filter: none; border-color: var(--line); }

/* -------------------------------------------------------------------------
   Feature / honesty grids
   ------------------------------------------------------------------------- */

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
@media (max-width: 900px) { .grid-3 { grid-template-columns: 1fr; } .grid-2 { grid-template-columns: 1fr; } }

.sec-head { max-width: 60ch; margin-bottom: clamp(28px, 4vw, 44px); }
.sec-head .lbl { display: block; margin-bottom: 14px; }
.sec-head h2 + p { margin-top: 14px; }

.card h3 + p { margin-top: 9px; font-size: 0.9375rem; }
.card .lbl { display: block; margin-bottom: 12px; }

/* -------------------------------------------------------------------------
   Feature highlights (#features)

   Reuses .card, .chip and the hatch vocabulary. The only genuinely new things
   are the two layouts and .chip-untested, which is the app's HATCHED Tag: the
   untested tone carries no hue at all (tokens.dart HonestyTone.untested), so
   "Partly checked" must never be given a fourth colour to sit beside mint,
   amber and red. Nothing in this section animates on a loop, and the reveal
   hooks are the same one-shot [data-reveal] every other section uses, so with
   JavaScript off (no html.js) nothing here is ever hidden.
   ------------------------------------------------------------------------- */

.feat-lead { display: block; text-decoration: none; color: inherit; margin-bottom: 14px; }
.feat-lead:hover { border-color: rgba(255, 255, 255, 0.22); }
.feat-lead h3 { font-size: clamp(1.125rem, 2.2vw, 1.375rem); margin-top: 4px; }
.feat-lead h3 + p { max-width: 64ch; }
.feat-verdicts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.feat-verdicts .chip { font-size: 0.625rem; padding: 5px 10px; }

/* The hatch, at chip scale: 45 degree stripes plus a 1px dashed edge, exactly
   as .hatch does it, but keeping the chip's own pill radius. */
.chip-untested {
  background-color: transparent;
  background-image: repeating-linear-gradient(45deg,
    transparent 0 4px, rgba(255, 255, 255, 0.06) 4px 8px);
  border: 1px dashed rgba(255, 255, 255, 0.20);
  color: var(--mut2);
}

.feat-more {
  display: inline-block;
  margin-top: 16px;
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
}
.feat-lead:hover .feat-more { color: var(--ink2); }

/* align-items:start so a two-sentence card hugs its content instead of
   stretching to the tallest card in its row. */
.feat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
  gap: 14px;
  align-items: start;
}
.feat-grid .card { padding: 20px; }
.feat-grid h3 { font-size: 1rem; }

@media (max-width: 640px) {
  .feat-grid { grid-template-columns: 1fr; }
  .feat-lead { margin-bottom: 12px; }
}

/* Tool groups */
/* align-items:start so a short group hugs its content instead of stretching to
   the tallest card in the row. Dead space under a tile list is the exact defect
   the app's own Tools screen was pulled up on. */
.tools {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(258px, 1fr));
  gap: 14px;
  align-items: start;
}
.tools ul { margin: 12px 0 0; padding: 0; list-style: none; display: grid; gap: 1px; }
.tools li {
  font-size: 0.875rem;
  color: var(--mut);
  padding: 7px 0;
  border-bottom: 1px solid var(--line-soft);
}
.tools li:last-child { border-bottom: 0; }

/* The promise band */
.promise { text-align: center; }
.promise h2 { font-size: clamp(1.9rem, 5vw, 3rem); letter-spacing: -0.04em; }
.promise .lead { margin: 18px auto 0; max-width: 52ch; }
.promise-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 30px;
}

/* Permission explainer — pre-empts the category's trust landmine */
.explain { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
@media (max-width: 760px) { .explain { grid-template-columns: 1fr; } }
.explain > div { padding: 22px; }
.explain > div:first-child { border-right: 1px solid var(--line-soft); }
@media (max-width: 760px) {
  .explain > div:first-child { border-right: 0; border-bottom: 1px solid var(--line-soft); }
}
.explain h3 { margin-bottom: 10px; }
.explain p { font-size: 0.9375rem; }

/* -------------------------------------------------------------------------
   Prose (privacy page)
   ------------------------------------------------------------------------- */

.prose { max-width: 74ch; }
.prose h2 { font-size: 1.375rem; margin: 44px 0 14px; }
.prose h3 { margin: 28px 0 10px; }
.prose p, .prose li { font-size: 0.9375rem; color: var(--mut); }
.prose p + p { margin-top: 14px; }
.prose ul { padding-left: 20px; margin: 14px 0; display: grid; gap: 9px; }
.prose strong { color: var(--ink2); font-weight: 700; }
.prose code {
  font-family: var(--mono);
  font-size: 0.8125rem;
  background: var(--sur-cell);
  border: 1px solid var(--line-soft);
  border-radius: 6px;
  padding: 2px 6px;
  color: var(--ink2);
}
.prose .card { margin: 22px 0; padding: 20px; }
.table-wrap { overflow-x: auto; margin: 20px 0; border-radius: var(--r-card); border: 1px solid var(--line-strong); }
table { width: 100%; border-collapse: collapse; min-width: 620px; background: var(--sur); }
th, td { text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--line-soft); font-size: 0.875rem; }
th {
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
  background: rgba(255, 255, 255, 0.04);
}
td { color: var(--mut); vertical-align: top; }
tr:last-child td { border-bottom: 0; }

.updated {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 16px;
}

/* -------------------------------------------------------------------------
   Footer
   ------------------------------------------------------------------------- */

.site-foot {
  border-top: 1px solid var(--line-soft);
  padding: 44px 0 56px;
  margin-top: clamp(40px, 6vw, 72px);
}
.foot-grid { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start; }
.foot-grid nav { margin-left: auto; display: flex; flex-wrap: wrap; gap: 22px; }
.foot-grid nav a { font-size: 0.875rem; color: var(--mut2); text-decoration: none; }
.foot-grid nav a:hover { color: var(--ink); }
.fine { font-size: 0.8125rem; color: var(--faint2); max-width: 52ch; margin-top: 18px; }

/* -------------------------------------------------------------------------
   Motion is a liveness cue: nothing hatched, withheld or stale may animate.

   So the site's own motion is exactly two things: the header radar mark (the
   app's logo, gated above) and a ONE-SHOT reveal on real content. There is no
   count-up anywhere, and no hatched element sits inside a revealed subtree.

   The hidden state is scoped to html.js, set by an inline script in <head>, so
   with JavaScript off nothing is ever invisible. html.motion-timeout is the
   second net: index.html's bootstrap sets it if motion.js never initialises,
   which un-hides everything permanently rather than stranding it.
   Stagger is data, not code: style="--reveal-delay:80ms" in the markup.
   will-change is deliberately absent — it would pin a compositor layer for
   every card on the page long after its 0.7s transition is over, which is the
   same reason motion.js refuses to set it from JS.
   ------------------------------------------------------------------------- */

html.js:not(.motion-timeout) [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
  transition-delay: var(--reveal-delay, 0ms);
}
html.js [data-reveal].is-in { opacity: 1; transform: none; }
/* transform does nothing to a non-replaced inline box, so the two standalone
   .lbl kickers that reveal on their own would fade without the 20px rise. Both
   are the only thing on their line, so an inline-block costs nothing. */
html.js [data-reveal].lbl { display: inline-block; }

/* motion.js shows everything immediately under reduce, but it reads the
   preference once at load. This makes a mid-session toggle safe too: a reveal
   can never be left half-transitioned or hidden. Nothing on this site uses
   .motion-keep, and mock.html forbids it on the mock, so there is no carve-out
   to preserve here. */
@media (prefers-reduced-motion: reduce) {
  html.js:not(.motion-opt-in) [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  /* The site's only other transitions: the buttons' hover lift and the tier
     chips' colour cross-fade. Both are movement or a timed change of state, so
     under reduce they resolve instantly. This is what lets an audit of the page
     report ZERO animations and ZERO transitions, with nothing merely paused. */
  html:not(.motion-opt-in) .btn,
  html:not(.motion-opt-in) .chip-btn { transition: none; }
  html:not(.motion-opt-in) { scroll-behavior: auto; }
}

/* NOTE ON THE OFF-SCREEN GATE. motion.js's generic [data-anim-gate] hook
   toggles .is-onscreen / .is-offscreen, and its intended CSS is a plain
   `animation-play-state: paused`. This file deliberately declares no such
   global rule, because pausing leaves a FROZEN MID-FRAME and there is no
   decorative-only animation on this site for which that would be acceptable.
   No element on this site carries [data-anim-gate] any more: .pg-mark used to
   be the one, and it is now a static svg, so initGates() finds an empty list
   and does nothing. The mock is gated separately by motion.js's dedicated
   observer, which drives mock.css's own .is-offstage. Do not add a blanket
   `.is-offscreen { animation-play-state: paused }` rule here. */

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 6px; }

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: var(--gutter);
  top: 12px;
  z-index: 50;
  padding: 10px 16px;
  background: var(--ink);
  color: var(--on-light);
  border-radius: 10px;
  font-weight: 700;
}
