/* Tether web identity layer — shared by landing and onboarding (login retired U5).
   Palette values mirror design/tokens/tether-tokens.json color.web.{dark,light};
   tests/test_web_landing_identity.py fails when they drift. No remote fonts.
   Motion is opacity/transform/stroke only, and only where it encodes real state. */

:root {
  --bg: #0c0f13;
  --bg-grade: #10141a;
  --bg-glow: #171d26;
  --surface-1: #14181f;
  --surface-2: #1a1f28;
  --surface-3: #222833;
  --hairline: rgba(255, 255, 255, 0.075);
  --hairline-strong: rgba(255, 255, 255, 0.14);
  --overlay-up: rgba(255, 255, 255, 0.06);
  --overlay-up-strong: rgba(255, 255, 255, 0.16);
  --overlay-down: rgba(0, 0, 0, 0.2);

  --text-1: #eef2f7;
  --text-2: #aab5c2;
  --text-3: #8a95a3;

  --pass: #56c99c;
  --pass-dim: rgba(86, 201, 156, 0.14);
  --fail: #ef7a6d;
  --fail-dim: rgba(239, 122, 109, 0.13);
  --warn: #e0a94a;
  --warn-dim: rgba(224, 169, 74, 0.13);
  --info: #7fb0e3;
  --focus-ring: var(--text-1);
  --info-dim: rgba(127, 176, 227, 0.12);
  /* Text colour for content painted ON a filled --info surface (the primary
     button). Dark mode's --info (#7fb0e3) is light, so it needs dark text
     (measured 8.43:1 against --bg); light mode's --info (#2a6db0) is dark, so
     it needs light text (measured 5.37:1 against white). Both clear WCAG AA
     4.5:1 -- see tests/test_web_contrast_tint_discipline.py for the pinned
     ratios and web/shared.css history for why one literal cannot serve both. */
  --on-info: #0c0f13;

  /* Hairlines carrying a state colour: the palette colour at 0.35 alpha. */
  --pass-line: rgba(86, 201, 156, 0.35);
  --fail-line: rgba(239, 122, 109, 0.35);
  --warn-line: rgba(224, 169, 74, 0.35);
  --info-line: rgba(127, 176, 227, 0.35);

  /* Fill behind state-coloured text. Dark mode can tint; the light palette's
     status colours only clear 4.5:1 over an opaque surface, so light mode
     resolves these to --surface-1 instead of the tint. */
  --tint-pass: var(--pass-dim);
  --tint-fail: var(--fail-dim);
  --tint-warn: var(--warn-dim);
  --tint-info: var(--info-dim);

  /* --bg at 0.92 alpha, so the sticky bar reads as the page it floats over. */
  --nav-bg: rgba(12, 15, 19, 0.92);
  /* Fully opaque --bg for the mobile disclosure panel: measured, the 0.92
     translucent tint plus 16px blur still let hero copy read through as
     ghosted-but-legible text behind the open link list at 390px (high
     contrast text survives the blur radius). A dropdown that must fully
     obscure the page below it needs a solid surface, not a glass one. */
  --nav-bg-solid: var(--bg);

  --t-11: 0.6875rem;
  --t-12: 0.75rem;
  --t-13: 0.8125rem;
  --t-15: 0.9375rem;
  --t-18: 1.125rem;
  --t-20: 1.25rem;
  --t-24: 1.5rem;
  --t-34: 2.125rem;
  --t-40: 2.5rem;
  --t-48: 3rem;
  --t-56: 3.5rem;
  --t-64: 4rem;

  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --s-8: 64px;
  --s-9: 96px;
  --gutter: clamp(16px, 4vw, 32px);
  --measure: 40rem;
  --page: 72rem;

  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 16px;
  --r-pill: 999px;

  --e-1: 0 1px 2px rgba(0, 0, 0, 0.4);
  --e-2: 0 2px 6px rgba(0, 0, 0, 0.35), 0 12px 28px rgba(0, 0, 0, 0.28);
  --e-3: 0 4px 12px rgba(0, 0, 0, 0.4), 0 24px 56px rgba(0, 0, 0, 0.34);

  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-fast: 150ms;
  --dur: 200ms;
  --dur-slow: 240ms;
  /* Same value as status-dashboard/tokens.css --press-scale: a pressed button
     is a real state (finger/pointer down), never a decorative hover. */
  --press-scale: 0.97;

  /* Release curves ported from Open Props (argyleink/open-props, MIT,
     src/props.easing.css --ease-spring-2, 2026-10-11 head).
     --ease-spring is a CSS linear() spring that settles ~3% past target and
     back, so a control released from :active lands like a physical key rather
     than sliding. It drives transform only; reduced-motion kills it below. */
  --ease-spring: var(--ease);
  --dur-spring: 320ms;

  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;

  --focus: 0 0 0 3px rgba(127, 176, 227, 0.28);

  /* Layered depth kit (from landing a0efebd), shared so every surface lights
     from one source: an elevation tier plus a top-lit inset hairline. */
  --lit: inset 0 1px 0 var(--overlay-up);
  --sunk: inset 0 1px 2px var(--overlay-down);
  --depth-1: var(--e-1), var(--lit);
  --depth-2: var(--e-2), var(--lit);
  --depth-3: var(--e-3), var(--lit);
  /* Two-layer focus: the 3px outline (offset) plus a soft outer halo. */
  --focus-halo: 0 0 0 6px var(--info-dim);

  /* Display scale for secondary-surface titles (landing hero keeps its own). */
  --t-display: clamp(1.75rem, 6vw, var(--t-48));

  /* Smallest pointer target the surfaces ship. */
  --tap: 44px;
}

