/* ============================================================
   Pipepoint — site.css
   Dark-first, typography-led. Structure + styling (no animation yet).
   ============================================================ */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Display face. One weight, upright only — Instrument Serif ships no other
   weight, which is why it was picked over a variable serif: the pairing stays
   at three files total. High stroke contrast against Inter's even colour is
   what carries the headings; it is never used below ~1.5rem, where the thin
   strokes start to disappear on a dark background. */
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/instrumentserif-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('/assets/fonts/jetbrainsmono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---------- tokens ---------- */
:root {
  --bg:          #0A0A0A;
  --bg-raised:   #0E0E0E;
  --bg-inset:    #101010;

  /* Second surface tone. Alternated between sections for rhythm. The first
     pass used #121212 — 8 points off the base, which measured as a difference
     but did not read as one on a real screen. This is the working value; the
     raised/inset steps move with it so cards keep their depth order on alt
     sections instead of flattening into the background. */
  --bg-alt:        #1A1A1A;
  --bg-alt-raised: #1F1F1F;
  --bg-alt-inset:  #212121;   /* ceiling: --text-faint hits 4.55:1 here, AA's floor */

  /* Components read --surface-*, never --bg-* directly, so anything sitting on
     a section inherits that section's tone and keeps its depth order right.
     .section--alt reassigns these three; everything else follows. */
  --surface:        var(--bg);
  --surface-raised: var(--bg-raised);
  --surface-inset:  var(--bg-inset);
  --text:        #EAE8E4;
  --text-dim:    #A2A09A;
  --text-faint:  #8A8884; /* 5.3:1 on --bg — keeps small mono labels AA-compliant */
  --line:        rgba(234, 232, 228, 0.10);
  --line-strong: rgba(234, 232, 228, 0.18);
  --accent:      #3DE08F;
  --accent-dim:  rgba(61, 224, 143, 0.14);

  --sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Georgia first in the fallback: it is the only widely-installed serif whose
     stroke contrast is close enough that a swap does not restructure the page. */
  --serif: 'Instrument Serif', Georgia, 'Times New Roman', serif;

  --shell: 1180px;
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --rule: 1px solid var(--line);
  --radius: 4px;

  /* Motion. Fast and mechanical, not floaty — the brief for hover is that it
     answers immediately. */
  --ease-out: cubic-bezier(0.22, 0.68, 0.3, 1);
  --t-fast: 160ms;

  /* Arrow glyph, used as a mask so it takes currentColor. Inline data URI —
     no request, and it cannot 404 the way a sprite can. */
  --arrow-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 8h11M9 4l4 4-4 4' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  /* Grain. feTurbulence rendered once into a 160px tile and repeated. Kept at
     baseFrequency 0.8 — coarse enough to read as texture rather than as the
     compression noise a finer value looks like on an OLED panel. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.6;
  font-feature-settings: 'ss01' 1, 'cv05' 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ---------- texture ----------
   Two layers, both non-interactive and both very low contrast:
   a fixed grain over the whole page, and a faint grid behind the hero only.
   The grain is `position: fixed` so it does not scroll with content — moving
   grain reads as a rendering fault. pointer-events: none keeps it out of every
   hit test, and aria-hidden is implicit for a pseudo-element. */
/* Deliberately NOT mix-blend-mode. A fixed, full-viewport blended layer has to
   be re-composited against everything under it on every repaint, and the signal
   feed repaints continuously while it types — together that pegged the renderer
   hard enough to stall screenshots, which is a preview of what it would do to
   the Lighthouse performance score on a mid-range phone.
   Plain alpha over a near-black ground looks near-identical (overlay barely
   shifts anything against #0A0A0A) and costs one static composited layer.
   `contain` and the transform hint keep it on that layer and off the main
   thread's repaint path. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: var(--grain);
  background-size: 160px 160px;
  opacity: 0.03;
  contain: strict;
  transform: translateZ(0);
}

/* Everything real sits above the grain. The header is left alone — it is
   already sticky at z-index 50, and forcing position:relative here would drop
   it out of the sticky flow. */
main, .site-footer { position: relative; z-index: 2; }

h1, h2, h3, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; height: auto; }

a { color: inherit; text-decoration: none; }

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

::selection { background: var(--accent); color: #0A0A0A; }

/* ---------- utilities ---------- */
.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.skip-link {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--text);
  color: var(--bg);
  font-size: 0.875rem;
  font-weight: 500;
  border-radius: var(--radius);
  transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }

/* ---------- type scale ----------
   Two families doing two jobs: the serif sets every heading, Inter sets every
   piece of running text. The size jump between them is the point — the hero now
   opens at ~5.5x body size against the old ~3.5x, which is what makes the page
   read as art-directed rather than as one ramp of sizes.

   Instrument Serif needs almost none of the negative tracking Inter wanted; its
   fitting is already tight, and pulling it further collapses the thin joins. */
/* .h3 is deliberately NOT in this list. Card and list titles sit at ~17px,
   below the size where this face's thin strokes survive on a dark background —
   they stay Inter at 600, which is also what keeps the card interiors readable
   as a block of UI rather than as more editorial matter. */
.display,
.h2,
.legal__title,
.notfound__title,
.contact__title {
  font-family: var(--serif);
  font-weight: 400;
  font-feature-settings: normal;
}

/* 76px ceiling, not the ~100px an 8vw ramp reaches at 1440. The hero copy sits
   in the narrower of two columns — around 540px — and at 100px this headline
   broke to four characters a line and towered over the feed beside it. 76px
   against 16px body is a 4.75x ratio, up from the 3.5x it replaced, which is
   the contrast the brief asked for without the column fighting it. */
.display {
  font-size: clamp(2.75rem, 6vw, 4.75rem);
  line-height: 0.98;
  letter-spacing: -0.016em;
  text-wrap: balance;
  max-width: 13ch;
}
@media (max-width: 939px) {
  .display { max-width: 16ch; }
}

.h2 {
  font-size: clamp(2.25rem, 4.6vw, 3.5rem);
  line-height: 1.0;
  letter-spacing: -0.014em;
  text-wrap: balance;
}

.h3 {
  font-size: 1.0625rem;
  line-height: 1.3;
  letter-spacing: -0.012em;
  font-weight: 600;
}

.lede {
  margin-top: 1.5rem;
  max-width: 34em;
  font-size: clamp(1rem, 1.6vw, 1.125rem);
  line-height: 1.55;
  color: var(--text-dim);
  text-wrap: pretty;
}

.fig,
.eyebrow,
.card__index,
.feed__tag,
.feed__status,
.feed__source,
.section__kicker,
.sample__cap,
.chips li {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  font-weight: 400;
}

.fig { color: var(--text-faint); }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--text-dim);
  margin-bottom: 2rem;
}
.eyebrow__dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-dim);
}

