/* ===========================================================================
   Driftpatch design system
   Owner: Design (PER-9). Specs: the `design-system` and `ux-review` documents
   on PER-9. Rebased on the Engineer's in-flight PER-4 landing-page work, so
   every selector that views.js emitted on 2026-10-02 is still styled here.

   Rules for anyone editing this file:
   1. No raw colour, length, radius or font size inside a rule. Use a token.
      If the token you need is missing, add it to :root AND to the dark block,
      and say so on PER-9 so it stays a system change, not a one-off.
   2. Every colour pair here is checked by `node scripts/check-contrast.mjs`.
      Change a colour, re-run that script, keep it at zero failures.
   3. Severity is never carried by colour alone. Each level gets a word (the
      badge label), a shape (the badge glyph) and a rail (the card's left border
      style). The three light-mode badge colours sit within 0.006 of each other
      in greyscale luminance on purpose: colour here is decoration, not data.
   4. Mobile first. Breakpoints: 720px and 1000px, in that order, nothing else.
      Everything must work at 390px.
   =========================================================================== */

:root {
  /* --- Colour: surfaces and text (light) ------------------------------- */
  --bg: #ffffff;
  --bg-subtle: #f6f7f9;          /* panels, form wells, example block */
  --bg-inset: #eceef2;           /* code, neutral chips */
  --text-primary: #15171c;       /* 17.9:1 on --bg */
  --text-secondary: #555c6a;     /* 6.7:1 on --bg, 6.3:1 on --bg-subtle */
  --text-tertiary: #656d7c;      /* 5.2:1 on --bg. We ship no sub-AA grey. */
  --border: #cfd5de;             /* hairline, 1.47:1 — visible, not a wall */
  --border-strong: #868e9c;      /* 3.3:1 — form controls, WCAG 1.4.11 */

  /* --- Colour: accent -------------------------------------------------- */
  --accent: #1a56db;             /* 6.2:1 on --bg, 5.8:1 on --bg-subtle */
  --accent-hover: #15409f;
  --accent-fg: #ffffff;          /* 6.2:1 on --accent */
  --focus-ring: #1a56db;         /* 6.2:1 on --bg, over the 3:1 floor */

  /* --- Colour: severity scale ------------------------------------------
     Four levels. `behaviour` is live today only on oasdiff finding badges
     (oasdiff level 2, "potentially breaking"). The changes table still has a
     three-value CHECK constraint, so it is not yet a change-level severity;
     it is reserved for the behavioural canaries in the plan's Phase 2.
     ---------------------------------------------------------------------- */
  --sev-breaking-fg: #a81b11;
  --sev-breaking-bg: #fdeceb;
  --sev-breaking-rail: #d6372a;
  --sev-behaviour-fg: #6435b8;
  --sev-behaviour-bg: #f2edfd;
  --sev-behaviour-rail: #8257e8;
  --sev-deprecation-fg: #7d4a00;
  --sev-deprecation-bg: #fdf2da;
  --sev-deprecation-rail: #ad7505;
  --sev-informational-fg: #3d4654;
  --sev-informational-bg: #eceff3;
  --sev-informational-rail: #848d9b;

  /* --- Colour: diff (PER-43) -------------------------------------------
     Added and removed lines in the proof patch on the landing page. Blue and
     orange, not green and red: the pair stays apart for protan and deutan
     readers, and red already means "breaking" in the severity scale. Colour
     is the third signal, after the +/- marker and the ins/del styling on the
     changed token. The two tints are equally light in greyscale on purpose,
     like the severity scale: the marker is the data. `-strong` is the
     word-level tint on the exact token that changed. */
  --diff-add-bg: #e6f0ff;
  --diff-add-strong: #c2d8fc;
  --diff-add-fg: #0d4db0;        /* the + marker; 6.8:1 on --diff-add-bg */
  --diff-del-bg: #fff0e2;
  --diff-del-strong: #fcd2a8;
  --diff-del-fg: #953c00;        /* the - marker; 6.4:1 on --diff-del-bg */

  /* --- Spacing: 4px base ----------------------------------------------- */
  /* Half step. Only for vertical padding inside an inline chip (badge, code,
     samp), where 4px makes the chip taller than the line it sits in. Do not
     use it for layout: if a gap wants 2px, the gap is wrong. */
  --space-half: 2px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  /* --- Type ------------------------------------------------------------- */
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --text-xs: 12px;               /* floor. Nothing smaller ships. */
  --text-sm: 14px;
  --text-base: 16px;
  --text-lg: 19px;
  --text-xl: 24px;
  --text-2xl: 30px;              /* 40px at >=720px */
  --leading-tight: 1.15;
  --leading-snug: 1.35;
  --leading-normal: 1.55;
  --weight-regular: 400;
  --weight-strong: 600;          /* two weights. Hierarchy comes from size and colour. */
  --tracking-caps: 0.04em;
  --tracking-tight: -0.015em;

  /* --- Shape ------------------------------------------------------------ */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;
  --border-width: 1px;
  --border-width-rail: 4px;
  /* No shadow tokens on purpose. Nothing in Phase 0 floats above the page, and
     a shadow reads as dirt in dark mode. Elevation is a border and a surface
     tint: --border plus --bg-subtle. If a real overlay arrives (a dialog, a
     dropdown), add --shadow-md here and say so on PER-9 — do not inline one. */

  /* --- Layout ----------------------------------------------------------- */
  --measure-page: 880px;         /* feed and landing shell */
  --measure-narrow: 560px;       /* single-message pages, privacy, forms */
  --measure-prose: 68ch;         /* paragraph line length */
  --tap-min: 44px;               /* minimum interactive target */
  /* Hanging indent for a wrapped diff line, in characters. A continuation
     sits this far right of the line's own indentation, so it can never be
     mistaken for the next line of code. Unitless: multiplied by 1ch. */
  --diff-hang: 2;

  /* --- Motion ----------------------------------------------------------- */
  --transition-fast: 120ms ease-out;

  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0e1014;
    --bg-subtle: #171a20;
    --bg-inset: #1f242c;
    --text-primary: #e9ebf0;     /* 16.0:1 */
    --text-secondary: #a8b0bd;   /* 8.7:1 */
    --text-tertiary: #949daa;    /* 7.0:1 */
    --border: #2e343f;           /* 1.52:1 */
    --border-strong: #616a79;    /* 3.5:1 */

    --accent: #8aacff;           /* 8.6:1 */
    --accent-hover: #a8c2ff;
    --accent-fg: #0e1014;        /* 8.6:1 on --accent */
    --focus-ring: #8aacff;

    --sev-breaking-fg: #ff9d93;
    --sev-breaking-bg: #3a1513;
    --sev-breaking-rail: #f0574a;
    --sev-behaviour-fg: #cdb4ff;
    --sev-behaviour-bg: #261a3d;
    --sev-behaviour-rail: #9b72f5;
    --sev-deprecation-fg: #f6c46a;
    --sev-deprecation-bg: #33240a;
    --sev-deprecation-rail: #d89b24;
    --sev-informational-fg: #c2c9d4;
    --sev-informational-bg: #222730;
    --sev-informational-rail: #7a8493;

    --diff-add-bg: #0f2442;
    --diff-add-strong: #1e4277;
    --diff-add-fg: #8fbaff;
    --diff-del-bg: #2f1b0b;
    --diff-del-strong: #5c3510;
    --diff-del-fg: #ffb27a;
  }
}