/* Light palette: design/tokens/tether-tokens.json color.web.light. */
@media (prefers-color-scheme: light) {
  :root {
    --bg: #f4f6f9;
    --bg-grade: #eaeef3;
    --bg-glow: #e3e9f2;
    --surface-1: #ffffff;
    --surface-2: #f3f5f8;
    --surface-3: #e7ebf0;
    --hairline: rgba(16, 24, 40, 0.08);
    --hairline-strong: rgba(16, 24, 40, 0.16);
    --overlay-up: rgba(16, 24, 40, 0.05);
    --overlay-up-strong: rgba(16, 24, 40, 0.12);
    --overlay-down: rgba(16, 24, 40, 0.06);

    --text-1: #14181f;
    --text-2: #3d4856;
    --text-3: #5b6675;

    --pass: #177a54;
    --pass-dim: rgba(23, 122, 84, 0.12);
    --fail: #c2402f;
    --fail-dim: rgba(194, 64, 47, 0.12);
    --warn: #8a5e05;
    --warn-dim: rgba(138, 94, 5, 0.12);
    --info: #2a6db0;
    --focus-ring: var(--text-1);
    --info-dim: rgba(42, 109, 176, 0.12);
    --on-info: #ffffff;

    --pass-line: rgba(23, 122, 84, 0.35);
    --fail-line: rgba(194, 64, 47, 0.35);
    --warn-line: rgba(138, 94, 5, 0.35);
    --info-line: rgba(42, 109, 176, 0.35);

    /* Opaque, so state-coloured text keeps 4.5:1 (the tints land at ~4.2). */
    --tint-pass: var(--surface-1);
    --tint-fail: var(--surface-1);
    --tint-warn: var(--surface-1);
    --tint-info: var(--surface-1);

    --nav-bg: rgba(244, 246, 249, 0.92);

    --e-1: 0 1px 2px rgba(16, 24, 40, 0.10);
    --e-2: 0 2px 6px rgba(16, 24, 40, 0.08), 0 12px 28px rgba(16, 24, 40, 0.08);
    --e-3: 0 4px 12px rgba(16, 24, 40, 0.10), 0 24px 56px rgba(16, 24, 40, 0.10);

    --focus: 0 0 0 3px rgba(42, 109, 176, 0.28);
  }
}

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

html, body { margin: 0; padding: 0; }

html {
  overflow-x: clip;
}