/* ---------- buttons / links ---------- */
/* The arrow is a background-image on an empty pseudo-element rather than a
   glyph in `content`. Pseudo-element text IS announced by VoiceOver and
   NVDA — an arrow character would have every CTA read as "Get in touch right
   arrow". content:'' is silent, and the SVG is inline so it costs no request. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.8rem 1.4rem;
  background: var(--text);
  color: #0A0A0A;
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  border-radius: var(--radius);
  transform: translateZ(0);
  transition: background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}

.btn::after {
  content: '';
  width: 0.75em;
  height: 0.75em;
  flex: none;
  background-color: currentColor;
  /* Mask, not a background-image: the arrow inherits the button's text colour,
     so it flips with the label on hover instead of needing a second asset. */
  -webkit-mask: var(--arrow-mask) center / contain no-repeat;
          mask: var(--arrow-mask) center / contain no-repeat;
  transform: translateX(0);
  transition: transform var(--t-fast) var(--ease-out);
}

.btn:hover {
  background: var(--accent);
  transform: scale(1.025);
  box-shadow: 0 10px 24px -14px rgba(61, 224, 143, 0.75);
}
.btn:hover::after { transform: translateX(4px); }
.btn:active { transform: scale(0.99); transition-duration: 80ms; }

.btn--sm { padding: 0.5rem 0.9rem; font-size: 0.875rem; }

.link-quiet {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9375rem;
  color: var(--text-dim);
  border-bottom: 1px solid var(--line-strong);
  padding-bottom: 2px;
  transition: color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}
.link-quiet::after {
  content: '';
  width: 0.7em;
  height: 0.7em;
  flex: none;
  background-color: currentColor;
  -webkit-mask: var(--arrow-mask) center / contain no-repeat;
          mask: var(--arrow-mask) center / contain no-repeat;
  transform: translateX(0);
  transition: transform var(--t-fast) var(--ease-out);
}
.link-quiet:hover { color: var(--text); border-color: var(--accent); }
.link-quiet:hover::after { transform: translateX(4px); }

/* ============================================================
   HEADER
   ============================================================ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(10, 10, 10, 0.82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: var(--rule);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: 2rem;
  height: 64px;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  margin-right: auto;
  transition: opacity 140ms ease;
}
.wordmark:hover { opacity: 0.78; }

/* Sized by height, so a re-export at any resolution drops straight in. */
.wordmark img {
  height: 24px;
  width: auto;
}

.site-nav__list {
  display: flex;
  gap: 1.75rem;
  font-size: 0.9375rem;
}
.site-nav a {
  color: var(--text-dim);
  transition: color 140ms ease;
}
.site-nav a:hover { color: var(--text); }

@media (max-width: 720px) {
  .site-nav { display: none; }
}

/* ============================================================
   HERO
   ============================================================ */
.hero {
  position: relative;
  padding-top: clamp(3.5rem, 7vw, 6.5rem);
  padding-bottom: clamp(5rem, 9vw, 8rem);
  border-bottom: var(--rule);
  /* Kept after the bleed was removed: the hero's faint grid is inset:0 on a
     positioned box and the globe's atmosphere halo extends past its own edge,
     so this still guarantees neither can widen the page. `clip` not `hidden` —
     hidden makes this a scroll container and breaks the sticky header. */
  overflow-x: clip;
}

/* Faint grid, hero only. 80px cells at 1px — visible as structure when you
   look for it, invisible when you do not. Fades out down the section so it
   never competes with the copy at the fold. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line) 1px, transparent 1px);
  background-size: 80px 80px;
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 78%);
          mask-image: linear-gradient(to bottom, #000 0%, transparent 78%);
}

.hero__grid {
  position: relative;
  display: grid;
  gap: clamp(3rem, 6vw, 4.5rem);
  align-items: start;
}

/* Asymmetric on purpose. The copy column is the narrower of the two and starts
   inset from the shell edge; the feed is wider and runs off the right. Nothing
   on this row is centred and the two columns do not match — that mismatch is
   what stops the fold reading as a template's 50/50 hero. */
