/* DiskDyno marketing site
 * Single stylesheet. Integer-versioned for cache busting: /css/* is immutable
 * for a year, so this file is RENAMED (site.N.css, bump N) on every change, never
 * edited in place and redeployed. See docs/web/WEB-ENGINEERING-SPEC.md 4.2 and 9.3.
 *
 * Every colour here is measured. Do not substitute a value from the app's token
 * set without recomputing its contrast: six of eight app palette values fail
 * WCAG as web text in light mode. See docs/web/WEB-DESIGN.md 1.4.
 *
 * v7 (2026-08-03): the hero shows the app running, not just its output.
 *   The page had no evidence it was an app at all: every figure was a still of
 *   a finished result, so a visitor saw numbers without ever seeing the thing
 *   that produced them.
 *   + .shot--motion / .shot--motion video / .shot--motion .still
 *   ~ the hero figure is a looping muted <video>; the still it used to show is
 *     now that video's poster, so the LCP element is the same kind of image at
 *     the same moment and LCP does not regress.
 *   NOTE the reduced-motion fallback below swaps the video for an <img> whose
 *   src IS the poster URL, so it costs no extra bytes.
 *
 * v6 (2026-08-03): screenshots re-cut and re-laid-out for legibility.
 *   The v5 layout gave screenshots half-column slots, which rendered the app's
 *   UI at 0.29x to 0.59x of the size it was designed at. That is illegible
 *   regardless of resolution, and it read as "low-resolution imagery" when the
 *   images in fact carried up to 6.9x more pixels than they displayed.
 *   ~ hero is one column; every figure spans its container. See "screenshot
 *     frame" below for the rule, and crop-for-web.py for the measurements.
 *   - .shot--device, --shot-h, .gallery: no whole-device shots remain.
 *   + .shot--mid, .hero-copy
 *
 * v5 (2026-08-03): the site got its screenshots and its first call to action.
 *   + .cta-pending / .cta-terms / .closer   (there was no CTA at all)
 *   + .audience
 *   + .badge-legend
 *   - .gauge and friends, and --gauge-stroke: the hand-drawn dial is replaced
 *     by a real screenshot of a real result. A page arguing against invented
 *     numbers should not open with an invented instrument.
 *   - --bezel: the screenshot frame no longer paints one. See "screenshot frame".
 *   ~ .limits rules --border-strong -> --border-hairline
 */

/* ---------------------------------------------------------------- tokens */