/* ===========================================================================
   Base
   =========================================================================== */

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

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

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
  color: var(--text-primary);
  background: var(--bg);
  /* Endpoint paths and provider slugs must never force a sideways scroll. */
  overflow-wrap: break-word;
}

a {
  color: var(--accent);
  text-underline-offset: 2px;
}

a:hover {
  color: var(--accent-hover);
}

/* One focus treatment for the whole product. It sits outside the element, so it
   stays visible on top of the accent button too. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

:focus:not(:focus-visible) {
  outline: none;
}

h1,
h2,
h3 {
  font-weight: var(--weight-strong);
  letter-spacing: var(--tracking-tight);
}

h1 {
  font-size: var(--text-2xl);
  line-height: var(--leading-tight);
  margin: var(--space-5) 0 var(--space-3);
}

h2 {
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
  margin: 0 0 var(--space-3);
}

h3 {
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  margin: 0 0 var(--space-2);
}

p {
  margin: 0 0 var(--space-3);
  max-width: var(--measure-prose);
}

code,
kbd,
samp {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  background: var(--bg-inset);
  padding: var(--space-half) var(--space-1);
  border-radius: var(--radius-sm);
}

/* ===========================================================================
   Pattern: page shell
   .skip-link / .site-header / .brand / .site-nav / main / .site-footer
   =========================================================================== */

.skip-link {
  position: absolute;
  left: var(--space-2);
  top: var(--space-2);
  z-index: 10;
  padding: var(--space-2) var(--space-3);
  background: var(--bg);
  color: var(--text-primary);
  border: var(--border-width) solid var(--border-strong);
  border-radius: var(--radius-md);
  transform: translateY(-200%);
  transition: transform var(--transition-fast);
}

.skip-link:focus {
  transform: translateY(0);
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: var(--space-2) var(--space-4);
  border-bottom: var(--border-width) solid var(--border);
}

.site-header nav,
.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
}

/* Nav targets are 44px tall so a thumb can hit them. The padding does that
   work, not the font size. */
.site-header nav a,
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 var(--space-2);
  text-decoration: none;
  border-radius: var(--radius-sm);
}