@media (min-width: 940px) {
  .hero__grid {
    /* Copy column is fractionally the wider of the two. This ratio was set
       when the right-hand cell held the feed and bled off the edge; it now
       holds the globe, which sits inside the shell. The gentle mismatch is
       what keeps the fold off a template 50/50 — an aggressive ratio was
       tried and starved the headline. */
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    gap: clamp(2.5rem, 4vw, 4rem);
    align-items: center;
  }

  .hero__copy {
    padding-top: clamp(1rem, 3vw, 3rem);
    /* Optical indent. The serif's left sidebearing already pulls the headline
       slightly left of the rule above it, so this is measured from the text
       edge, not the box. */
    padding-left: clamp(0rem, 1.5vw, 1.5rem);
  }

  /* The bleed that used to live here is gone.
     It was a hero composition device: the panel ran off the right edge to
     balance the offset headline beside it, and worked because the two were
     read as one arrangement. When the feed moved out of the hero into its own
     section that reasoning left with it — the rule targeted `.feed` globally,
     followed the panel down the page, and in a plain section a panel running
     off the edge reads as clipped rather than placed. The hero now holds the
     globe, which sits inside the shell, so nothing is left for it to do. */
}

.hero__actions {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  margin-top: 2.5rem;
  flex-wrap: wrap;
}

/* ---------- signal feed ---------- */
/* The feed is the one thing in the hero that is actually doing something, so
   it gets an accent edge and a low, wide cast to lift it off the page. The
   glow is carried on the border and an outer ring — no blurred halo, which at
   this size reads as a rendering artefact rather than a highlight. */
.feed {
  position: relative;
  background: var(--surface-raised);
  border: 1px solid rgba(61, 224, 143, 0.28);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(61, 224, 143, 0.08),
              0 24px 70px -28px rgba(61, 224, 143, 0.38),
              0 30px 80px -34px rgba(0, 0, 0, 0.95);
}

/* A single bright line along the top edge, brightest at the left where the eye
   lands coming off the headline. This is the accent treatment that marks the
   panel as the focal point — it sits on the edge rather than behind the panel,
   because a blurred halo at this size reads as a rendering artefact. */
.feed::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(to right,
    rgba(61, 224, 143, 0.9) 0%,
    rgba(61, 224, 143, 0.35) 38%,
    rgba(61, 224, 143, 0) 88%);
}

.feed__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1.15rem;
  border-bottom: var(--rule);
  background: rgba(234, 232, 228, 0.015);
}

.feed__status {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--accent);
}

.feed__body { padding: 1.15rem 1.15rem 0.35rem; }

.feed__source {
  color: var(--text-faint);
  padding-bottom: 1rem;
  border-bottom: var(--rule);
  letter-spacing: 0.08em;
}

.feed__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'tag   score'
    'firm  score'
    'event event';
  column-gap: 1rem;
  padding: 1.05rem 0;
  border-bottom: var(--rule);
}
.feed__list > .feed__row:last-child { border-bottom: 0; }

.feed__tag {
  grid-area: tag;
  color: var(--accent);
  font-size: 0.625rem;
  letter-spacing: 0.13em;
}

.feed__firm {
  grid-area: firm;
  margin-top: 0.4rem;
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.012em;
}

.feed__event {
  grid-area: event;
  margin-top: 0.2rem;
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--text-dim);
}

.feed__score {
  grid-area: score;
  align-self: center;
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Live aggregate counts. Sits at the foot of the panel, directly above the
   disclaimer, so "these are real" and "those are examples" read together. */
.feed__counts {
  padding: 0.9rem 1.15rem 1rem;
  border-top: var(--rule);
}
/* The counts strip holds its own height from the first paint.

   It is filled from a fetch, so anything that establishes its height later —
   including JS adding a reservation class at DOMContentLoaded — is itself
   layout shift on a panel above the fold. Reserving in CSS, gated on the `.js`
   class that the inline script in <head> sets before the body paints, is the
   only version that costs nothing: the box is the right size before the first
   pixel lands.

   No JS: the strip is never filled, so it is never shown.
   Fetch fails or the data is stale: bail() sets [hidden] and it collapses. */
.feed__counts { display: none; }
.js .feed__counts {
  display: block;
  min-height: 8.5rem;   /* measured render: 137px at desktop panel width */
}
/* Last, so it beats the .js rule above at equal specificity. */
.feed__counts[hidden] { display: none; }

.feed__counts-label {
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--accent);
}

.feed__counts-list {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-dim);
}
.feed__counts-n {
  color: var(--text);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.feed__counts-sep { color: var(--text-faint); margin-inline: 0.45rem; }

.feed__counts-updated {
  margin-top: 0.5rem;
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The UK/EU split. Sits directly under the headline number because it is the
   thing that stops that number being misread — styled as data, not as a
   footnote, so it carries the same weight as the figure it qualifies. */
.feed__counts-split {
  margin-top: 0.4rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* Source credit for the live counts. Quiet, but never hidden — the EBA's terms
   make acknowledgement the condition of reproducing their material, so this
   cannot be styled down to invisibility. text-faint holds 5.46:1 here. */
.feed__counts-credit {
  margin-top: 0.5rem;
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.06em;
  line-height: 1.5;
  color: var(--text-faint);
  text-wrap: pretty;
}

.feed__note {
  padding: 0.9rem 1.15rem;
  border-top: var(--rule);
  font-size: 0.75rem;
  color: var(--text-faint);
  background: rgba(234, 232, 228, 0.015);
}

/* ============================================================
   COVERAGE GLOBE
   ============================================================ */
.globe {
  position: relative;
  background: var(--surface-raised);
  border: 1px solid rgba(61, 224, 143, 0.24);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(61, 224, 143, 0.06),
              0 30px 80px -34px rgba(61, 224, 143, 0.3),
              0 34px 90px -40px rgba(0, 0, 0, 0.95);
}
.globe::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(to right,
    rgba(61, 224, 143, 0.85) 0%, rgba(61, 224, 143, 0.3) 38%, rgba(61, 224, 143, 0) 88%);
}

.globe__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1.15rem;
  border-bottom: var(--rule);
  background: rgba(234, 232, 228, 0.015);
}
.globe__meta {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--accent);
  white-space: nowrap;
}