:root {
  color-scheme: light dark;

  /* Data tokens. Meaning, not chrome. Never recoloured, never used as text. */
  --data-read:  #FF3B30;
  --data-write: #1C1C1E;
  --data-bus:   #0A84FF;

  /* Surfaces */
  --surface-page:  #EFEFF4;
  --surface-card:  #FFFFFF;
  --surface-inset: #F6F6F9;
  --surface-glass: rgba(255, 255, 255, 0.72);

  /* Text. Derived solids: the app's alpha tokens fail on a web background. */
  --text-primary:   #1C1C1E;
  --text-secondary: #5B5B60;
  --text-tertiary:  #6A6A71;

  /* Borders */
  --border-hairline: #DEDEDF;
  --border-strong:   #86868B;

  /* Accent, three-way split. --accent is GRAPHICS ONLY. */
  --accent:      #FF3B30;
  --accent-text: #DA0C00;
  --accent-ink:  #FFFFFF;

  --alert:      #FF9F0A;
  --alert-edge: #9C5F00;
  --alert-ink:  #FFFFFF;

  /* Confidence states */
  --state-high-fg:     #217D38;
  --state-high-bg:     rgba(52, 199, 89, 0.14);
  --state-caveated-fg: #9C5F00;
  --state-caveated-bg: rgba(255, 159, 10, 0.15);
  /* NOT --accent-text, though it once shared that hex. #DA0C00 was measured at
     5.21:1 on WHITE, but this token is only ever painted on --state-suspect-bg,
     a 12% red tint that composites to rgb(255,231,230). Against its real
     backdrop #DA0C00 is 4.42:1 and FAILS WCAG AA. axe caught it on the live
     site 2026-08-02. #C50D00 is 5.19:1 on the tint, 6.12:1 on white.
     Measure state colours against the composited tint, never against white. */
  --state-suspect-fg:  #C50D00;
  --state-suspect-bg:  rgba(255, 59, 48, 0.12);

  --shadow-card: 0 1px 2px rgba(0, 0, 0, .07), 0 10px 30px rgba(0, 0, 0, .07);

  /* Type */
  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
               "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue",
               Arial, "Noto Sans", sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, "Cascadia Mono",
               "Roboto Mono", Consolas, "Liberation Mono", monospace;

  --type-display: clamp(2.125rem, 1.55rem + 2.45vw, 3.5rem);
  --type-h2:      clamp(1.5rem, 1.29rem + 0.89vw, 2.125rem);
  --type-h3:      clamp(1.1875rem, 1.13rem + 0.27vw, 1.375rem);
  --type-lede:    clamp(1.125rem, 1.02rem + 0.45vw, 1.375rem);
  --type-body:    clamp(1rem, 0.98rem + 0.09vw, 1.0625rem);
  --type-small:   clamp(0.8125rem, 0.79rem + 0.09vw, 0.875rem);
  --type-eyebrow: clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem);

  /* Spacing. There is no --sp-5. If a gap needs 20px the layout is wrong. */
  --sp-1: 0.25rem;  --sp-2: 0.5rem;   --sp-3: 0.75rem;  --sp-4: 1rem;
  --sp-6: 1.5rem;   --sp-7: 1.75rem;  --sp-8: 2rem;     --sp-9: 2.75rem;
  --sp-10: 3.5rem;  --sp-12: 4.5rem;

  /* Documented exceptions: arithmetic, not rhythm. */
  --card-pad: 18px;

  --container-prose: 44rem;
  --container:       68rem;
  --container-wide:  82rem;
  --container-pad:   clamp(1rem, 0.6rem + 1.7vw, 2rem);

  --space-section: clamp(3.5rem, 2.6rem + 3.8vw, 6rem);
  --space-block:   clamp(1.5rem, 1.2rem + 1.3vw, 2.5rem);

  --radius-chip: 8px;  --radius-control: 11px;
  --radius-card: 16px; --radius-pill: 999px;

  --tap-min: 2.75rem;
  --header-h: 3.5rem;

  --dur-fast: 140ms;
  --ease-standard: cubic-bezier(.2, .8, .2, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --data-read:  #FF453A;
    --data-write: #EBEBF5;

    --surface-page:  #000000;
    --surface-card:  #1C1C1E;
    --surface-inset: #2C2C2E;
    --surface-glass: rgba(28, 28, 30, 0.72);

    --text-primary:   #FFFFFF;
    --text-secondary: #9C9CA3;
    --text-tertiary:  #909098;

    --border-hairline: #2F2F32;
    --border-strong:   #8E8E93;

    --accent:      #FF453A;
    --accent-text: #FF453A;
    --accent-ink:  #000000;   /* white measures 3.41:1 here and fails */

    --alert:      #FF9F0A;
    --alert-edge: #FF9F0A;
    --alert-ink:  #000000;

    --state-high-fg:     #30D158;
    --state-high-bg:     rgba(48, 209, 88, 0.18);
    --state-caveated-fg: #FF9F0A;
    --state-caveated-bg: rgba(255, 159, 10, 0.20);
    /* Dark had the SAME bug as light, and it is the one pa11y actually caught:
       headless Chrome runs dark, so the light fix alone changed nothing.
       #FF453A on the 18% tint over --surface-card composites to rgb(69,35,35)
       and is 4.06:1, FAIL. #FF7A70 is 5.45:1 there, and still 4.81:1 on the
       --surface-inset tint, which WEB-DESIGN.md 1.4 already records as the
       surface that breaks marginal pairs. Verified light AND dark before ship. */
    --state-suspect-fg:  #FF7A70;
    --state-suspect-bg:  rgba(255, 69, 58, 0.18);

    --shadow-card: 0 1px 2px rgba(0, 0, 0, .30), 0 10px 30px rgba(0, 0, 0, .40);
  }
}

/* ------------------------------------------------------------------ base */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--type-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0 0 var(--sp-4); line-height: 1.15; }
h1 { font-size: var(--type-display); font-weight: 700; letter-spacing: -0.03em;
     line-height: 1.05; text-wrap: balance; }