body {
  min-height: 100vh;
  min-width: 0;
  background:
    radial-gradient(920px 420px at 12% -10%, var(--bg-glow) 0%, transparent 64%),
    linear-gradient(180deg, var(--bg-grade) 0%, var(--bg) 48%);
  background-attachment: scroll;
  color: var(--text-1);
  font-family: var(--sans);
  font-size: var(--t-15);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3, h4 { margin: 0; font-weight: 560; letter-spacing: -0.018em; color: var(--text-1); }
h1 { font-size: var(--t-40); line-height: 1.12; }
h2 { font-size: var(--t-34); line-height: 1.18; }
h3 { font-size: var(--t-18); line-height: 1.3; }

p { margin: 0; }

a { color: var(--info); }

code, pre, .num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

code, pre {
  overflow-wrap: anywhere;
  word-break: break-word;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

.skip-link {
  position: absolute; left: var(--s-4); top: -56px; z-index: 30;
  padding: var(--s-2) var(--s-3); border-radius: var(--r-sm);
  background: var(--surface-3); color: var(--text-1); text-decoration: none;
  font-size: var(--t-13);
}
.skip-link:focus, .skip-link:focus-visible { top: var(--s-4); }

/* Brand glyph: two machines joined by one tether. The lower node is --pass
   (a qualified controller), the upper node is --info (a connected peer), and
   the bar between them is the paired link. Same grammar as the fabric map. */
.mark {
  width: 32px; height: 32px; border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1px solid var(--hairline-strong);
  box-shadow: var(--e-1), inset 0 1px 0 var(--overlay-up);
  position: relative; flex: none;
}
.mark::before {
  content: ""; position: absolute;
  left: 9px; top: 15px; width: 14px; height: 2px;
  border-radius: 1px;
  background: var(--pass);
  transform: rotate(-45deg);
}
.mark::after {
  content: ""; position: absolute;
  left: 8px; top: 18px; width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--pass);
  box-shadow: 10px -10px 0 0 var(--info);
}

.brand {
  display: inline-flex; align-items: center; gap: var(--s-3);
  text-decoration: none; color: inherit;
  min-width: 0;
}
.brand span:last-child {
  font-size: var(--t-18); font-weight: 580; letter-spacing: -0.012em;
}

.chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  border: 1px solid var(--hairline-strong);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: var(--t-11);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.4;
}
/* A translucent tint over the hero glow measured 4.49:1 for the fail chip at 390px (pixels, dark).
   The opaque --surface-1 underlay pins the backdrop so the ratio no longer depends on what scrolls behind. */
.chip--pass { color: var(--pass); background: linear-gradient(var(--tint-pass), var(--tint-pass)), var(--surface-1); border-color: var(--pass-line); }
.chip--warn { color: var(--warn); background: linear-gradient(var(--tint-warn), var(--tint-warn)), var(--surface-1); border-color: var(--warn-line); }
.chip--fail { color: var(--fail); background: linear-gradient(var(--tint-fail), var(--tint-fail)), var(--surface-1); border-color: var(--fail-line); }
.chip--info { color: var(--info); background: linear-gradient(var(--tint-info), var(--tint-info)), var(--surface-1); border-color: var(--info-line); }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-sm);
  border: 1px solid var(--hairline-strong);
  background: var(--surface-2);
  color: var(--text-1);
  font: inherit; font-size: var(--t-13); font-weight: 560;
  cursor: pointer; text-decoration: none;
  box-shadow: var(--e-1);
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              transform var(--dur-spring) var(--ease-spring), box-shadow var(--dur-fast) var(--ease);
  min-height: 36px;
}
/* Same lift language .card:hover already uses (transform + a deeper shadow
   tier), scaled down for a control this size: a button is exactly as
   "interactive" as a card, so hovering it should not read flatter than
   hovering a card next to it. Guarded by (hover: hover) and reduced-motion
   like every other transform on this page. */
@media (hover: hover) {
  .btn:hover:not(:disabled) {
    background: var(--surface-3);
    border-color: var(--overlay-up-strong);
    transform: translateY(-1px);
    box-shadow: var(--e-2);
  }
}
.btn:active { background: var(--surface-1); }
.btn:active:not(:disabled) { transform: scale(var(--press-scale)); box-shadow: var(--e-1); }
.btn:disabled { color: var(--text-3); cursor: not-allowed; opacity: 0.7; }

/* A phone gives a 36px control a fingertip; the dense size stays for a mouse. */
@media (pointer: coarse) {
  .btn { min-height: var(--tap); }
}

/* Busy: a real in-flight request. The label says it in text ("Signing in..."), the ring is
   the second channel, and aria-busy carries it to assistive tech. Under reduced motion the
   shared block below zeroes the spin, leaving a static open ring. */