/* Fixed aspect so the canvas has its size before script runs — a canvas that
   gets its height from JS is a guaranteed layout shift on the largest element
   above the fold. */
.globe__stage {
  position: relative;
  aspect-ratio: 1 / 1;
  max-height: 26rem;
}
#globe-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Hover readout. Reserves its own line so appearing does not move the caption. */
.globe__readout {
  position: absolute;
  left: 0; right: 0; bottom: 0.75rem;
  min-height: 1.2em;
  padding-inline: 1.15rem;
  text-align: center;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  color: var(--accent);
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease-out);
  pointer-events: none;
}
.globe__readout.is-on { opacity: 1; }

.globe__cap {
  padding: 0.9rem 1.15rem;
  border-top: var(--rule);
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--text-dim);
  text-wrap: pretty;
}
.globe__figure { color: var(--text); font-weight: 500; }
.globe__figure span { font-variant-numeric: tabular-nums; }

/* The jurisdiction list is the accessible content behind the canvas. It stays
   in the DOM always; JS hides it visually once the globe is drawing, and shows
   it again if the globe cannot render. */
.globe__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.35rem 1.25rem;
  padding: 0.9rem 1.15rem 1.1rem;
  border-top: var(--rule);
  font-size: 0.8125rem;
  color: var(--text-dim);
}
.globe__list li { display: flex; align-items: baseline; gap: 0.5rem; }
.globe__code {
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  color: var(--text-faint);
}
.globe__n { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text); }

/* Taken out of flow immediately, not after the fetch resolves: rendering 31
   rows and then collapsing them is a third of a CLS point on the largest
   element above the fold. Still in the accessibility tree — the canvas is
   aria-hidden, so this list is what a screen reader actually reads. */
.js .globe__list {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* Globe could not draw — the list becomes the figure again. */
.js .globe.is-fallback .globe__stage { display: none; }
.js .globe.is-fallback .globe__list {
  position: static;
  width: auto; height: auto;
  margin: 0; padding: 0.9rem 1.15rem 1.1rem;
  overflow: visible;
  clip-path: none;
  white-space: normal;
  border-top: var(--rule);
}

.globe__credit {
  padding: 0.8rem 1.15rem;
  border-top: var(--rule);
  background: rgba(234, 232, 228, 0.015);
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.06em;
  line-height: 1.5;
  color: var(--text-faint);
  text-wrap: pretty;
}

/* ============================================================
   SIGNAL FEED SECTION
   ============================================================ */
/* Deliberately NOT --bg-alt. The feed panel's small print uses --text-faint,
   which measures 4.49:1 against the raised alt surface — just under AA. The
   panel's palette was designed against the base surface; moving it to the
   lighter one silently broke the contrast floor. */
.feed-section__grid { display: grid; gap: clamp(2.5rem, 5vw, 4rem); align-items: start; }
@media (min-width: 940px) {
  .feed-section__grid { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); }
}
.feed-section__copy .fig { margin-bottom: 1.25rem; }

/* ============================================================
   SEQUENCE
   ============================================================ */
.seq {
  display: grid;
  gap: 1rem;
  counter-reset: seq;
}
@media (min-width: 900px) {
  .seq { grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
}

.seq__step {
  display: flex;
  flex-direction: column;
  padding: clamp(1.4rem, 2.6vw, 1.9rem);
  background: var(--surface-raised);
  border: var(--rule);
  border-radius: 6px;
}
.seq__index {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  color: var(--accent);
}
.seq__label {
  margin-top: 1.1rem;
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.012em;
  color: var(--text);
}

.seq__body {
  margin-top: 1.1rem;
  padding: 1rem;
  border: var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--text-dim);
  text-wrap: pretty;
}
.seq__body--raw { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.02em; }
.seq__firm { color: var(--text); font-weight: 500; }
.seq__fields { margin: 0.75rem 0 0; display: grid; gap: 0.4rem; }
.seq__fields > div { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); gap: 0.5rem; }
.seq__fields dt { color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.625rem; }
.seq__fields dd { margin: 0; color: var(--text-dim); }
.seq__src {
  margin-top: 0.85rem; padding-top: 0.7rem; border-top: var(--rule);
  font-size: 0.625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-faint);
}
.seq__body--mail { border-color: rgba(61, 224, 143, 0.28); }
.seq__subject { color: var(--text); font-weight: 500; margin-bottom: 0.6rem; }
.seq__body em { color: var(--text); font-style: italic; }

.seq__note {
  margin-top: auto;
  padding-top: 1.1rem;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--text-faint);
  text-wrap: pretty;
}

/* Staged entrance. Applied only when JS is running AND motion is allowed —
   the base state above is fully visible, so no-JS and reduced-motion readers
   get all three stages with nothing withheld. */
.js .seq.is-staged .seq__step {
  opacity: 0;
  transform: translateY(20px);
}
.js .seq.is-staged .seq__step.is-on {
  opacity: 1;
  transform: none;
  transition: opacity 460ms ease, transform 460ms var(--ease-out);
}
.js .seq.is-staged .seq__step:nth-child(2).is-on { transition-delay: 70ms; }
.js .seq.is-staged .seq__step:nth-child(3).is-on { transition-delay: 140ms; }

/* ============================================================
   SECTIONS
   ============================================================ */
/* More air between sections, less inside them — the contrast between the two is
   what groups related content without needing a box around it. */
.section {
  padding-block: clamp(5.5rem, 11vw, 9.5rem);
  border-bottom: var(--rule);
  background: var(--surface);
}