h2 { font-size: var(--type-h2); font-weight: 700; letter-spacing: -0.02em;
     text-wrap: balance; }
h3 { font-size: var(--type-h3); font-weight: 600; letter-spacing: -0.01em; }

p { margin: 0 0 var(--sp-4); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent-text); text-decoration-thickness: 1px;
    text-underline-offset: 0.15em;
    transition: text-decoration-thickness var(--dur-fast) var(--ease-standard); }
a:hover { text-decoration-thickness: 2px; }

img { max-width: 100%; height: auto; }

ul, ol { margin: 0 0 var(--sp-4); padding-inline-start: var(--sp-6); }
li { margin-bottom: var(--sp-2); }

/* Focus ring: --accent-text, not --accent. Measured 4.54:1 on page,
   5.21:1 on card. --accent would pass at 3.09:1 with no margin. */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
  border-radius: var(--radius-chip);
}

/* Sticky header must never cover a jump target (SC 2.4.11). */
:target, h2[id], h3[id] { scroll-margin-top: calc(var(--header-h) + var(--sp-4)); }

.skip-link {
  position: absolute; inset-inline-start: var(--sp-4); inset-block-start: -100%;
  z-index: 10; padding: var(--sp-3) var(--sp-4);
  background: var(--surface-card); color: var(--text-primary);
  border-radius: var(--radius-chip); box-shadow: var(--shadow-card);
  text-decoration: none; font-weight: 600;
}
.skip-link:focus { inset-block-start: var(--sp-3); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.eyebrow {
  font-size: var(--type-eyebrow); font-weight: 600; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--text-tertiary);
  margin: 0 0 var(--sp-3);
}

/* ---------------------------------------------------------------- layout */

.section { padding-block: var(--space-section); padding-inline: var(--container-pad); }
.section > * { max-width: var(--container); margin-inline: auto; }
.section--prose > * { max-width: var(--container-prose); }
.section--wide  > * { max-width: var(--container-wide); }

.surface-card  { background: var(--surface-card); }
.surface-inset { background: var(--surface-inset); }

/* ---------------------------------------------------------------- header */

.site-header {
  position: sticky; top: 0; z-index: 5;
  height: var(--header-h);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4);
  padding-inline: var(--container-pad);
  background: var(--surface-glass);
  border-bottom: 1px solid var(--border-hairline);
}
/* The ONLY backdrop-filter call site on the site. */
@supports (backdrop-filter: blur(1px)) {
  .site-header { backdrop-filter: saturate(180%) blur(20px); }
}

.wordmark {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-weight: 700; font-size: 1.25rem; letter-spacing: -0.02em;
  color: var(--text-primary); text-decoration: none;
}
.wordmark-tile {
  width: 30px; height: 30px; border-radius: var(--radius-chip);
  background: var(--accent); display: grid; place-items: center; flex: none;
}

.site-nav { display: flex; gap: var(--sp-4); }
.site-nav a {
  font-size: var(--type-small); font-weight: 600;
  display: inline-flex; align-items: center; min-height: var(--tap-min);
}

/* ------------------------------------------------------------------ hero */

.hero { padding-block: var(--space-section) var(--space-section); }
/*
 * Single column, deliberately. This was two columns until 2026-08-03, which
 * gave the hero screenshot a 441px slot. That crop's UI is 745 CSS px at
 * natural size, so it rendered at 0.59x and the small type turned to mush.
 * Stacking gives it the full 68rem container instead, i.e. about 1.45x, and it
 * is legible. See the header of docs/store/screenshots/crop-for-web.py.
 */
.hero-grid { display: grid; gap: var(--space-block); }
.hero-copy { max-width: var(--container-prose); }
.hero-lede {
  font-size: var(--type-lede); color: var(--text-secondary);
  max-width: 34rem; text-wrap: pretty;
}