.btn[aria-busy="true"] { cursor: progress; }
.btn[aria-busy="true"]::before {
  content: "";
  width: 12px; height: 12px; flex: none;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-right-color: transparent;
  animation: btn-spin 700ms linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

/* Filled accent so a primary action reads as visually distinct from a plain
   .btn (--surface-2/--surface-3) at a glance -- see test_web_contrast_tint_discipline.py
   test_btn_primary_background_differs_from_a_plain_btn_in_both_themes /
   test_btn_primary_text_meets_aa_contrast_in_both_themes for the pinned
   background-differs and >=4.5:1 assertions in both themes. */
.btn--primary {
  background: var(--info);
  border-color: var(--info);
  color: var(--on-info);
}
@media (hover: hover) {
  .btn--primary:hover:not(:disabled) {
    background: color-mix(in srgb, var(--info) 88%, black);
    border-color: color-mix(in srgb, var(--info) 88%, black);
    /* Inherit .btn:hover's lift transform/shadow; only the fill darkens here. */
  }
}
.btn--primary:active:not(:disabled) {
  background: color-mix(in srgb, var(--info) 78%, black);
  border-color: color-mix(in srgb, var(--info) 78%, black);
}
.btn--primary:focus-visible {
  /* The global :focus-visible outline is --info, which is invisible against
     an --info fill (measured contrast 1.0). Swap the outline to --on-info so
     the ring stays visible on the button it is drawn on. */
  outline-color: var(--on-info);
  outline-offset: -3px;
  box-shadow: var(--e-1), var(--focus-halo);
}
.btn--primary:disabled {
  background: var(--surface-3);
  border-color: var(--hairline-strong);
  color: var(--text-3);
}

.btn--large {
  padding: 10px var(--s-5);
  font-size: var(--t-15);
  min-height: var(--tap);
}

.btn--full { width: 100%; }

.site-nav {
  position: sticky; top: 0; z-index: 10;
  background: var(--nav-bg);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--hairline);
}
.site-nav__inner {
  position: relative;
  max-width: var(--page);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--gutter);
  flex-wrap: wrap;
  min-width: 0;
}
.site-nav__links {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  list-style: none; margin: 0; padding: 0;
  min-width: 0;
}
.site-nav__links a {
  color: var(--text-2); text-decoration: none;
  font-size: var(--t-13);
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  transition: color var(--dur-fast) var(--ease);
}
.site-nav__links a:hover { color: var(--text-1); }
.site-nav__links a[aria-current="page"] { color: var(--text-1); font-weight: 580; }
/* `.site-nav__links a` (0,1,1) outranks `.btn--primary` (0,1,0), so the Status pill
   inherited nav grey on its --info fill: axe measured 1.09:1 dark, 1.73:1 light.
   Re-assert the button's own text colour at nav specificity, including hover/current. */
.site-nav__links a.btn--primary,
.site-nav__links a.btn--primary:hover,
.site-nav__links a.btn--primary[aria-current="page"] { color: var(--on-info); }
.site-nav .brand { min-height: var(--tap); }

/* Mobile nav disclosure. Below 960px (measured: the widest real nav, landing's
   9 links, still wraps to a second row up to 900px -- see
   tests/test_web_narrow_viewport_usability.py test_header_height_budget for
   the pinned per-width/page budget) `.site-nav__links` collapses behind a
   toggle button so the sticky header cannot eat 21-27% of a phone viewport.
   `.site-nav.is-collapsible` is added by shared-nav.js once it has run; with
   no JS the button never appears (display:none by default) and the list
   stays in normal flow, visible and reachable, wrapping same as before --
   the documented no-JS fallback. */