/* The lighter surface. Applied to alternate sections in the markup rather than
   with :nth-child, so the rhythm survives a section being added or reordered. */
.section--alt {
  --surface:        var(--bg-alt);
  --surface-raised: var(--bg-alt-raised);
  --surface-inset:  var(--bg-alt-inset);
}

.section__grid {
  display: grid;
  gap: clamp(2rem, 4vw, 3rem);
}

@media (min-width: 880px) {
  .section__grid {
    grid-template-columns: 200px minmax(0, 1fr);
    gap: clamp(3rem, 6vw, 6rem);
  }
}

.section__label {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.section__kicker { color: var(--text); }

.section__head {
  max-width: 42em;
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
}
.section__head .fig { margin-bottom: 1.25rem; }

.section__intro {
  margin-top: 1.25rem;
  max-width: 40em;
  color: var(--text-dim);
  font-size: 1.0625rem;
}

.section__body { max-width: 46em; }

.prose p {
  margin-top: 1.5rem;
  color: var(--text-dim);
  font-size: clamp(1rem, 1.5vw, 1.0625rem);
  line-height: 1.65;
  text-wrap: pretty;
}
.prose__pull {
  color: var(--text) !important;
  border-left: 2px solid var(--accent);
  padding-left: 1.25rem;
  margin-top: 2rem !important;
}

/* ---------- bento ---------- */
.bento {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(12, 1fr);
}

.card {
  grid-column: span 12;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  background: var(--surface-raised);
  border: var(--rule);
  border-radius: 6px;
  transition: border-color 160ms ease, background 160ms ease,
              transform 180ms cubic-bezier(0.22, 0.68, 0.3, 1),
              box-shadow 180ms ease;
}
.card:hover {
  border-color: var(--line-strong);
  background: var(--surface-inset);
}

/* Hover lift. The card has to look picked up, so travel, shadow and edge all
   move together — a 4px nudge with a shadow alone was too quiet to notice.
   The accent sits on the border and in a low-opacity ring rather than as a
   bloom, so it reads as an edge catching light, not a glowing box. */
.js .card:hover {
  transform: translateY(-10px);
  border-color: rgba(61, 224, 143, 0.45);
  box-shadow: 0 22px 44px -18px rgba(0, 0, 0, 0.95),
              0 0 0 1px rgba(61, 224, 143, 0.16);
}

.card__index {
  color: var(--accent);
  margin-bottom: 1.75rem;
}

.card p:not(.card__index) {
  margin-top: 0.85rem;
  color: var(--text-dim);
  font-size: 0.9375rem;
  line-height: 1.6;
  max-width: 44ch;
}

/* A separate offer, not part of the step. Quieter, and ruled off from it. */
.card p.card__aside {
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: var(--rule);
  color: var(--text-faint);
  font-size: 0.875rem;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 1.75rem;
}
.chips li {
  padding: 0.3rem 0.6rem;
  border: var(--rule);
  border-radius: 999px;
  color: var(--text-faint);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
}

/* Cell rhythm. The three cards deliberately do not divide evenly: 8/4 on the
   first row, then a 12 split 5/7 on the second. Consecutive rows never break at
   the same x, so the grid has a beat instead of a repeat. Card B is the narrow
   one and gets extra top padding to offset its shorter copy — the two cards on
   row one finish at different heights on purpose. */
@media (min-width: 880px) {
  .card--a { grid-column: span 8; }
  .card--b {
    grid-column: span 4;
    padding-top: clamp(2rem, 4vw, 3.25rem);
  }
  .card--c {
    grid-column: span 12;
    display: grid;
    grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
    gap: clamp(2rem, 4vw, 3.5rem);
    align-items: start;
  }
}

.sample {
  border: var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.sample__cap {
  padding: 0.65rem 1rem;
  border-bottom: var(--rule);
  color: var(--text-faint);
}
.sample__body { padding: 1.15rem 1rem 1.35rem; }
.card .sample__body p {
  margin-top: 0.85rem;
  color: var(--text-dim);
  font-size: 0.875rem;
  line-height: 1.6;
}
.sample__subject {
  margin-top: 0 !important;
  color: var(--text) !important;
  font-weight: 500;
}

/* ---------- product imagery: the list view ----------
   Real markup, not a screenshot. Stays sharp at any pixel density, adds no
   image weight, and the copy inside it is selectable and translatable. */
.dash {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  background: var(--surface-raised);
  border: var(--rule);
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 30px 70px -40px rgba(0, 0, 0, 0.95);
}

.dash__chrome {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.8rem 1.15rem;
  border-bottom: var(--rule);
  background: rgba(234, 232, 228, 0.02);
}

.dash__app,
.dash__meta {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}
.dash__app { color: var(--text-dim); }
.dash__meta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text-faint);
  white-space: nowrap;
}

/* Six columns will not fit a 390px screen at a readable size, so the table
   scrolls inside its own box rather than the page. tabindex makes that scroll
   container reachable by keyboard, which is required once it can scroll. */
.dash__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.dash__scroll:focus-visible { outline-offset: -2px; }

.dash__table {
  width: 100%;
  min-width: 52rem;
  border-collapse: collapse;
  font-size: 0.8125rem;
  text-align: left;
}

.dash__table thead th {
  padding: 0.7rem 1rem;
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 400;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-faint);
  border-bottom: var(--rule);
  white-space: nowrap;
}

.dash__table tbody th,
.dash__table tbody td {
  padding: 0.85rem 1rem;
  border-bottom: var(--rule);
  vertical-align: middle;
  color: var(--text-dim);
  font-weight: 400;
}
.dash__table tbody tr:last-child th,
.dash__table tbody tr:last-child td { border-bottom: 0; }