/* ------------------------------------------------------------------- CTA */
/*
 * The site shipped its first ten sections with no call to action of any kind:
 * a convinced reader's only exits were Privacy, Terms and a mailto. Added
 * 2026-08-03 in the hero and in a closer after the limits table.
 *
 * DELIBERATELY NOT A LINK, and deliberately not Apple's badge artwork. The app
 * is not approved yet, there is no App Store URL to point at, and Apple's
 * Marketing Resources terms only license the badge for apps that are actually
 * available. A dead badge is the worst possible thing to put on a page whose
 * entire argument is that it does not fake things.
 *
 * WHEN THE APP IS APPROVED, this is the single edit:
 *   1. Change each `<p class="cta-pending">` to
 *      `<a class="cta-pending" href="https://apps.apple.com/app/id<APP_ID>">`
 *      and give this rule `background: var(--accent); color: var(--accent-ink);
 *      border-color: transparent; text-decoration: none;`
 *      Use --accent-ink, never #fff: in dark mode --accent-ink is BLACK,
 *      because white on the dark --accent measures 3.41:1 and fails.
 *   2. Add the App Store badge artwork, unaltered, per Apple's guidelines.
 *   3. Set `offers` and `downloadUrl` in the JSON-LD in index.html, and
 *      RECOMPUTE the script-src SHA-256 in public/_headers.
 *
 * Outlined rather than filled while it is only a status: a filled pill that
 * does nothing when clicked is a broken promise.
 * --accent-text on --surface-page measures 4.54:1, and 5.21:1 on the white
 * --surface-card the closer sits on. Both pass AA. Do not swap in --accent,
 * which is graphics-only and fails as text.
 */
.cta-pending {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-2);
  min-block-size: var(--tap-min);
  padding-inline: var(--sp-7);
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  color: var(--accent-text);
  font-size: var(--type-body); font-weight: 600;
}

/* The line under the CTA that carries the terms. Not fine print elsewhere on
   the page: it belongs where the ask is. */
.cta-terms {
  margin-top: var(--sp-3); margin-bottom: 0;
  font-size: var(--type-small); color: var(--text-secondary);
  max-width: 32rem;
}
.hero-cta { margin-top: var(--sp-8); margin-bottom: 0; }

.closer { text-align: center; }
.closer > * { margin-inline: auto; }
/* No max-width on the heading: --container-prose already holds it to a sane
   measure, and 24ch on top of that broke "Test the card before the shoot, not
   after" across three lines at desktop. */
.closer .cta-terms { margin-inline: auto; }

@media (forced-colors: active) {
  .cta-pending { border: 1px solid CanvasText; }
}

/* ---------------------------------------------------------- feature rows */

.feature-row { display: grid; gap: var(--space-block); align-items: center; }
@media (min-width: 48rem) {
  .feature-row { grid-template-columns: 1fr 1fr; gap: var(--sp-12); }
  .feature-row--flip .feature-row-media { order: -1; }
}

/* Left-aligned, not auto-centred: a centred claim under a left-aligned heading
   reads as an accidental indent rather than as emphasis. */
.claim {
  font-size: var(--type-lede); color: var(--text-primary);
  max-width: var(--container-prose);
  text-wrap: pretty;
}

/* A feature section with no screenshot yet. Keeps the measure readable instead
   of stretching one column across the full container beside an empty half. */
.feature-solo { max-width: var(--container-prose); }

/* ------------------------------------------------------- screenshot frame */
/*
 * No drawn bezel. The masters are CLEAN captures with no device chrome, and
 * wrapping a clean capture in a fake bezel reads as a picture frame and puts
 * two different corner radii 8px apart. A hairline ring and the card shadow
 * do the separating instead.
 *
 * outline, not border: a border would participate in the box and shift the
 * intrinsic aspect ratio the width/height attributes promise, which is the
 * thing holding CLS at 0.
 */
.shot picture { display: block; }
.shot img {
  display: block; margin-inline: auto;
  max-inline-size: 100%;
  border-radius: var(--radius-card);
  outline: 1px solid var(--border-hairline);
  outline-offset: -1px;
}
.shot figcaption {
  margin-top: var(--sp-3);
  font-size: var(--type-small); color: var(--text-secondary);
}

