/* 阿桥 — the landing page at `/`.
 *
 * Layout and surface treatment. Every colour comes from tokens.css, which is
 * where the system is documented.
 *
 * ── The shape of the reference ───────────────────────────────────────────
 *
 * A near-black field, a single iridescent bubble rising into it from below, and
 * everything else kept quiet so the bubble is the only thing with colour in it.
 * Three rules follow from that, and they are what stop this drifting back into
 * an ordinary landing page:
 *
 *   1. The words are plain white. No gradient type, no coloured headings. All
 *      the colour on the page comes from the picture — that contrast is the
 *      whole effect, and tinting the headline destroys it.
 *   2. The primary action is a plain pill with maximum contrast and no hue: a
 *      white one on the dark page, a near-black one on the light page.
 *   3. Panels are glass, not cards. A translucent fill, a hairline of light on
 *      the top edge, a wide soft shadow — so the bubble reads THROUGH them and
 *      the page keeps one depth rather than a stack of opaque rectangles.
 *
 * ── Three constraints inherited from the app ─────────────────────────────
 *
 *   - Every byte is served by the binary. No CDN, no web font, no icon font,
 *     and no 3D render — the bubble is layered CSS gradients, which is what an
 *     iridescent form costs when you cannot ship a 4MB texture.
 *   - It survives 150% text and a phone. No fixed heights, a clamp()-based
 *     scale, and every grid wraps rather than scrolls.
 *   - Colour never carries meaning alone. The met / unsure / unmet rows are
 *     labelled in words as well as tinted — a window terminal in daylight loses
 *     hue before it loses text.
 */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink-700);
  font: var(--fs)/1.7 var(--font);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.wrap { width: 100%; max-width: 1180px; margin: 0 auto; padding: 0 clamp(20px, 4vw, 44px); }

a { color: var(--brand); }

h1, h2, h3 { color: var(--ink-900); margin: 0; font-weight: 600; }
h1 {
  font-size: clamp(2.15rem, 5.6vw, 4rem);
  line-height: 1.14;
  letter-spacing: -.03em;
}
h2 { font-size: clamp(1.55rem, 3.2vw, 2.5rem); line-height: 1.2; letter-spacing: -.025em; }
h3 { font-size: 1.02rem; line-height: 1.4; letter-spacing: -.01em; }
p { margin: 0; }
code { font-family: var(--mono); font-size: .84em; }

/* The reader who never gets the script still gets the page.
   `.reveal` is visible by default and only hidden once the inline head script
   has stamped `js` on <html> — so a blocked, failed or slow module leaves a
   readable document rather than a column of empty space. */
.reveal { transition: opacity .7s ease, transform .7s ease; }
html.js .reveal { opacity: 0; transform: translateY(18px); }
html.js .reveal.in { opacity: 1; transform: none; }

/* The recovery path, and why it is a separate class rather than just `.in`.
   `.in` starts a transition, and a transition only advances while the document
   is being rendered — so on the path that exists precisely BECAUSE nothing is
   being rendered (a crawler, a prerender, a background tab), adding `.in` moves
   the class but leaves the computed opacity at 0. Measured: 53/53 elements
   carried `.in` and the headline still computed to opacity 0. So the recovery
   states the final value outright and switches the transition off. */
html.shown .reveal { opacity: 1; transform: none; transition: none; }

@media (prefers-reduced-motion: reduce) {
  html.js .reveal { opacity: 1; transform: none; transition: none; }
  /* The bubble is a canvas driven by <video>; paintBubble() in home.js
     holds it on one frame, since CSS cannot stop a video. */
}

.skip {
  position: absolute; left: -9999px; top: 8px; z-index: 100;
  background: var(--surface); color: var(--ink-900);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 14px;
}
.skip:focus { left: 12px; }

:where(a, button):focus-visible {
  outline: 2px solid var(--brand); outline-offset: 3px; border-radius: var(--r-xs);
}

/* ── glass ──────────────────────────────────────────────────────────────
   One treatment, used by every panel on the page. The hairline is a top-edge
   highlight rather than a full border: a lit top edge and a soft shadow beneath
   is what makes a surface read as floating IN FRONT OF something, which a
   uniform 1px box does not. */