/* Firm name is the row header — it is the thing every other cell describes. */
.dash__table tbody th {
  color: var(--text);
  font-weight: 500;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.dash__table tbody tr { transition: background var(--t-fast) var(--ease-out); }
.dash__table tbody tr:hover { background: rgba(234, 232, 228, 0.028); }

.dash__num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  width: 1%;
  white-space: nowrap;
}
.dash__table td.dash__num { color: var(--text-faint); }

/* Score reads as a measurement, so it gets a box and a tabular figure. The
   accent variant marks the rows above the action threshold. */
.dash__score {
  display: inline-block;
  min-width: 2.4em;
  padding: 0.2rem 0.4rem;
  text-align: center;
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--text-dim);
  font-size: 0.75rem;
}
.dash__score--hot {
  border-color: rgba(61, 224, 143, 0.4);
  background: rgba(61, 224, 143, 0.09);
  color: var(--accent);
}

.dash__why {
  min-width: 20rem;
  line-height: 1.45;
  text-wrap: pretty;
}

.dash__pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}
.dash__pills li {
  padding: 0.2rem 0.5rem;
  border: var(--rule);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}
/* Event-type colour coding, matched to the feed's tag colours. Contrast is
   carried by the text, never by the fill alone. */
.dash__pills li.is-new      { color: var(--accent); border-color: rgba(61, 224, 143, 0.35); }
.dash__pills li.is-restrict { color: #F0A868; border-color: rgba(240, 168, 104, 0.35); }
.dash__pills li.is-var      { color: #8FB8F0; border-color: rgba(143, 184, 240, 0.35); }

.dash__cap {
  padding: 0.85rem 1.15rem;
  border-top: var(--rule);
  background: rgba(234, 232, 228, 0.015);
  font-size: 0.75rem;
  color: var(--text-faint);
  text-wrap: pretty;
}

/* ---------- the two tiers ----------
   Two columns that are deliberately NOT identical: the upgrade carries an
   accent edge and one extra block. Equal-weight columns would read as a
   pricing table with the price missing, which invites the reader to look for
   the catch instead of the difference. */
.tiers {
  display: grid;
  gap: 1rem;
  align-items: start;
}
@media (min-width: 860px) {
  .tiers { grid-template-columns: 1fr 1fr; }
}

.tier {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  background: var(--surface-raised);
  border: var(--rule);
  border-radius: 6px;
}

/* The upgrade. Accent sits on the top edge only — a fully accented card would
   outrank the entry product, and the entry product is the one most readers
   should buy first. */
.tier--up {
  border-color: rgba(61, 224, 143, 0.3);
  box-shadow: inset 0 1px 0 0 rgba(61, 224, 143, 0.5);
}

.tier__name {
  font-family: var(--serif);
  font-size: clamp(1.5rem, 2.6vw, 1.875rem);
  line-height: 1.1;
  letter-spacing: -0.012em;
}
.tier--up .tier__name { color: var(--accent); }

.tier__line {
  margin-top: 0.5rem;
  padding-bottom: 1.25rem;
  border-bottom: var(--rule);
  color: var(--text-dim);
  font-size: 0.9375rem;
}

.tier__list {
  margin-top: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--text-dim);
}
.tier__list li {
  position: relative;
  padding-left: 1.4rem;
}
/* Tick drawn in CSS rather than a glyph — content:'' is silent to screen
   readers, and the list semantics already carry the meaning. */
.tier__list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 0.5rem;
  height: 0.28rem;
  border-left: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  transform: rotate(-45deg);
}
.tier__list strong { color: var(--text); font-weight: 500; }

.tier__who {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: var(--rule);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text-dim);
  text-wrap: pretty;
}
.tier__who strong { color: var(--text); font-weight: 500; }

/* Lead time. Sits last and quietest — it is a caveat, not a feature, and
   burying it would be the kind of thing that costs trust on the first call. */
.tier__note {
  margin-top: auto;
  padding-top: 1.25rem;
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--text-faint);
  text-wrap: pretty;
}
.tier__note strong { color: var(--text-dim); font-weight: 500; }

/* ---------- who it's for ---------- */
.fit-list {
  display: grid;
  gap: 1px;
  margin-top: 2.75rem;
  background: var(--line);
  border: var(--rule);
  border-radius: 6px;
  overflow: hidden;
}
@media (min-width: 620px) {
  .fit-list { grid-template-columns: 1fr 1fr; }
}

.fit {
  padding: 1.5rem;
  background: var(--surface);
  transition: background 160ms ease;
}
.fit:hover { background: var(--surface-inset); }
.fit p {
  margin-top: 0.5rem;
  color: var(--text-dim);
  font-size: 0.9375rem;
}

.notfit {
  margin-top: 2.5rem;
  padding-left: 1.25rem;
  border-left: var(--rule);
  border-left-width: 2px;
}
.notfit__title {
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--text-faint);
}
.notfit p {
  margin-top: 0.6rem;
  max-width: 52ch;
  color: var(--text-dim);
  font-size: 0.9375rem;
}

/* ---------- contact ---------- */
.section--contact { border-bottom: 0; }

.contact { max-width: 46em; }
.contact__title { margin-top: 1.25rem; }
.contact__body {
  margin-top: 1.5rem;
  max-width: 38em;
  color: var(--text-dim);
  font-size: 1.0625rem;
}