/*
 * THE RULE THAT GOVERNS EVERY SCREENSHOT ON THIS SITE
 *
 * A screenshot is legible only when it is displayed at roughly the size its UI
 * was designed at. Masters are 2x captures, so a crop's "natural" CSS width is
 * its pixel width / 2. Displayed narrower than that, the UI is MINIFIED and its
 * small type becomes mush no matter how many pixels are behind it. Measured on
 * the first pass: the phone shots carried 6.9x more pixels than they displayed
 * and still looked soft, because they were being shown at 0.29x.
 *
 * So: every slot below gives its image at least its natural width, and there
 * are no whole-device screenshots at all, because a whole iPad cannot be shown
 * at 1x in any web layout. Crops only. Full measurements and the per-slot
 * arithmetic are in docs/store/screenshots/crop-for-web.py.
 *
 * block-size stays auto everywhere, so the intrinsic ratio from the width and
 * height attributes still applies and nothing reflows on load.
 */
.shot--detail img { inline-size: 100%; block-size: auto; }

/* Full container, about 1088px: verdict and throughput and codec are 745
   natural, split is 1015. All at or above 1x. */
.shot--detail { margin-inline: auto; }

/*
 * THE HERO'S MOVING FIGURE
 *
 * Same legibility arithmetic as the stills: the recording is 1280px wide, so
 * its natural width is 640 CSS px, and the container gives it about 1088. That
 * is ~1.7x, magnified rather than minified, which is the side of 1x that stays
 * readable. See docs/store/screenshots/record-demo.py for the crop.
 *
 * width and height are on the element, so the intrinsic ratio holds the layout
 * before the poster decodes and CLS stays at 0 -- the same reason every <img>
 * on this page carries them.
 */
.shot--motion video {
  display: block; margin-inline: auto;
  inline-size: 100%; block-size: auto;
  border-radius: var(--radius-card);
  outline: 1px solid var(--border-hairline);
  outline-offset: -1px;
  background: var(--surface-inset);
}

/* The still is the reduced-motion fallback, and it is not in the flow until it
   is needed. */
.shot--motion .still { display: none; }

/* Positioning context for the pause control, and nothing else. It has to be a
   wrapper rather than the <figure>, or the button would be placed against the
   figure's full height including the caption and sit too low. */
.motion-frame { position: relative; }

/*
 * THE PAUSE CONTROL. Ships `hidden`; /js/hero.js reveals it at the same moment
 * it starts playback, so the loop is never running without it. See that file.
 *
 * OPAQUE ON PURPOSE. It sits over footage that is near-black during the run and
 * near-white on the result, so any translucent or video-dependent colouring
 * would fail 4.5:1 against one half of the loop. Giving it its own solid
 * surface makes the contrast a property of the button (white on #1C1C1E,
 * 16.1:1) rather than of whichever frame happens to be underneath. The
 * `color-contrast` Lighthouse assertion is an error here, not a warning.
 *
 * Fixed dark in both schemes, again because the thing behind it is a screenshot
 * whose own colours do not follow the page's.
 */
.motion-toggle {
  position: absolute;
  inset-block-end: var(--sp-3);
  inset-inline-end: var(--sp-3);
  min-block-size: var(--tap-min);
  padding-inline: var(--sp-4);
  border: 0;
  border-radius: var(--radius-pill);
  background: #1C1C1E;
  color: #FFFFFF;
  font: inherit;
  font-size: var(--type-small);
  font-weight: 600;
  cursor: pointer;
}

.motion-toggle:focus-visible {
  outline: 2px solid #FFFFFF;
  outline-offset: -5px;
}

/*
 * REDUCED MOTION: an 11.9s loop of a sweeping needle and counting numerals is
 * precisely the kind of thing that provokes a vestibular response, and CSS
 * cannot stop a video playing. So the video is taken out of the layout and an
 * <img> takes its place.
 *
 * That <img>'s src is the video's own poster URL. Same file, already fetched,
 * so the swap costs nothing and the reader sees exactly the frame the video
 * would have settled on: the finished Verified result, which is the frame that
 * carries the argument anyway.
 */
@media (prefers-reduced-motion: reduce) {
  .shot--motion video { display: none; }
  .shot--motion .still { display: block; }
  /* Nothing is moving, so there is nothing to pause. */
  .motion-toggle { display: none; }
}

/* For crops whose natural width is small enough that the full container would
   blow them up past about 1.8x, which starts to look like a zoom rather than a
   screenshot. pro is 605 natural, compare is 615. 44rem gives both ~1.15x.
   Left-aligned, not centred: every heading and paragraph on the page starts at
   the container's left edge, and a centred figure under a left-aligned heading
   reads as a stray indent. The full-width figures align there for free. */