.glass {
  background: var(--glass);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow), inset 0 1px 0 var(--glass-line);
  backdrop-filter: blur(20px) saturate(140%);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
}

/* ── the header ─────────────────────────────────────────────────────────── */

.nav {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  border-bottom: 1px solid transparent;
  backdrop-filter: saturate(150%) blur(18px);
  -webkit-backdrop-filter: saturate(150%) blur(18px);
  transition: border-color .3s ease;
}
/* color-mix is recent. Where it is missing the declaration above is dropped and
   the header would be transparent over scrolling content, so an opaque value is
   stated first and only replaced where the blur can actually do its job. */
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .nav { background: var(--bg); }
}
.nav.is-stuck { border-bottom-color: var(--line); }

/* Brand left, links dead centre, actions right — three tracks, so the links
   stay centred on the page rather than centred on whatever is left over. */
.nav-in { display: flex; align-items: center; gap: 18px; min-height: 72px; }
@media (min-width: 900px) {
  .nav-in { display: grid; grid-template-columns: 1fr auto 1fr; }
  .nav-tools { justify-self: end; }
}

.brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; margin-right: auto; }
.brand-mark { width: 32px; height: 32px; display: inline-flex; flex: none; }
.brand-mark svg { width: 100%; height: 100%; }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text b { color: var(--ink-900); font-size: 1.02rem; font-weight: 600; letter-spacing: -.01em; }
.brand-text small { color: var(--ink-400); font-size: .62rem; letter-spacing: .17em; text-transform: uppercase; }

.nav-links { display: none; gap: 32px; }
.nav-links a {
  color: var(--ink-500); text-decoration: none; font-size: .85em;
  transition: color .2s ease;
}
.nav-links a:hover { color: var(--ink-900); }

.nav-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.lang { display: inline-flex; align-items: center; gap: 2px; }
.lang .ghost.is-on { color: var(--ink-900); background: var(--surface-2); }

.nav-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--r-pill);
  color: var(--ink-500); text-decoration: none;
  transition: color .2s ease, background .2s ease;
}
.nav-icon:hover { color: var(--ink-900); background: var(--surface-2); }
.nav-icon svg { width: 17px; height: 17px; }

@media (min-width: 900px) { .nav-links { display: flex; } }

/* The header is sticky, so anything it wraps onto a second row it keeps, for
   the whole page. What goes, in this order: the section links (they duplicate
   scrolling), then the theme control (a preference, and the default is already
   right for most readers), then the icon link, then the wordmark under 阿桥 —
   never the language pair, because the first screen of a bilingual product has
   to be reachable in both languages, and never the button the page exists to
   point at. */