.site-header nav a:hover,
.site-nav a:hover {
  background: var(--bg-subtle);
}

.brand {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--text-lg);
  font-weight: var(--weight-strong);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  text-decoration: none;
}

.nav-cta {
  font-weight: var(--weight-strong);
  color: var(--accent);
}

main {
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: 0 var(--space-4) var(--space-7);
}

.site-footer {
  max-width: var(--measure-page);
  margin: var(--space-7) auto 0;
  padding: var(--space-5) var(--space-4) var(--space-6);
  border-top: var(--border-width) solid var(--border);
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.site-footer p {
  margin: 0 0 var(--space-2);
}

/* Footer links are small, so give them a real target without stretching the
   line height of the paragraph they sit in. */
.site-footer a {
  display: inline-block;
  padding: var(--space-1) 0;
}

.hero {
  padding: var(--space-4) 0 var(--space-6);
}

.section {
  padding: var(--space-6) 0;
  border-top: var(--border-width) solid var(--border);
}

.hero + .section,
main > .section:first-child {
  border-top: 0;
  padding-top: var(--space-4);
}

.section.narrow {
  max-width: var(--measure-narrow);
  border-top: 0;
}

/* ===========================================================================
   Typography helpers
   =========================================================================== */

.lede {
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
  margin: 0 0 var(--space-5);
  max-width: var(--measure-prose);
}

.muted {
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.fineprint {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: var(--space-3) 0 0;
  max-width: var(--measure-prose);
}

.more {
  font-weight: var(--weight-strong);
}

/* Scaffolding, not a pattern. Loud on purpose so unreviewed copy cannot ship
   quietly. No public page uses it now; test/unit/views.test.js fails if one does. */
.placeholder {
  outline: 2px dashed var(--sev-deprecation-rail);
  outline-offset: 2px;
}

.placeholder::before {
  content: "[placeholder] ";
  font-size: var(--text-xs);
  font-weight: var(--weight-strong);
  color: var(--sev-deprecation-fg);
  vertical-align: middle;
}

/* ===========================================================================
   Pattern: buttons
   button / .btn / .btn-secondary / .btn-block / .button-link
   A thing that can be pressed looks pressable. A thing that cannot must not.
   =========================================================================== */

button,
.btn,
.button-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-5);
  font: inherit;
  font-weight: var(--weight-strong);
  line-height: 1.2;
  text-align: center;
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-md);
  background: var(--accent);
  color: var(--accent-fg);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition-fast);
}

button:hover,
.btn:hover,
.button-link:hover {
  background: var(--accent-hover);
  color: var(--accent-fg);
}

/* The filter submit is secondary by rule, not by markup. Exactly one element
   per screen may be accent-filled, and on /feed that is Subscribe. The filter
   button sits inside the panel it acts on, so position already tells you what
   it does; it does not need colour as well. Found by looking at the rendered
   feed, where two filled buttons split the emphasis and neither won. */
.btn-secondary,
.filters button[type="submit"] {
  background: var(--bg);
  color: var(--text-primary);
  border-color: var(--border-strong);
}

.btn-secondary:hover,
.filters button[type="submit"]:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
}

.btn-block {
  width: 100%;
}

/* ===========================================================================
   Pattern: subscribe form and fields
   .subscribe / .subscribe-row / .field-label / .field-hint
   .form-error / .field-error / .hp
   =========================================================================== */

.subscribe,
.request {
  background: var(--bg-subtle);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  max-width: var(--measure-narrow);
}

.subscribe label,
.request label,
.field-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: var(--weight-strong);
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.subscribe-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

input[type="email"],
input[type="text"],
input[type="url"],
select {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-3);
  font: inherit;
  color: var(--text-primary);
  background: var(--bg);
  /* 3.3:1 against the page: a field edge is non-text UI, WCAG 1.4.11. */
  border: var(--border-width) solid var(--border-strong);
  border-radius: var(--radius-md);
}

input::placeholder {
  color: var(--text-tertiary);
  opacity: 1;
}

input[aria-invalid="true"] {
  border-color: var(--sev-breaking-rail);
  border-width: 2px;
}

.field-hint {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  color: var(--text-secondary);
}

/* Form error. Carries a word, a glyph and a rail — not just red text. */
.form-error,
.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: var(--space-3);
  max-width: none;
  font-size: var(--text-sm);
  font-weight: var(--weight-strong);
  color: var(--sev-breaking-fg);
  background: var(--sev-breaking-bg);
  border-left: var(--border-width-rail) solid var(--sev-breaking-rail);
  border-radius: var(--radius-sm);
}

.form-error::before,
.field-error::before {
  content: "!";
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-top: 1px;
  font-size: var(--text-xs);
  line-height: 18px;
  text-align: center;
  color: var(--accent-fg);
  background: var(--sev-breaking-rail);
  border-radius: var(--radius-pill);
}