.contact__email {
  display: inline-block;
  margin-top: 2.5rem;
  font-size: clamp(1.5rem, 4vw, 2.25rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  border-bottom: 1px solid var(--line-strong);
  padding-bottom: 0.15em;
  transition: color 160ms ease, border-color 160ms ease;
}
.contact__email:hover { color: var(--accent); border-color: var(--accent); }

.contact__sign {
  margin-top: 2rem;
  color: var(--text-faint);
  font-size: 0.9375rem;
}

/* ---------- lead form ---------- */
.lead {
  margin-top: 2.5rem;
  padding-top: 2.5rem;
  border-top: var(--rule);
}

.lead__grid {
  display: grid;
  gap: 1.25rem 1.5rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lead__field { margin: 0; min-width: 0; }
.lead__field--wide { grid-column: 1 / -1; }

/* One column below the point where two selects stop fitting a label each. */
@media (max-width: 34rem) {
  .lead__grid { grid-template-columns: minmax(0, 1fr); }
}

.lead__label {
  display: block;
  margin-bottom: 0.4rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.lead__req { color: var(--accent); }
.lead__opt {
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.7;
}

.lead__input {
  display: block;
  width: 100%;
  padding: 0.7rem 0.85rem;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--surface-inset);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
  /* iOS zooms the page on focus for anything under 16px, which on submit leaves
     the visitor scrolled somewhere they did not ask to be. 1rem keeps it still. */
}
.lead__input:hover { border-color: var(--line-strong); }
.lead__input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
.lead__input--area { resize: vertical; min-height: 5.5rem; }

/* Native select arrow is drawn in the OS light palette and disappears against
   the inset surface, so it is replaced with the site's own glyph. */
select.lead__input {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.25rem;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-faint) 50%),
                    linear-gradient(135deg, var(--text-faint) 50%, transparent 50%);
  background-position: calc(100% - 1.1rem) 55%, calc(100% - 0.8rem) 55%;
  background-size: 0.3rem 0.3rem, 0.3rem 0.3rem;
  background-repeat: no-repeat;
  cursor: pointer;
}
/* The options themselves render in the OS palette, not the page's. */
select.lead__input option { background: var(--bg-raised); color: var(--text); }

.lead__input[aria-invalid='true'] { border-color: #E0603D; }
.lead__err {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.8125rem;
  color: #E0603D;
}

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

.lead__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.5rem;
  margin-top: 1.75rem;
}
.lead__submit { cursor: pointer; }
.lead__submit[disabled] { opacity: 0.55; cursor: progress; }

.lead__note {
  flex: 1 1 18em;
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--text-faint);
}
.lead__note strong { color: var(--text-dim); font-weight: 500; }
.lead__note a { border-bottom: 1px solid var(--line-strong); }
.lead__note a:hover { color: var(--accent); border-color: var(--accent); }

.lead__status {
  margin-top: 1.25rem;
  padding: 0.85rem 1rem;
  font-size: 0.9375rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-inset);
}
.lead__status--ok { border-color: var(--accent); color: var(--text); }
.lead__status--bad { border-color: #E0603D; color: var(--text); }

.contact__body--alt { margin-top: 3rem; }

/* ============================================================
   LEGAL / LONG-FORM PAGE
   ============================================================ */
.legal {
  padding-top: clamp(3.5rem, 8vw, 6rem);
  padding-bottom: clamp(4rem, 9vw, 7rem);
}

.legal .shell { max-width: 780px; }

.legal__head {
  padding-bottom: 2.5rem;
  border-bottom: var(--rule);
}
.legal__title { margin-top: 1.25rem; }

.legal__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem;
  margin: 2rem 0 0;
}
.legal__meta dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.legal__meta dd {
  margin: 0.5rem 0 0;
  font-size: 0.9375rem;
  color: var(--text-dim);
}
.legal__meta a { border-bottom: 1px solid var(--line-strong); }
.legal__meta a:hover { color: var(--text); }

.legal__body {
  padding-top: 1rem;
  font-size: 1rem;
  line-height: 1.7;
  color: var(--text-dim);
  text-wrap: pretty;
}

.legal__body h2 {
  margin-top: 3rem;
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
}
.legal__body h3 {
  margin-top: 2rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
}
.legal__body p,
.legal__body ul,
.legal__body ol { margin-top: 1rem; }

.legal__body ul,
.legal__body ol {
  padding-left: 1.25rem;
  list-style: disc;
}
.legal__body ol { list-style: decimal; }
.legal__body li { margin-top: 0.5rem; }
.legal__body li::marker { color: var(--text-faint); }

.legal__body a {
  color: var(--text);
  border-bottom: 1px solid var(--line-strong);
}
.legal__body a:hover { border-color: var(--accent); }

.legal__body strong {
  color: var(--text);
  font-weight: 500;
}

/* Wide tables scroll inside their own box rather than the page. */
.legal__table {
  margin-top: 1.5rem;
  overflow-x: auto;
}
.legal__body table {
  width: 100%;
  min-width: 30rem;
  border-collapse: collapse;
  font-size: 0.9375rem;
}
.legal__body th,
.legal__body td {
  padding: 0.75rem 1.5rem 0.75rem 0;
  text-align: left;
  border-bottom: var(--rule);
  vertical-align: top;
}
.legal__body thead th {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-faint);
  font-weight: 400;
  padding-bottom: 0.6rem;
}
.legal__body tbody th {
  color: var(--text);
  font-weight: 500;
  width: 38%;
}

.legal__contacts {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 3rem;
  margin: 1.75rem 0 0;
  padding: 1.25rem 1.5rem;
  border: var(--rule);
  border-radius: var(--radius);
  background: var(--surface-raised);
}
.legal__contacts dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.legal__contacts dd { margin: 0.5rem 0 0; }

.legal__address {
  margin-top: 1rem;
  font-style: normal;
  line-height: 1.9;
}