@media (max-width: 620px) { .nav-links, #themeBtn, .nav-icon { display: none; } }
@media (max-width: 560px) {
  .brand-text small { display: none; }
  .nav-in { gap: 10px; }
  .ghost { padding: 7px 9px; }
  .cta-sm { padding: 9px 16px; }
}

/* ── actions ────────────────────────────────────────────────────────────── */

/* No hue and no gradient. The reference's primary action is a plain white pill
   on the dark page; the light theme takes the same idea and inverts it. This is
   the highest-contrast element on the page in both themes — 17.14:1 and
   18.59:1 measured — which is the right way round for the one thing every
   reader is meant to find. */
.cta {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  background: var(--brand-fill); color: var(--brand-ink);
  border: 1px solid transparent; border-radius: var(--r-pill);
  text-decoration: none; font-weight: 600;
  padding: 11px 22px; font-size: .9em; white-space: nowrap;
  transition: transform .22s ease, box-shadow .22s ease, opacity .22s ease;
}
.cta:hover { transform: translateY(-2px); box-shadow: var(--shadow); opacity: .93; }
.cta-lg { padding: 17px 40px; font-size: 1.02em; }
.cta-arrow { font-size: 1.05em; line-height: 1; }

.ghost {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid transparent; border-radius: var(--r-pill);
  color: var(--ink-500); cursor: pointer; font: inherit; font-size: .84em;
  padding: 8px 13px; text-decoration: none;
  transition: color .2s ease, background .2s ease;
}
.ghost:hover { color: var(--ink-900); background: var(--surface-2); }
.ghost-lg {
  padding: 16px 32px; font-size: 1em; font-weight: 500;
  border-color: var(--line); color: var(--ink-900);
}
.ghost-lg:hover { border-color: var(--ink-400); background: transparent; }

/* ── the hero ───────────────────────────────────────────────────────────── */

/* The hero follows the theme, like everything else.
 *
 * It was a permanently dark stage for one reason: the bubble was footage, and
 * both source clips are rendered on near-black, which cannot be composited onto
 * a warm white page. That reason is gone — the bubble is CSS now, and a
 * gradient has no background baked into it. The stage outlived its cause by one
 * change, and what was left was a black slab sitting in the middle of light
 * mode, which is not light mode.
 *
 * The rounded inset stays: both references put their page inside a rounded card
 * on a differently-coloured field, and that reads on either theme. */
.hero {
  position: relative; overflow: hidden;
  padding: clamp(60px, 10vw, 120px) 0 clamp(360px, 44vw, 560px);
  isolation: isolate;
  /* No card. The hero is part of the page.
     It went through an inset rounded panel with a border, then the same panel
     with a shadow — both inherited from the references, which are Dribbble
     presentation shots where the whole site sits inside a rounded frame on a
     backdrop. That frame is the MOCKUP, not the design; shipped, it just puts a
     visible rectangle around the top of the page. `overflow: hidden` stays,
     because the bubble still has to be clipped and faded. */
}


.hero-in { position: relative; z-index: 2; text-align: center; max-width: 880px; }

/* A soft dark ellipse behind the words.
   The bubble MOVES, so "the text happens to sit on a dark part" is not a fact
   about the design, it is a fact about one frame of an animation. Without this
   the lede was grey type on the bubble's bright core for a third of the loop —
   which is a contrast failure that no static check would ever catch, because
   statically it looks fine. The scrim rides above the bubble and under the
   text, is keyed to the theme, and is off in light where the bubble is faint. */
.hero-in::before {
  content: ""; position: absolute; z-index: -1;
  inset: -8% -14% -18%;
  background: radial-gradient(62% 58% at 50% 46%, rgba(0, 0, 0, var(--scrim)) 0%, transparent 72%);
  pointer-events: none;
}

.eyebrow {
  display: inline-flex; align-items: center; gap: 9px;
  color: var(--ink-500); font-size: .7rem; font-weight: 500;
  letter-spacing: .2em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 8px 18px; margin-bottom: 30px;
}

.hero h1 { margin-bottom: 24px; }
.hero h1 span { display: block; }

.lede {
  color: var(--ink-500); font-size: clamp(.94rem, 1.1vw, 1.02rem); line-height: 1.75;
  max-width: 34em; margin: 0 auto;
}

.hero-act { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 38px; }

/* ── the bubble ─────────────────────────────────────────────────────────
   Pure CSS. What the reference frames actually show, having gone back and
   looked properly:

     It is a FILLED organic mass with a lit rim — not a hoop.

   That was the mistake in every previous attempt. First it was five blurred
   blobs (a glow), then a thin annulus (a hoop with a hollow middle). The real
   thing is a soft egg-shaped body lit from the upper left: bright gold where
   the light hits, falling through amber to near-black at the far side, with a
   thin bright line following the silhouette, a cold blue refraction low down,
   and one hot caustic. The whole form is slightly out of focus.

   The parts:
     background     the body. One off-centre radial — that offset IS the light
                    source, and it is what makes a flat shape read as a sphere.
     box-shadow     the rim, as inset glows. An inset shadow follows the border
                    radius exactly, so it traces the silhouette however organic
                    the shape is; a gradient cannot do that.
     ::before       the cold refraction, low and left.
     ::after        the caustic — the one hot spot on the rim.
     filter: blur   the soft focus. Modest, and it works here where it did not
                    before, because there is now a structured form underneath
                    to soften rather than five vague clouds to smear together.

   border-radius is eight values, not 50%: the reference silhouette is an egg
   leaning slightly, and a perfect circle reads as a button. */

.bubble-wrap {
  /* No mask. It used to fade the bottom out with
       linear-gradient(#000 0%, #000 84%, transparent 99%)
     and that 84% stop drew a visible horizontal line ACROSS the bubble. The
     stop is a derivative break -- flat opaque, then a steep fade -- and the eye
     reads that kink as an edge (a Mach band), which on the light theme showed
     as a line through the middle of the form. Geometry confirms it: on the
     1098px hero the stop lands at y=922, and the canvas spans 578..1306, so the
     kink sat 47% down the bubble, exactly where it appeared.

     Nothing is lost by removing it. The fade it provided is now done properly
     by the canvas's own radial feather, which has no flat section to kink, and
     .hero already has overflow:hidden so the bubble is still clipped. */
  position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden;
}

/* ── the bubble ──────────────────────────────────────────────────────────────
   The stage for the liquid form. Square, because the shader normalises its uv
   by min(width, height) and frames the body in that square -- a wide box would
   just add empty margin either side and pay for it in fragments.

   Pushed down by 38% of its own width so the hero shows the upper part of the
   form rising past the copy, and derived from the width rather than a px clamp
   because the hero's bottom padding is in vw: a fixed offset made the visible
   part taller than the space under the text at some widths. */
.bubble-gl {
  position: absolute; left: 50%;
  bottom: calc(min(1100px, 92vw) * -0.38);
  width: min(1100px, 92vw); aspect-ratio: 1 / 1;
  transform: translateX(-50%);
  pointer-events: none;
}

/* Light is the default theme, so violet is the default film. Mirrors exactly
   the selectors tokens.css uses for --bub-*; if those move, these move too. */
.film-gold   { opacity: 0; }
.film-violet { opacity: 1; }
:root[data-theme="dark"] .film-gold   { opacity: 1; }
:root[data-theme="dark"] .film-violet { opacity: 0; }
@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] .film-gold   { opacity: 1; }
  :root[data-theme="system"] .film-violet { opacity: 0; }
}