.shot--mid { max-inline-size: 44rem; }
.shot--mid picture { margin-inline: 0; }

/*
 * Seat every screenshot on a card. This is needed in BOTH schemes, for opposite
 * reasons, and it was verified by rendering the page rather than reasoned about:
 *
 *   Light: the app's own grouped background is #F2F2F7 and --surface-page is
 *          #EFEFF4. Four points apart. The hero screenshot dissolved into the
 *          page and read as an empty rounded box until it was seated.
 *   Dark:  every screenshot is a LIGHT UI, so on the black page it is a glaring
 *          slab and --shadow-card vanishes against it.
 *
 * The fix is the surface behind the image, NOT any filter on the image. Dimming
 * or tinting a screenshot would misrepresent what the app looks like, on a site
 * whose entire argument is that it does not do that.
 */
.shot picture {
  background: var(--surface-card);
  padding: var(--sp-3);
  border-radius: calc(var(--radius-card) + var(--sp-3));
  box-shadow: var(--shadow-card);
  /*
   * A plain block, NOT inline-size: fit-content.
   *
   * fit-content was here to stop the seat stretching past a height-capped
   * device shot. No device shots remain, and combining it with the image's
   * `inline-size: 100%` sets up a cyclic size dependency: the picture's
   * fit-content width depends on the image's contribution, and the image's
   * width is a percentage of the picture. Chrome resolved that one way on
   * first layout and another after decode, which shifted the hero and put
   * mobile CLS at 0.086 against a 0.02 budget. Desktop measured 0.000 and
   * hid it completely. Measured, not reasoned about.
   */
  max-inline-size: 100%;
  margin-inline: auto;
}
/* The seat carries the shadow now, so the image keeps only its hairline. */
.shot img { box-shadow: none; }

/* On a section that is already --surface-card, a white seat on white is
   invisible, so the inset surface separates it instead. */
.surface-card .shot picture { background: var(--surface-inset); }

/* Every figure follows a block of copy now, so it needs its own leading. */
.shot { margin-block-start: var(--space-block); }

/* -------------------------------------------------------------- audience */
/*
 * The page used "bitrate", "codec" and "when the record light is on" without
 * ever naming its reader, so a photographer with a CFexpress card or someone
 * holding a suspicious cheap SSD could read the whole thing without once
 * seeing themselves in it. Four plain lines, no icons: this is a checklist,
 * not a feature grid, and card chrome would inflate it into one.
 */
.audience {
  display: grid; gap: var(--sp-3);
  padding: 0; margin: 0; list-style: none;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  font-size: var(--type-small); color: var(--text-secondary);
}
.audience li {
  margin: 0; padding-top: var(--sp-3);
  border-top: 2px solid var(--accent);
}

/* ------------------------------------------------------------ cards, grid */

.card-grid {
  display: grid; gap: var(--sp-4); padding: 0; list-style: none;
  /* margin-block only. A blanket `margin: 0` here silently overrides the
     `margin-inline: auto` from `.section > *` (same specificity, later in the
     file), which left this grid flush-left inside .section--wide: 32px of gutter
     on the left and 576px on the right at 1920. Only visible above ~1400px,
     because below that the row fills the available width and looks centred. */
  margin-block: 0; margin-inline: auto;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}
.card {
  background: var(--surface-card); border-radius: var(--radius-card);
  padding: var(--card-pad); box-shadow: var(--shadow-card);
}
.card h3 { margin-bottom: var(--sp-2); }
.card p { font-size: var(--type-small); color: var(--text-secondary); }
.card-icon { color: var(--accent); margin-bottom: var(--sp-3); display: block; }

/* --------------------------------------------------- confidence badge */

.badge {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: 30px; padding-inline: var(--sp-3);
  border-radius: var(--radius-pill);
  font-size: var(--type-small); font-weight: 600;
}
.badge-glyph {
  inline-size: 18px; block-size: 18px; flex: none;
  display: grid; place-items: center;
}
.badge--high     { background: var(--state-high-bg);     color: var(--state-high-fg); }
.badge--caveated { background: var(--state-caveated-bg); color: var(--state-caveated-fg); }
.badge--suspect  { background: var(--state-suspect-bg);  color: var(--state-suspect-fg); }