/* Honeypot. Off-screen, never display:none, so bots still fill it. */
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ===========================================================================
   Pattern: feed toolbar
   .filters / .filter-actions / .feed-links / .result-count
   =========================================================================== */

.filters {
  display: grid;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
  padding: var(--space-4);
  background: var(--bg-subtle);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-lg);
}

.filters label {
  display: grid;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-strong);
}

.filter-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

/* "Clear filters" is a reset, not the primary action. It must not compete with
   the Filter button, so it is a link, not a second button. */
.filter-clear {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--text-sm);
  font-weight: var(--weight-strong);
}

/* On a phone the panel folds behind its summary (ux-review F-12), so the first
   screen shows changes. From 720px it fits on one row and is always open. */
.filters-disclosure {
  margin: 0;
  font-size: inherit;
}

.filters-disclosure > summary {
  min-height: var(--tap-min);
  margin-bottom: var(--space-3);
}

.feed-links {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0 0 var(--space-5);
}

.result-count {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0 0 var(--space-3);
}

/* ===========================================================================
   Pattern: change card
   .changes (list) / .change-card (row) / .sev-* (severity rail)

   The pattern is called a card but it renders as a dense row with a severity
   rail, not a floating box. Jakob's Law: this is a changelog, so it should read
   like GitHub releases and Dependabot alerts. Boxes would cost scan density.
   `.change` is the legacy class views.js still emits; drop it from these
   selector lists once views.js emits .change-card.
   =========================================================================== */

.changes {
  list-style: none;
  padding: 0;
  margin: 0;
  border-top: var(--border-width) solid var(--border);
}

/* feedPage renders <ul class="changes"> even with nothing in it, which would
   leave a stray hairline under the empty state. Guard it here so the empty
   state looks finished whether or not views.js stops emitting the list. */
.changes:empty {
  border-top: 0;
}

.change-card,
.change {
  position: relative;
  padding: var(--space-4) 0 var(--space-4) var(--space-4);
  border-bottom: var(--border-width) solid var(--border);
  /* The rail is the common region: it ties badge, provider, title and detail
     together, and separates one change from the next down a long list. */
  border-left: var(--border-width-rail) solid var(--sev-informational-rail);
}

.change-card:hover,
.change:hover {
  background: var(--bg-subtle);
}

/* Severity rail: colour AND border style differ, so four levels stay four
   levels in greyscale or with any colour-vision deficiency. */
.sev-breaking {
  border-left-color: var(--sev-breaking-rail);
  border-left-style: solid;
}

.sev-behaviour {
  border-left-color: var(--sev-behaviour-rail);
  border-left-style: double;
}

.sev-deprecation {
  border-left-color: var(--sev-deprecation-rail);
  border-left-style: dashed;
}

.sev-informational {
  border-left-color: var(--sev-informational-rail);
  border-left-style: dotted;
}

.change-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

/* Pattern: provider label. The provider's own name, never ours. */
.provider,
.provider-label {
  font-weight: var(--weight-strong);
  color: var(--text-primary);
  text-decoration: none;
}

.provider:hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