/* A handful of dust motes, as in the reference. Fixed positions in a repeating
   tile rather than 40 elements. */
.stars {
  position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: var(--star-a);
  background-image:
    radial-gradient(1.5px 1.5px at 12% 18%, #fff, transparent),
    radial-gradient(1px 1px at 27% 42%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 43% 12%, #fff, transparent),
    radial-gradient(1px 1px at 58% 33%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 71% 8%, #fff, transparent),
    radial-gradient(1px 1px at 84% 28%, #fff, transparent),
    radial-gradient(1px 1px at 92% 47%, #fff, transparent),
    radial-gradient(1px 1px at 7% 58%, #fff, transparent),
    radial-gradient(1.5px 1.5px at 36% 63%, #fff, transparent),
    radial-gradient(1px 1px at 63% 55%, #fff, transparent);
}

/* ── the floating stat panels ───────────────────────────────────────────
   The reference hangs two small glass panels over the bubble. Both of these
   carry a real number: 31 is the count of official destinations in
   data/service_directory.json, and 0 is the number of eligibility verdicts this
   product will ever issue — which is the thesis of the whole thing, and the
   only stat worth putting that big. */
.stat {
  position: absolute; z-index: 3; width: min(272px, 42vw);
  padding: 17px 19px;
}
/* Anchored to the BOTTOM, not the top, and inside the hero's own bottom
   padding. Positioned from the top with a percentage they drifted onto the
   lede and sat across the primary button — the text block's height depends on
   how the copy wraps, so any top-anchored percentage is a collision waiting for
   the right viewport. From the bottom they are pinned into the empty region the
   bubble occupies and cannot reach the words at any width. */
.stat-a { left: clamp(16px, 6vw, 120px);  bottom: clamp(158px, 25vw, 330px); }
.stat-b { right: clamp(16px, 6vw, 120px); bottom: clamp(64px, 12vw, 176px); }

.stat-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.stat-label { color: var(--ink-400); font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; }
.stat-go {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 25px; height: 25px; border-radius: 50%;
  background: var(--brand-fill); color: var(--brand-ink);
  font-size: .75em; text-decoration: none;
}
.stat-val { color: var(--ink-900); font-size: 1.75rem; font-weight: 600; letter-spacing: -.03em; margin-top: 12px; line-height: 1.1; }
.stat-sub { color: var(--ink-500); font-size: .78rem; margin-top: 5px; line-height: 1.5; }
.stat-bar { height: 2px; border-radius: 2px; background: var(--line); margin-top: 13px; overflow: hidden; }
.stat-bar i { display: block; height: 100%; background: var(--brand-fill); }

/* The wrapper dissolves so each panel's containing block is .hero, the tall
   element with the bubble in it, rather than the text block. */
@media (min-width: 861px) { .stats { display: contents; } }

@media (max-width: 860px) {
  /* Two panels absolutely positioned over a bubble is a desktop composition.
     On a phone they would sit on top of each other and on top of the button,
     so they leave the picture and stack under it as ordinary panels. */
  .hero { padding-bottom: clamp(230px, 56vw, 330px); }
  .stat { position: static; width: auto; }
  .stats {
    display: grid; gap: 12px; position: relative; z-index: 3;
    max-width: 1180px; margin: 34px auto 0; padding: 0 clamp(20px, 4vw, 44px);
  }
}

/* ── sections ───────────────────────────────────────────────────────────── */

.section { padding: clamp(64px, 9vw, 120px) 0; position: relative; }
/* The header is sticky, so an in-page link would otherwise land its target
   underneath it — the reader clicks "它的边界" and arrives mid-paragraph with
   the heading hidden. */
.section[id], #main { scroll-margin-top: 92px; }
.band { background: var(--surface-2); }

.s-label {
  display: flex; align-items: center; gap: 13px; color: var(--ink-400);
  font-size: .7em; letter-spacing: .19em; text-transform: uppercase;
  margin-bottom: 18px;
}
.s-num { font-family: var(--mono); color: var(--brand); font-size: .95em; letter-spacing: .05em; }
.sub { color: var(--ink-500); max-width: 54em; margin-top: 18px; }
.section h2 + .grid, .section h2 + .steps, .sub + .grid, .sub + .steps, .sub + .rules, .section h2 + .rules { margin-top: 48px; }
.note { color: var(--ink-400); font-size: .85em; margin-top: 24px; }

.grid { display: grid; gap: 16px; }
/* Exactly two, not auto-fit. Both .grid-2 sections hold FOUR cards, and an
   auto-fit track lands on three-plus-one at desktop widths — the orphan reads
   as a card that failed to load rather than as the fourth of a set. */
.grid-2 { grid-template-columns: 1fr; }
@media (min-width: 760px) { .grid-2 { grid-template-columns: 1fr 1fr; } }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(262px, 1fr)); }

.card {
  position: relative; padding: 28px;
  transition: transform .25s ease, box-shadow .25s ease;
}
.card:hover { transform: translateY(-3px); }
.card h3 { margin-bottom: 10px; }
.card p { color: var(--ink-500); font-size: .91em; }
.card.sm { padding: 23px; }
/* The corpus tally under the first "honest limits" card. It is a second
   sentence rather than a number inside the claim, because /api/meta feeds it
   and the claim has to read correctly without it. Spacing matches .card h3 so
   the card keeps one rhythm. See home.js. */
.card .tally { margin-top: 10px; }
.card.warn { background: var(--warn-soft); border-color: var(--warn-border); }
.card.warn h3 { color: var(--warn); }
.card.warn p { color: var(--ink-700); }

.card-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--brand-soft); border: 1px solid var(--brand-border);
  color: var(--brand); margin-bottom: 16px;
}
.card-ico svg { width: 19px; height: 19px; }

/* ── the sample conversation ────────────────────────────────────────────── */

.preview-sec .chat { margin-top: 44px; padding: clamp(20px, 3vw, 36px); max-width: 860px; }
.chat-tag {
  display: inline-block; font-size: .68em; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-400); border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 5px 14px; margin-bottom: 24px;
}
.bubble-you {
  background: var(--brand-fill); color: var(--brand-ink);
  border-radius: var(--r) var(--r) 6px var(--r);
  padding: 13px 18px; margin-left: auto; max-width: 34em; width: fit-content; font-size: .94em;
}
.turn { display: flex; gap: 13px; margin-top: 26px; }
.turn-face { width: 34px; height: 34px; flex: none; }
.turn-face svg { width: 100%; height: 100%; }
.turn-body { min-width: 0; flex: 1; }

.badge {
  display: inline-block; background: var(--brand-soft); color: var(--brand);
  border: 1px solid var(--brand-border); border-radius: var(--r-pill);
  padding: 5px 13px; font-size: .75em; margin-bottom: 13px;
}
.say { color: var(--ink-700); font-size: .94em; }

.ocard { margin-top: 18px; border: 1px solid var(--line); border-radius: var(--r); padding: 20px; background: var(--surface-2); }
.ocard-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 9px; }
.ocard-id { font-family: var(--mono); font-size: .69em; color: var(--ink-400); }
.tag-free {
  display: inline-block; margin-top: 11px; background: var(--ok-soft); color: var(--ok);
  border: 1px solid var(--ok-border); border-radius: var(--r-pill); padding: 4px 12px; font-size: .77em; font-weight: 600;
}
.ocard-meta { display: grid; gap: 7px; margin: 16px 0 0; }
.ocard-meta > div { display: flex; gap: 10px; font-size: .85em; }
.ocard-meta dt { color: var(--ink-400); flex: none; min-width: 3.2em; }
.ocard-meta dd { margin: 0; color: var(--ink-700); }
.ocard-need { margin-top: 13px; font-size: .85em; color: var(--ink-500); }

.crit-title { margin-top: 20px; font-size: .68em; color: var(--ink-400); letter-spacing: .16em; text-transform: uppercase; }
.crit { list-style: none; margin: 11px 0 0; padding: 0; display: grid; gap: 9px; }
/* Tint AND a word. The word is the part that survives a bad screen. */
.crit li { display: flex; gap: 11px; align-items: baseline; font-size: .85em; border-left: 3px solid var(--line); padding-left: 13px; }
.crit b { flex: none; font-size: .92em; }
.crit span { color: var(--ink-500); }
li.is-ok { border-left-color: var(--ok); }
li.is-ok b { color: var(--ok); }
li.is-unknown { border-left-color: var(--warn); }
li.is-unknown b { color: var(--warn); }
li.is-unmet { border-left-color: var(--ink-400); }
li.is-unmet b { color: var(--ink-400); }

.trace { margin-top: 18px; font-size: .77em; color: var(--ink-400); font-family: var(--mono); }
.chat-foot { margin-top: 25px; padding-top: 18px; border-top: 1px solid var(--line); font-size: .79em; color: var(--ink-400); }

/* ── the five steps ─────────────────────────────────────────────────────── */

.steps { list-style: none; margin: 48px 0 0; padding: 0; counter-reset: step; display: grid; gap: 0; }
.steps li {
  counter-increment: step; position: relative;
  padding: 26px 0 26px 78px; border-top: 1px solid var(--line);
}
.steps li:last-child { border-bottom: 1px solid var(--line); }
.steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute; left: 0; top: 26px;
  font-family: var(--mono); font-size: .76em; color: var(--brand);
  border: 1px solid var(--brand-border); background: var(--brand-soft);
  border-radius: var(--r-pill);
  width: 50px; height: 30px;
  display: flex; align-items: center; justify-content: center;
}
.steps li b { display: block; color: var(--ink-900); font-size: 1.1rem; font-weight: 600; margin-bottom: 8px; letter-spacing: -.015em; }
.steps li p { color: var(--ink-500); font-size: .92em; max-width: 60em; }

/* ── the rules ──────────────────────────────────────────────────────────── */

.rules { border-radius: var(--r-lg); overflow: hidden; }
.rule-hd, .rule { display: grid; gap: 6px 24px; padding: 19px clamp(17px, 2.4vw, 28px); }
.rule-hd {
  display: none; color: var(--ink-400);
  font-size: .68em; letter-spacing: .16em; text-transform: uppercase;
  border-bottom: 1px solid var(--line);
}
.rule { border-top: 1px solid var(--line); }
.rule:first-of-type { border-top: 0; }
.rule b { color: var(--ink-900); font-size: .95em; font-weight: 600; }
.rule span { color: var(--ink-500); font-size: .89em; }
.rule code { color: var(--brand); word-break: break-word; }

@media (min-width: 880px) {
  .rule-hd, .rule { grid-template-columns: 1.05fr 1.5fr .85fr; align-items: baseline; }
  .rule-hd { display: grid; }
  .rule-hd + .rule { border-top: 0; }
}

/* ── the voice section ──────────────────────────────────────────────────── */

.voice { display: grid; gap: clamp(28px, 5vw, 64px); align-items: start; }
@media (min-width: 900px) { .voice { grid-template-columns: 300px 1fr; } }

.voice-art { position: relative; justify-self: center; }
/* Scoped to .voice-figure, NOT to `.voice-art img`.
 *
 * Why, because the loose selector was a live bug: .voice-mark is a child of
 * .voice-art, and once the avatar stopped being an inline <svg> and became an
 * <img> (see avatar.js), `.voice-art img` started matching the 62px badge too.
 * At specificity 0,1,1 it beat .oba-avatar's own `width: 100%` (0,1,0), so the
 * badge's image was laid out 230px wide inside a 62px overflow:hidden box — a
 * blown-up crop of one cheek. The rule below must never widen to `img` again.
 * docs/bugfix/2026-09-02-the-voice-badge-was-sized-by-the-figure-rule.md
 *
 * The figure is the full character (/mascot-full.png, 340×800), not the square
 * head the avatar slots use, so it is sized narrower than the 260px square that
 * used to sit here. The 200px cap is set against the TEXT, not the column: the
 * three paragraphs beside it run about 367px tall at desktop, and 200px wide
 * puts the figure at 468px — close enough to read as one block rather than a
 * column that keeps going after the words stop. It was 230px (538px tall) and
 * overhung by 171px, which looked like a layout accident. It cannot simply match
 * the text height: 367px tall is 156px wide, at which point the character is too
 * small to be worth showing. Width-driven with height:auto rather than a fixed
 * box — a full figure letterboxed into a square is a small figure with two empty
 * bands. The 150px floor is what keeps her recognisable once the section stacks:
 * at a 375px viewport that is 150×350, centred, and the badge stays inside the
 * page — measured, not assumed. */
.voice-art .voice-figure {
  display: block; width: clamp(150px, 40vw, 200px); height: auto; border-radius: var(--r-lg);
  background: var(--surface-2); border: 1px solid var(--line); box-shadow: var(--glow);
}
.voice-mark {
  position: absolute; right: -10px; bottom: -10px; width: 62px; height: 62px;
  border-radius: 19px; overflow: hidden; box-shadow: var(--shadow);
}
/* svg is the historical shape, img is the current one; both fill the badge. */
.voice-mark svg, .voice-mark img { width: 100%; height: 100%; display: block; }
.voice-text > p:not(.s-label) { color: var(--ink-500); margin-top: 18px; max-width: 52em; }
.voice-text h2 { margin-bottom: 4px; }

/* ── the closing call ───────────────────────────────────────────────────── */

.end { text-align: center; position: relative; overflow: hidden; }
.end-in { max-width: 640px; margin: 0 auto; position: relative; z-index: 2; }
.end h2 { margin-bottom: 16px; }
.end p { color: var(--ink-500); }
.end .cta { margin-top: 34px; }

.foot { border-top: 1px solid var(--line); padding: 44px 0; }
.foot-in { display: flex; flex-wrap: wrap; gap: 10px 28px; align-items: center; font-size: .84em; color: var(--ink-500); }
.foot-in b { color: var(--ink-900); font-weight: 600; }
.foot-links { display: flex; gap: 13px; align-items: center; }
.foot-links a { text-decoration: none; }
.foot-links a:hover { text-decoration: underline; }
.foot-note { flex-basis: 100%; color: var(--ink-400); font-size: .92em; }