/* The three badges are ONE legend, not three paragraphs. As separate <p>s they
   sat a full paragraph gap apart and read as three orphaned statements rather
   than as a set of three possible answers. */
.badge-legend {
  display: grid; gap: var(--sp-4);
  padding: 0; margin: 0 0 var(--sp-6); list-style: none;
}
.badge-legend li { margin: 0; }
.badge-legend p {
  margin: var(--sp-2) 0 0;
  font-size: var(--type-small); color: var(--text-secondary);
}

/* --------------------------------------------------------- limits table */

.limits { width: 100%; border-collapse: collapse; font-size: var(--type-small); }
.limits th, .limits td {
  text-align: start; padding: var(--sp-3) var(--sp-2);
  /* --border-hairline, not --border-strong. Six rows of --border-strong turned
     the last section of the page into a heavy ruled grid, which is the wrong
     amount of emphasis for the fine print. */
  border-bottom: 1px solid var(--border-hairline);
  vertical-align: top;
}
.limits th { font-weight: 600; white-space: nowrap; }
.limits td { color: var(--text-secondary); }

/* ------------------------------------------------------------------- FAQ */

.faq details {
  border-bottom: 1px solid var(--border-hairline);
  padding-block: var(--sp-3);
}
.faq summary {
  cursor: pointer; font-weight: 600; min-height: var(--tap-min);
  display: flex; align-items: center;
}
.faq details > *:not(summary) { margin-top: var(--sp-2); color: var(--text-secondary); }

/* ----------------------------------------------------------------- prose */
/* Legal pages. Generated by tools/render-legal.py from docs/legal/*.md */

.prose h2 { margin-top: var(--sp-10); }
.prose h3 { margin-top: var(--sp-7); }
.prose > h2:first-of-type { margin-top: var(--sp-6); }
.prose code {
  font-family: var(--font-mono); font-size: 0.9em;
  background: var(--surface-inset); padding: 0.1em 0.35em;
  border-radius: var(--radius-chip);
}
.prose table { width: 100%; border-collapse: collapse; font-size: var(--type-small); }
.prose th, .prose td {
  text-align: start; padding: var(--sp-2);
  border-bottom: 1px solid var(--border-hairline);
}
.prose blockquote {
  margin: var(--sp-4) 0; padding: var(--sp-3) var(--sp-4);
  background: var(--surface-inset); border-radius: var(--radius-control);
  color: var(--text-secondary);
}
.doc-meta { color: var(--text-secondary); font-size: var(--type-small); }

/* ---------------------------------------------------------------- footer */

.site-footer {
  background: var(--surface-inset);           /* no top border: surface separates */
  padding-block: var(--space-section);
  padding-inline: var(--container-pad);
  font-size: var(--type-small); color: var(--text-secondary);
}
.site-footer > div {
  max-width: var(--container); margin-inline: auto;
  display: grid; gap: var(--sp-6);
}
@media (min-width: 40rem) {
  .site-footer > div { grid-template-columns: repeat(3, 1fr); }
}
.site-footer a { color: var(--accent-text); }          /* underline stays at rest */
.site-footer ul { list-style: none; padding: 0; margin: 0; }
.site-footer li { margin-bottom: var(--sp-3); }
.site-footer h2 {
  font-size: var(--type-eyebrow); font-weight: 600; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--text-tertiary); margin-bottom: var(--sp-3);
}

/* ------------------------------------------------------- 404, small pages */

.narrow-page { min-height: 50vh; display: grid; align-content: center; }

/* -------------------------------------------------------- accessibility */

@media (prefers-contrast: more) {
  :root {
    --text-secondary: var(--text-primary);
    --text-tertiary:  var(--text-primary);
    --border-hairline: var(--border-strong);
  }
  /* Data tokens deliberately unchanged: raising them breaks the identity
     with the app and with the redline they name. */
}

@media (forced-colors: active) {
  .badge { border: 1px solid CanvasText; }
  /* On .shot img, not .shot picture: the bezel that <picture> used to paint is
     gone, so in forced colours the screenshot would otherwise have no edge. */
  .shot img { border: 1px solid CanvasText; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