.legal__back {
  margin-top: 3.5rem;
  padding-top: 2.5rem;
  border-top: var(--rule);
}

/* ============================================================
   404
   ============================================================ */
.notfound {
  display: flex;
  align-items: center;
  min-height: min(70vh, 34rem);
  padding-block: clamp(4rem, 10vw, 7rem);
}
.notfound .shell { max-width: 46em; }
.notfound__title { margin-top: 1.25rem; }
.notfound__body {
  margin-top: 1.5rem;
  max-width: 38em;
  color: var(--text-dim);
  font-size: 1.0625rem;
}
.notfound__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.75rem;
  margin-top: 2.5rem;
}

/* ============================================================
   FOOTER
   ============================================================ */
.site-footer {
  border-top: var(--rule);
  padding-block: 2.5rem;
}
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 2rem;
  font-size: 0.875rem;
  color: var(--text-dim);
}
.site-footer__name {
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.02em;
  margin-right: auto;
}
/* Must wrap. This row held three links until /terms was added as a fourth, and
   four items at a 1.5rem gap measure 435px — wider than a 390px screen, which
   put a horizontal scrollbar on every page. The gap tightens before the wrap
   happens so the common case still fits on one line. */
.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  min-width: 0;
}
@media (max-width: 460px) {
  .site-footer__links { gap: 0.7rem 1rem; }
}
.site-footer__links a {
  border-bottom: 1px solid transparent;
  transition: color 140ms ease, border-color 140ms ease;
}
.site-footer__links a:hover { color: var(--text); border-color: var(--line-strong); }
.site-footer__legal { color: var(--text-faint); }

@media (max-width: 620px) {
  .site-footer__name { width: 100%; margin-right: 0; }
}

/* ============================================================
   MOTION
   ============================================================ */

/* ---------- section heading underline ----------
   Draws in when its section reveals. Defined visible by default so the no-JS
   page still gets the rule; .js hides it and the reveal draws it. */
.section .h2 { position: relative; }
.section .h2::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -0.55rem;
  width: 2.75rem;
  height: 2px;
  background: var(--accent);
  transform: scaleX(1);
  transform-origin: left center;
}
.js .section .h2::after {
  transform: scaleX(0);
  transition: transform 460ms cubic-bezier(0.22, 0.68, 0.3, 1) 140ms;
}
.js .reveal.is-in .h2::after { transform: scaleX(1); }

/* ---------- parallax ----------
   site.js writes a pixel offset here. Kept off .reveal elements, whose own
   transform would fight it — see initParallax. */
[data-parallax] { will-change: transform; }

/* ---------- scroll reveals ---------- */
/* Hidden state only applies when JS is running, so no-JS keeps everything
   visible and nothing above the fold is ever withheld. */
.js .reveal {
  opacity: 0;
  transform: translateY(22px);
}
.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 420ms ease, transform 420ms cubic-bezier(0.22, 0.68, 0.3, 1);
}

/* ---------- signal feed ---------- */
.feed.is-live .feed__row {
  overflow: hidden;
  opacity: 0;
  transform: translateY(6px);
}
.feed.is-live .feed__row.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 380ms ease, transform 380ms cubic-bezier(0.22, 0.68, 0.3, 1);
}
.feed.is-live .feed__row.is-leaving {
  opacity: 0;
  padding-block: 0;
  transition: height 320ms cubic-bezier(0.4, 0, 0.2, 1),
              padding 320ms cubic-bezier(0.4, 0, 0.2, 1),
              opacity 200ms ease;
}

/* Caret sits on whichever line is currently being typed. */
.feed__row.is-typing-firm .feed__firm::after,
.feed__row.is-typing-event .feed__event::after {
  content: '';
  display: inline-block;
  width: 0.5em;
  height: 0.95em;
  margin-left: 0.12em;
  background: var(--accent);
  vertical-align: -0.12em;
  animation: pp-caret 1000ms steps(1) infinite;
}

@keyframes pp-caret {
  0%, 50%   { opacity: 0.8; }
  51%, 100% { opacity: 0; }
}

/* ---------- opt out ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .js .reveal { opacity: 1; transform: none; }
  .feed.is-live .feed__row { opacity: 1; transform: none; }
  .feed__row.is-typing-firm .feed__firm::after,
  .feed__row.is-typing-event .feed__event::after { display: none; }

  /* The underline is a state, not an entrance — show it, do not draw it. */
  .js .section .h2::after { transform: scaleX(1); }
  /* No lift, no drift — but the accent edge stays, so hover still answers.
     Reduced motion means less movement, not less feedback. */
  .js .card:hover {
    transform: none;
    box-shadow: 0 0 0 1px rgba(61, 224, 143, 0.16);
  }
  [data-parallax] { transform: none !important; will-change: auto; }

  /* Same principle for the CTAs: the scale and the arrow travel are movement,
     so they go. The colour flip is feedback, so it stays — and it is the part
     that actually tells you the control is live. */
  .btn:hover,
  .btn:active { transform: none; }
  .btn:hover::after,
  .link-quiet:hover::after { transform: none; }

  /* The grain is static, but a blend-mode layer over a scrolling page is still
     compositing work on every frame for no benefit to anyone who asked for less
     motion. Drop it and keep the flat surface. */
  body::before { display: none; }

  /* The globe stops rotating — initGlobe never starts its rAF loop under
     reduced motion, and renders one static frame instead. Hover still works
     and still redraws, because that is feedback, not motion. The sequence
     keeps every stage visible: `is-staged` is never applied, so the rules
     that hide un-entered steps never match. */
  .js .seq.is-staged .seq__step { opacity: 1; transform: none; }
  .globe__readout { transition: none; }
}