.change-meta time {
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

/* "First seen by us", not "published by the provider": a different claim needs
   a different label. See the date finding in the ux-review document. */
.date-estimated {
  border-bottom: var(--border-width) dotted var(--border-strong);
  cursor: help;
}

/* The title is the one line a reader scans, so it outranks the badge in size
   and weight. The badge outranks it in colour. */
.change-title {
  display: inline-block;
  font-size: var(--text-lg);
  font-weight: var(--weight-strong);
  line-height: var(--leading-snug);
  color: var(--text-primary);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.change-title:hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

/* Signifier for "this leaves Driftpatch and opens the provider's own page". */
.change-title[target="_blank"]::after,
.notice-link[target="_blank"]::after {
  content: " \2197";
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  color: var(--text-secondary);
  text-decoration: none;
}

.signals,
.change-why {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: var(--space-2) 0 0;
  max-width: var(--measure-prose);
  overflow-wrap: anywhere;
}

/* Progressive disclosure. Hick's Law: the row stays short, the diff opens on
   demand. A summary is clickable, so it looks clickable. */
details {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
}

summary {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: var(--space-1) 0;
  color: var(--accent);
  font-weight: var(--weight-strong);
  cursor: pointer;
}

summary:hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

.findings {
  list-style: none;
  padding: var(--space-3);
  margin: var(--space-2) 0 0;
  background: var(--bg-subtle);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

.findings li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2) 0;
  border-bottom: var(--border-width) solid var(--border);
  overflow-wrap: anywhere;
}

.findings li:first-child {
  padding-top: 0;
}

.findings li:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

/* ===========================================================================
   Pattern: severity badge
   .badge + .badge-breaking | .badge-behaviour | .badge-deprecation
          | .badge-informational | .badge-coming

   Three redundant encodings: the word, the shape, the tint. The shape is drawn
   with a pseudo-element rather than a character, so no screen reader announces
   "black circle Breaking".
   =========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex: 0 0 auto;
  padding: var(--space-half) var(--space-2);
  font-size: var(--text-xs);
  font-weight: var(--weight-strong);
  line-height: 1.5;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  white-space: nowrap;
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-sm);
  color: var(--sev-informational-fg);
  background: var(--sev-informational-bg);
}

.badge::before {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  background: currentColor;
}

/* Breaking: filled disc. The heaviest shape for the heaviest level. */
.badge-breaking {
  color: var(--sev-breaking-fg);
  background: var(--sev-breaking-bg);
}

.badge-breaking::before {
  border-radius: var(--radius-pill);
}

/* Behaviour change: diamond. */
.badge-behaviour {
  color: var(--sev-behaviour-fg);
  background: var(--sev-behaviour-bg);
}

.badge-behaviour::before {
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

/* Deprecation: triangle. */
.badge-deprecation {
  color: var(--sev-deprecation-fg);
  background: var(--sev-deprecation-bg);
}

.badge-deprecation::before {
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

/* Informational: hollow ring, the lightest shape for the lightest level.
   .badge-warning and .badge-info are legacy aliases views.js emits for oasdiff
   levels; they should become .badge-behaviour and .badge-informational. */
.badge-informational,
.badge-info,
.badge-warning {
  color: var(--sev-informational-fg);
  background: var(--sev-informational-bg);
}

.badge-informational::before,
.badge-info::before,
.badge-warning::before {
  background: transparent;
  border: 2px solid currentColor;
  border-radius: var(--radius-pill);
}

/* Not a severity. Marks a roadmap claim as not built yet, per the [COMING]
   markers in the PER-3 copy. Neutral and dashed on purpose: it must read as
   "not yet", never as an alert, and never as a live feature. */
.badge-coming {
  color: var(--text-secondary);
  background: var(--bg-inset);
  border-color: var(--border-strong);
  border-style: dashed;
}

.badge-coming::before {
  display: none;
}

/* ===========================================================================
   Feed page call to action: .cta
   =========================================================================== */

/* Von Restorff: the subscribe action is the one thing on this page allowed to
   shout. Nothing else gets a filled button. */
.cta {
  border-top: var(--border-width) solid var(--border);
}

.cta h2 {
  font-size: var(--text-xl);
}

/* ===========================================================================
   Sales landing page (PER-43). Spec: the `landing-ventas-ux` document on
   PER-43. Three weights, and only three:
   - Tier 1, dominant: the hero (size, and the one filled button on the first
     screen), the proof patch and the offer (both on a .band).
   - Tier 2: the closing form. It has the filled button, but no band.
   - Tier 3, quiet: .section-quiet. Smaller heading, no surface.
   =========================================================================== */

/* Screen-reader-only text: "Added line:" before a diff line, and the like.
   Same off-screen technique as .hp, for the opposite reason. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- Hero: copy and the request form ------------------------------------ */

.hero-grid {
  display: grid;
  gap: var(--space-5);
}

.hero-grid h1 {
  margin-top: 0;
}

.hero-copy .lede {
  margin-bottom: 0;
}

.hero-action {
  max-width: var(--measure-narrow);
}

/* The live numbers under the hero. Facts, not a pitch: small, secondary,
   tabular so they do not jitter when they change. */
.trust-line {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The secondary action ("take the free feed"). A link, never a button. */
.alt-link {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
}

.alt-link a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

/* --- Request form (repo URL + email) ------------------------------------
   Same panel, fields and error pattern as .subscribe. Fields stack at every
   width and the button is full width: in a 360px card and in the hero's
   right-hand column there is no room for a row. */
.field + .field {
  margin-top: var(--space-4);
}

.request .field-error {
  margin-top: var(--space-2);
}

/* --space-4, not --space-5: at 360x640 that is the difference between the
   button sitting on the first screen and sitting under it. */
.request button[type="submit"] {
  width: 100%;
  margin-top: var(--space-4);
}

/* --- Section weights ---------------------------------------------------- */

/* Tier 3. Context a reader may skip; it must not compete with the bands. */
.section-quiet h2 {
  font-size: var(--text-lg);
}

/* Tier 1. A surface band: full bleed on a phone (it reaches into main's
   gutter, which is exactly --space-4), a framed panel from 720px. */
.band {
  margin: var(--space-6) calc(var(--space-4) * -1);
  padding: var(--space-7) var(--space-4);
  background: var(--bg-subtle);
  border-top: var(--border-width) solid var(--border);
  border-bottom: var(--border-width) solid var(--border);
}

.band + .section {
  border-top: 0;
}

.band h2 {
  font-size: var(--text-xl);
}

/* --- Problem: three costs ----------------------------------------------- */

.costs {
  list-style: none;
  margin: 0;
  padding: 0;
}

.costs li {
  padding: var(--space-3) 0;
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  border-bottom: var(--border-width) solid var(--border);
  max-width: var(--measure-prose);
}

.costs li:first-child {
  padding-top: 0;
}

.costs li:last-child {
  border-bottom: 0;
}

/* --- How it works: Watch, Match, Patch ---------------------------------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-5);
}

.step h3 {
  margin-bottom: var(--space-1);
}

.step p {
  margin: 0;
}

/* The step's real status, in words. "Live" carries no chip on purpose: the
   absence of .badge-coming is the signal, and a green "live" chip would be
   colour doing the work. */
.step-state {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  font-weight: var(--weight-strong);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

/* --- Proof: the patch ----------------------------------------------------
   Source first, then the diff, then the checks. On a phone they stack in
   that order; from 1000px the source and checks sit in a column to the left
   of the diff, so the notice still reads before the code (F-pattern). */
.patch {
  display: grid;
  gap: var(--space-4);
  margin: var(--space-5) 0 var(--space-4);
}

.patch dl {
  margin: 0;
}

.patch dt {
  font-size: var(--text-xs);
  font-weight: var(--weight-strong);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.patch dd {
  margin: 0 0 var(--space-3);
}

.patch dd:last-child {
  margin-bottom: 0;
}

.patch-checks {
  font-size: var(--text-sm);
}

/* Inline-block with padding, not inline-flex: in the narrow column at 1000px
   the link wraps, and the ↗ must stay at the end of the words. */
.notice-link {
  display: inline-block;
  padding: var(--space-3) 0;
  font-weight: var(--weight-strong);
}

/* The diff itself. No horizontal scroll and no clipping at any width: long
   lines wrap, and the wrapped part hangs --diff-hang characters to the right
   of the line's own indentation. Nothing sets overflow here, so nothing can
   hide code; the shots harness measures that no line is wider than the box. */
.diff-figure {
  margin: 0;
  background: var(--bg);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

/* On a phone the diff takes the band's gutter too: edge to edge, like code on
   GitHub's mobile view. That is four more characters per line at 360px. */
.band .diff-figure {
  margin: 0 calc(var(--space-4) * -1);
  border-left: 0;
  border-right: 0;
  border-radius: 0;
}

.patch-file {
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  border-bottom: var(--border-width) solid var(--border);
  overflow-wrap: anywhere;
}

.diff {
  list-style: none;
  margin: 0;
  padding: var(--space-1) 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
}

.diff-line {
  display: grid;
  grid-template-columns: 2ch minmax(0, 1fr);
  padding: 0 var(--space-2);
}

.diff-mark {
  font-weight: var(--weight-strong);
  user-select: none;
}

.diff-code {
  --indent: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  padding-left: calc((var(--indent) + var(--diff-hang)) * 1ch);
  text-indent: calc((var(--indent) + var(--diff-hang)) * -1ch);
}

/* Leading spaces of the line, halved and capped at 8 (16 characters), so the
   hang lines up under the code and not under the margin. Views.js picks the
   class; CSP forbids an inline style, which is why these are classes. */
.diff-indent-1 { --indent: 2; }
.diff-indent-2 { --indent: 4; }
.diff-indent-3 { --indent: 6; }
.diff-indent-4 { --indent: 8; }
.diff-indent-5 { --indent: 10; }
.diff-indent-6 { --indent: 12; }
.diff-indent-7 { --indent: 14; }
.diff-indent-8 { --indent: 16; }

.diff-hunk {
  color: var(--text-secondary);
  background: var(--bg-inset);
}

/* Context lines are plain code on the figure's own surface. Named so the
   markup's four line kinds map to four rules. */
.diff-ctx {
  background: var(--bg);
}

.diff-add {
  background: var(--diff-add-bg);
}

.diff-add .diff-mark {
  color: var(--diff-add-fg);
}

.diff-del {
  background: var(--diff-del-bg);
}

.diff-del .diff-mark {
  color: var(--diff-del-fg);
}

/* The exact token that changed. Strike-through and underline are the
   track-changes convention everyone already reads (Jakob's Law), and they
   survive greyscale, forced colours and print. */
/* nowrap keeps the changed token in one piece: a model id must not break at
   its own hyphen ("claude-" / "3-haiku…"). Its quotes stay with it too, since
   there is no break opportunity between a quote and the token. That is only
   safe for short tokens, so views.js emits <ins>/<del> for a token of at most
   32 characters and plain text above that (spec: landing-ventas-ux). */
.diff ins,
.diff del {
  white-space: nowrap;
  border-radius: var(--radius-sm);
  text-decoration-thickness: var(--border-width);
}

.diff ins {
  background: var(--diff-add-strong);
  text-decoration-line: underline;
  text-underline-offset: 2px;
}

.diff del {
  background: var(--diff-del-strong);
  text-decoration-line: line-through;
}

/* The engine's own track record, under the patch: what it refused, too. */
.patch-record {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* --- Offer -------------------------------------------------------------- */

/* The live slot count. The biggest number on the page, and a real one. */
.slots {
  margin: 0 0 var(--space-4);
  font-size: var(--text-lg);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.slots-count {
  display: block;
  font-size: var(--text-2xl);
  line-height: var(--leading-tight);
  font-weight: var(--weight-strong);
  color: var(--text-primary);
  letter-spacing: var(--tracking-tight);
}

.offer-terms {
  display: grid;
  gap: var(--space-4);
  margin: var(--space-5) 0;
}

.offer-terms > div {
  padding-top: var(--space-3);
  border-top: var(--border-width) solid var(--border);
}

.offer-terms dt {
  font-weight: var(--weight-strong);
  margin-bottom: var(--space-1);
}

.offer-terms dd {
  margin: 0;
  max-width: var(--measure-prose);
}

/* --- Objections ---------------------------------------------------------
   Open, not behind <details>: five two-line answers are cheaper to read
   than five clicks. */
.faq {
  margin: 0;
}

.faq dt {
  font-weight: var(--weight-strong);
  padding-top: var(--space-4);
  border-top: var(--border-width) solid var(--border);
}

.faq dt:first-child {
  padding-top: 0;
  border-top: 0;
}

.faq dd {
  margin: var(--space-1) 0 var(--space-4);
  max-width: var(--measure-prose);
}

.faq dd p {
  margin: 0 0 var(--space-1);
}

/* --- Pricing --------------------------------------------------------------
   Cards, not a table: a four-column table does not fit 360px. No card has a
   button, because nothing here is on sale; the section's one action is the
   free-slot link under the cards.

   What works today and what is planned are two different structures, not
   one list with a footnote. "Works today" is a plain list on the card.
   "Planned" sits in its own dashed box, the same dashed "not yet" language as
   .badge-coming, in secondary text, always after the
   works-today list. A reader comparing tiers compares like with like: real
   with real, planned with planned. Works-today items take a solid disc and planned items a
   hollow circle; set both explicitly, because a list nested in a list
   defaults to circles and would make the two look alike.

   Weight: tier 3. The price is --text-lg, smaller than the band headings and
   the slot count, so pricing supports the offer and does not compete with it. */
.plans {
  list-style: none;
  margin: var(--space-4) 0 var(--space-4);
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

.plan {
  padding: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

.plan h3 {
  margin-bottom: var(--space-1);
}

.plan-price {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-strong);
  font-variant-numeric: tabular-nums;
}

/* "launch price, locked for the first 10 slots". Plain sentence-case text,
   never a .badge: a badge is uppercase and nowrap, and at 360px this label
   in caps is wider than the card. It wraps at word boundaries; views.js
   joins "first 10 slots" with non-breaking spaces so the number never ends
   a line alone. */
.plan-lock {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.plan-list-title {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-xs);
  font-weight: var(--weight-strong);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.plan-today,
.plan-planned ul {
  margin: 0;
  padding-left: var(--space-5);
}

.plan-today {
  list-style: disc;
}

.plan-today li,
.plan-planned li {
  margin-bottom: var(--space-1);
}

.plan-planned {
  padding: 0 var(--space-3) var(--space-3);
  color: var(--text-secondary);
  border: var(--border-width) dashed var(--border-strong);
  border-radius: var(--radius-md);
}

.plan-today {
  margin-bottom: var(--space-4);
}

.plan-planned .plan-list-title {
  margin-top: var(--space-3);
}

.plan-planned ul {
  list-style: circle;
}

.plans-note {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* ===========================================================================
   Pattern: empty state and error state
   .empty-state / .empty / .error-state
   Both say what happened and what to do next. Nielsen: error recovery.
   =========================================================================== */

.empty-state,
.empty {
  padding: var(--space-6) var(--space-4);
  text-align: center;
  color: var(--text-secondary);
  background: var(--bg-subtle);
  border: var(--border-width) dashed var(--border-strong);
  border-radius: var(--radius-lg);
}

.empty-state p,
.empty p {
  max-width: none;
  margin: 0 0 var(--space-3);
}

.empty-state p:last-child,
.empty p:last-child {
  margin-bottom: 0;
}

.empty-state-title {
  display: block;
  font-size: var(--text-lg);
  font-weight: var(--weight-strong);
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.empty-state-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
}

.error-state {
  padding: var(--space-4);
  color: var(--text-primary);
  background: var(--sev-breaking-bg);
  border-left: var(--border-width-rail) solid var(--sev-breaking-rail);
  border-radius: var(--radius-sm);
}

.error-state-title {
  display: block;
  font-weight: var(--weight-strong);
  color: var(--sev-breaking-fg);
  margin-bottom: var(--space-1);
}

/* ===========================================================================
   Pattern: provider list and pager
   =========================================================================== */

.provider-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 var(--space-5);
}

.provider-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-2) 0;
  border-bottom: var(--border-width) solid var(--border);
}

.provider-list a {
  font-weight: var(--weight-strong);
  text-decoration: none;
}

.provider-list a:hover {
  text-decoration: underline;
}

/* Newer always sits left, Older always sits right, even when only one of them
   exists. `space-between` alone put a lone "Older" link on the left edge. */
.pager {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-5) 0 0;
}

.pager-prev {
  grid-column: 1;
  justify-self: start;
}

.pager-next {
  grid-column: 2;
  justify-self: end;
}

/* Until views.js adds .pager-prev / .pager-next, a lone link has no side. CSS
   cannot tell "Newer" from "Older", so centre it: neutral is never wrong. The
   :not([class]) stops this winning once the real classes are there. */
.pager > a:only-child:not([class]) {
  grid-column: 1 / -1;
  justify-self: center;
}

.pager a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 var(--space-2);
  font-weight: var(--weight-strong);
  text-decoration: none;
}

.pager a:hover {
  text-decoration: underline;
}

/* ===========================================================================
   720px and up
   =========================================================================== */

@media (min-width: 720px) {
  :root {
    --text-2xl: 40px;
  }

  .hero {
    padding-top: var(--space-6);
  }

  .subscribe-row {
    flex-direction: row;
    align-items: stretch;
  }

  .subscribe-row input {
    flex: 1 1 auto;
  }

  .subscribe-row button {
    flex: 0 0 auto;
  }

  .filters {
    grid-template-columns: 2fr 1fr auto;
    align-items: end;
  }

  /* Without ::details-content the disclosure stays as it is on a phone, which
     works. Hiding the summary there would hide the filters with it. */
  @supports selector(::details-content) {
    .filters-disclosure > summary {
      display: none;
    }

    .filters-disclosure::details-content {
      content-visibility: visible;
    }
  }

  .provider-list {
    grid-template-columns: 1fr 1fr;
  }

  .change-card,
  .change {
    padding-left: var(--space-5);
  }

  /* Sales landing page (PER-43) */
  .band {
    margin: var(--space-7) 0;
    padding: var(--space-8) var(--space-6);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-lg);
  }

  .band .diff-figure {
    margin: 0;
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-md);
  }

  .diff {
    font-size: var(--text-sm);
  }

  .diff-line {
    padding: 0 var(--space-3);
  }

  .steps {
    grid-template-columns: 1fr 1fr 1fr;
  }

  .offer-terms {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-5);
  }

  /* Two plans or three, side by side, whichever variant the CEO switches on. */
  .plans {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }
}

/* ===========================================================================
   1000px and up. Only three-up grids need this; nothing else changes.
   =========================================================================== */

@media (min-width: 1000px) {
  .provider-list {
    grid-template-columns: 1fr 1fr 1fr;
  }

  /* Sales landing page (PER-43). Copy left, form right: the form is on the
     first screen of a laptop without scrolling, next to the promise it acts
     on. The trust line sits under the copy, not under the form. */
  .hero-grid {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    grid-template-areas:
      "copy action"
      "trust action";
    grid-template-rows: auto 1fr;
    column-gap: var(--space-6);
    row-gap: var(--space-4);
    align-items: start;
  }

  .hero-copy { grid-area: copy; }
  .hero-action { grid-area: action; }
  .hero-grid .trust-line { grid-area: trust; }

  /* Notice and checks in a narrow left column, the diff to their right. */
  .patch {
    grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
    grid-template-areas:
      "source diff"
      "checks diff";
    grid-template-rows: auto 1fr;
    column-gap: var(--space-5);
    align-items: start;
  }

  .patch-source { grid-area: source; }
  .diff-figure { grid-area: diff; }
  .patch-checks { grid-area: checks; }
}

/* ===========================================================================
   Accessibility fallbacks
   =========================================================================== */

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

/* Windows high contrast and other forced-colour modes drop our tints. Keep the
   badge readable as a bordered chip and keep the severity rail visible. */
@media (forced-colors: active) {
  .badge {
    forced-color-adjust: none;
    color: CanvasText;
    background: Canvas;
    border-color: CanvasText;
  }

  .badge::before {
    forced-color-adjust: none;
    background: CanvasText;
  }

  .badge-informational::before,
  .badge-info::before,
  .badge-warning::before {
    background: transparent;
    border-color: CanvasText;
  }

  .change-card,
  .change {
    border-left-color: CanvasText;
  }

  :focus-visible {
    outline-color: Highlight;
  }
}