.site-nav__toggle {
  display: none;
  align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--hairline-strong);
  background: var(--surface-2);
  color: var(--text-1);
  font: inherit; font-size: var(--t-13); font-weight: 560;
  cursor: pointer;
  min-height: var(--tap); min-width: var(--tap);
}
.site-nav__toggle:hover { background: var(--surface-3); }
.site-nav__toggle-icon {
  width: 18px; height: 18px;
  display: inline-block;
  position: relative;
}
.site-nav__toggle-icon::before, .site-nav__toggle-icon::after {
  content: ""; position: absolute; left: 0; right: 0;
  height: 2px; background: currentColor; border-radius: 1px;
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.site-nav__toggle-icon::before { top: 4px; }
.site-nav__toggle-icon::after { top: 12px; }
.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-icon::before {
  top: 8px; transform: rotate(45deg);
}
.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-icon::after {
  top: 8px; transform: rotate(-45deg);
}

@media (max-width: 959.98px) {
  /* .site-nav__inner is a wrapping flexbox with `gap: var(--s-3) ...`, and a
     flex line's own row-gap allocation is independent of that line's content
     height -- measured, an empty second flex line still added a phantom 12px
     to the "closed" header height (68px content -> 80px) even at
     max-height:0. Taking the panel out of flow while closed (position:
     absolute) removes it from the flex line calculation entirely; switching
     back to a normal flex child only once open (data-nav-open="true") is what
     lets it actually push page content down / sit in the sticky bar's own
     stacking context correctly while expanded. */
  .site-nav.is-collapsible .site-nav__toggle { display: inline-flex; }
  .site-nav.is-collapsible .site-nav__links {
    position: absolute;
    left: 0; right: 0; top: 100%;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 0;
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    background: var(--nav-bg-solid);
    border-bottom: 1px solid var(--hairline);
    padding: 0 var(--gutter);
    transition: max-height var(--dur-slow) var(--ease), opacity var(--dur) var(--ease);
  }
  /* .site-nav__links' direct flex children are <li>, not <a> -- without this
     the <li> boxes kept their natural shrink-to-fit width under
     flex-direction:column and the row overflowed sideways (measured:
     scrollWidth 473-637px against a 390px viewport) instead of stacking. */
  .site-nav.is-collapsible .site-nav__links li { width: 100%; }
  .site-nav.is-collapsible .site-nav__links a { min-height: var(--tap); width: 100%; }
  .site-nav.is-collapsible[data-nav-open="true"] .site-nav__links {
    /* Generous enough for every real nav's link count (up to 9 stacked rows)
       plus the .btn--primary Status pill; a transition targets height, so
       this only needs to clear the tallest real content, not match it. */
    max-height: 90vh;
    opacity: 1;
    padding: var(--s-3) var(--gutter) var(--s-4);
  }
}

@media (min-width: 960px) {
  .site-nav__toggle { display: none !important; }
  .site-nav__links { max-height: none; opacity: 1; }
}

.card {
  position: relative;
  min-width: 0;
  padding: var(--s-5);
  border-radius: var(--r-lg);
  border: 1px solid var(--hairline);
  background: var(--surface-1);
  box-shadow: var(--depth-2);
}

/* Two-layer focus ring: the offset outline stays (3px solid, the keyboard
   gates read it) and a soft halo sits outside it so the ring separates from
   any card edge it lands on. */
:focus-visible { box-shadow: var(--focus-halo); }
.btn:focus-visible, .code-block__copy:focus-visible { box-shadow: var(--e-1), var(--focus-halo); }

.code-block {
  background: var(--surface-3);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-md);
  padding: var(--s-4);
  margin: 0 0 var(--s-4);
  position: relative;
  min-width: 0;
  box-shadow: var(--sunk);
}
/* Copy feedback is a real state (clipboard write resolved), so it may colour. */
[data-copy-state="done"] { color: var(--pass); border-color: var(--pass-line); }
[data-copy-state="failed"] { color: var(--fail); border-color: var(--fail-line); }
.code-block__header {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}
.code-block__label {
  font-size: var(--t-11); text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-3); font-weight: 560;
}
.code-block__copy {
  display: inline-flex; align-items: center;
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-sm);
  border: 1px solid var(--hairline);
  background: var(--surface-1);
  color: var(--text-3);
  font: inherit; font-size: var(--t-11);
  cursor: pointer;
  min-height: 28px;
}
.code-block__copy:hover { color: var(--text-1); border-color: var(--hairline-strong); }
.code-block pre {
  margin: 0; padding: 0;
  font-size: var(--t-13);
  color: var(--text-1);
  white-space: pre-wrap;
}

.field {
  display: flex; flex-direction: column; gap: var(--s-1);
  margin-bottom: var(--s-4);
  min-width: 0;
}
.field__label {
  font-size: var(--t-12); font-weight: 560; color: var(--text-2);
}
.field__input, .field__textarea {
  width: 100%;
  padding: 10px var(--s-3);
  border-radius: var(--r-md);
  border: 1px solid var(--hairline-strong);
  background: var(--surface-1);
  color: var(--text-1);
  font: inherit; font-size: var(--t-15);
}
.field__textarea {
  min-height: 220px;
  font-family: var(--mono);
  font-size: var(--t-12);
  line-height: 1.5;
  resize: vertical;
}
.field__input::placeholder, .field__textarea::placeholder { color: var(--text-3); }
.field__input:focus, .field__textarea:focus {
  outline: none;
  border-color: var(--info);
  box-shadow: var(--focus);
}
.field__input[aria-invalid="true"], .field__textarea[aria-invalid="true"] {
  border-color: var(--fail);
  box-shadow: 0 0 0 3px var(--fail-line);
}
.field__error {
  font-size: var(--t-12); color: var(--fail);
  display: none;
}
.field__error[aria-hidden="false"] { display: block; }

.hint, .state {
  padding: var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--info-line);
  background: var(--tint-info);
  margin: 0 0 var(--s-4);
  font-size: var(--t-13); color: var(--text-2);
  line-height: 1.55;
}
.hint strong, .state strong { color: var(--text-1); }
.hint a, .state a { color: var(--info); text-decoration: underline; text-underline-offset: 2px; }

.state--empty {
  border-color: var(--hairline-strong);
  background: var(--surface-1);
  color: var(--text-2);
}
.state--ok {
  border-color: var(--pass-line);
  background: var(--tint-pass);
}
.state--error {
  border-color: var(--fail-line);
  background: var(--tint-fail);
  color: var(--text-2);
}
.state[hidden] { display: none; }

/* Canonical site footer. Landing defines it; every other surface mirrors it. */
.pagefoot {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s-7) var(--gutter) var(--s-8);
  border-top: 1px solid var(--hairline);
  color: var(--text-3); font-size: var(--t-12);
  min-width: 0;
}
.pagefoot a {
  color: var(--text-2); text-decoration: none;
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  min-width: var(--tap);
}

/* Inline links inside prose are 16px tall (one line box). Vertical padding on an inline box
   grows its hit area without moving a single line of text, so every prose link reaches
   32px (WCAG 2.2 SC 2.5.8 asks 24px; Apple HIG asks 44pt for primary controls). Classed
   links (buttons, nav, cards) size themselves. */
main a:not([class]) { padding-block: 0.5625rem; }
.pagefoot a:hover { color: var(--text-1); text-decoration: underline; text-underline-offset: 3px; }

.pagefoot__top {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: var(--s-5) var(--s-4);
  align-items: start;
}
.pagefoot__ident { display: grid; gap: var(--s-3); min-width: 0; }
.pagefoot__line {
  color: var(--text-3); font-size: var(--t-12); line-height: 1.55;
  max-width: 34ch;
}
.pagefoot__nav { display: contents; }
.pagefoot__group { min-width: 0; }
.pagefoot__heading {
  font-size: var(--t-11); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-3); font-weight: 560;
  margin: 0 0 var(--s-1);
}
.pagefoot__links {
  display: grid; gap: 0;
  list-style: none; margin: 0; padding: 0;
}
.pagefoot__bottom {
  margin-top: var(--s-6);
  padding-top: var(--s-4);
  border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--s-3);
  font-size: var(--t-11); color: var(--text-3);
}

@media (max-width: 860px) {
  .pagefoot__top { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pagefoot__ident { grid-column: 1 / -1; }
}

@media (max-width: 520px) {
  .pagefoot__top { grid-template-columns: minmax(0, 1fr); }
}

/* linear() spring (Open Props --ease-spring-2, MIT). Gated so engines without
   linear() keep the plain --ease curve defined in :root. */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --ease-spring: linear(
      0, 0.007, 0.029 2.2%, 0.118 4.7%, 0.625 14.4%, 0.826 19%, 0.902, 0.962,
      1.008 26.1%, 1.041 28.7%, 1.064 32.1%, 1.07 36%, 1.061 40.5%, 1.015 53.4%,
      0.999 61.6%, 0.995 71.2%, 1
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  html:focus-within { scroll-behavior: auto; }
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}
