/* ══════════════════════════════════════════════════════════════════
         The homepage. The hex field is a bounded BAND at the top, not a
         full viewport, and three things about it are load-bearing:

         1. The bottom third dissolves into the page. Full-bleed, the
            enormous near-field tiles get sliced off on a hard horizontal
            cut exactly where content begins.
         2. The scrim behind the copy is part of the composition, not a
            patch over it — any text over the field sits on one.
         3. The render loop pauses once the band scrolls away.
            requestAnimationFrame throttles for hidden TABS but not for
            elements scrolled past, so without it the GPU keeps drawing a
            field nobody can see.

         Renderer lives in /assets/hexfield.js, generated from
         design/hero-hexfield.html by design/tools/build-hexfield.py. It is
         an external file rather than inline because the site ships
         script-src 'self' — an inline <script> is silently blocked.
         ══════════════════════════════════════════════════════════════════ */

      :root {
        /* The field's fog blends to this, so the page canvas has to match it
           exactly. See --bg-deep in brand.css. */
        --bg: var(--bg-deep);

        /* Header height is DECLARED, not measured, because the hero height is
           computed from it below. `header { height: var(--header-h) }` makes the
           declaration authoritative so the two cannot disagree — without that,
           the hero's arithmetic silently drifts the moment the nav's type or
           padding changes. */
        --header-h: 68px;

        /* How much of the page below shows under the hero. ZERO, deliberately.
           An earlier 88px "deliberate peek" was wrong twice over:
             1. `section` has 72px of top padding, so 88px of peek reached 16px
                PAST it and clipped the tops of the "Packet in. Action out."
                letterforms — visible on both desktop and mobile.
             2. Even sized under 72px it buys nothing, because it would be
                INVISIBLE: .hero::after dissolves to var(--bg) and the page below
                is the same var(--bg), so an empty peek has no edge to perceive.
                The only way it reads at all is by revealing content, which is
                exactly the thing to avoid.
           So the hero fills the viewport below the header exactly, and content
           starts on scroll. If a scroll affordance is ever wanted it needs to be
           a real element, not a sliver of the next section. */
        --hero-peek: 0px;
      }

      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        background: var(--bg);
        color: var(--ink);
        font-family: var(--sans);
        line-height: 1.62;
        -webkit-font-smoothing: antialiased;
        text-rendering: optimizeLegibility;
      }

      /* ── Header ──────────────────────────────────────────────────────── */

      header {
        position: sticky;
        top: 0;
        z-index: 20;
        display: flex;
        align-items: center;
        gap: 1.6rem;
        /* Pinned rather than content-derived so --header-h stays truthful — the
           hero's height subtracts it. box-sizing: border-box is global, so this
           includes the padding and the bottom border. */
        height: var(--header-h);
        padding: 0.9rem clamp(1.2rem, 5vw, 3.5rem);
        background: rgba(6, 11, 17, 0.72);
        backdrop-filter: blur(10px);
        border-bottom: 1px solid var(--rule);
        font-size: 0.9rem;
      }

      /* The <a> is header's flex child, so the auto margin that pushes the nav
         and CTA to the right edge has to live HERE. Putting it on the <img>
         inside does nothing — an auto margin only distributes free space
         against flex siblings, and the img's only sibling context is the
         inline <a> that wraps it. */
      .brand {
        display: flex;
        align-items: center;
        margin-right: auto;
      }

      .lockup {
        /* Height-driven only: the file's viewBox carries the 292:28 ratio, so
           width follows. The lockup is 10.3:1, which means its WIDTH is ~10x
           whatever is set here — the single biggest consumer of header space,
           and the reason narrow viewports scale it down below. */
        height: 20px;
        width: auto;
        display: block;
      }

      header nav {
        display: flex;
        gap: 1.3rem;
        color: var(--muted);
      }

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

      header a:hover {
        color: var(--ink);
      }

      .cta {
        color: #fff;
        background: var(--brand);
        border-radius: 8px;
        padding: 0.45rem 0.95rem;
        font-weight: 600;
        text-decoration: none;
        /* Kept from when this button read "Get started", which wrapped at 320px
           and took it from 38px to 61px tall. "Download" is one word and cannot
           wrap, so this is currently inert — retained because the constraint is
           real for any future two-word label, not because it is doing work now. */
        white-space: nowrap;
      }

      .cta:hover {
        background: var(--brand-bright);
        color: #fff;
      }

      /* ── Download menu ───────────────────────────────────────────────────
         Positioned relative to the header control, not the page, so it tracks
         the button at every width the header scales through. */
      .dl {
        position: relative;
      }

      .dl-menu {
        position: absolute;
        top: calc(100% + 0.5rem);
        right: 0;
        z-index: 50;
        min-width: 16rem;
        padding: 0.4rem;
        /* OPAQUE, deliberately, and this is the whole point of the two
           declarations. --panel is rgba(255,255,255,0.04) — an OVERLAY token,
           correct for .facet/.closing because those sit in flow on the opaque
           page background. A menu floats over whatever it happens to open on
           top of, and 4% white is not a background, so the copy underneath read
           straight through it (worst on a phone, where the fixed menu below
           covers body text rather than the dark hero).

           Painting the same tint over an opaque --bg composites to the exact
           colour a card renders as, while stopping the bleed. The previous
           `var(--panel, #0e1a24)` looked like it handled this, but a var()
           fallback applies only when the property is UNDEFINED, and brand.css
           always defines --panel — so the opaque colour was unreachable. */
        background-color: var(--bg);
        background-image: linear-gradient(var(--panel), var(--panel));
        border: 1px solid var(--rule);
        border-radius: 10px;
        box-shadow: 0 12px 30px rgb(0 0 0 / 45%);
        display: flex;
        flex-direction: column;
        gap: 0.1rem;
      }

      /* [hidden] needs an explicit rule: the flex display above would otherwise
         win over the UA stylesheet's `display: none`, leaving the menu open. */
      .dl-menu[hidden] {
        display: none;
      }

      .dl-item {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 1rem;
        padding: 0.5rem 0.6rem;
        border-radius: 6px;
        color: var(--ink);
        text-decoration: none;
        white-space: nowrap;
      }

      .dl-item:hover,
      .dl-item:focus-visible {
        background: rgb(255 255 255 / 6%);
        color: var(--ink);
      }

      /* The detected platform. Brand blue is the only interactive colour on this
         site, so the highlight is a tint of it rather than a new accent — and it
         is set ONLY when both the OS and the architecture are known (see
         download.js), because a confident mark on one of two incompatible macOS
         builds is worse than no mark. */
      .dl-item.is-me {
        background: rgb(32 140 183 / 18%);
        box-shadow: inset 0 0 0 1px rgb(32 140 183 / 45%);
      }

      .dl-arch {
        color: var(--muted);
        font-size: 0.82em;
      }

      .dl-warn,
      .dl-hint {
        margin: 0.3rem 0.6rem 0.45rem;
        color: var(--muted);
        font-size: 0.78rem;
        line-height: 1.35;
      }

      .dl-warn {
        color: #f5a524;
      }

      .dl-all {
        margin-top: 0.25rem;
        padding: 0.45rem 0.6rem 0.3rem;
        border-top: 1px solid var(--rule);
        color: var(--muted);
        font-size: 0.8rem;
        text-decoration: none;
      }

      .dl-all:hover {
        color: var(--ink);
      }

      /* The header is already tight at 320px (see the .cta note above). Let the
         menu use the viewport rather than forcing the header to grow. */
      @media (max-width: 420px) {
        .dl-menu {
          position: fixed;
          left: 0.5rem;
          right: 0.5rem;
          min-width: 0;
        }
      }

      /* ── Hero band ───────────────────────────────────────────────────── */

      .hero {
        position: relative;
        /* Fill the viewport below the header, less a FIXED peek of the next
           section. The previous `min(82vh, 780px)` looked right on a laptop by
           accident — 82vh happens to leave 59-95px over — but past ~950px of
           viewport the 780px cap bound while the screen kept growing, so on a
           tall display a whole section climbed above the fold (measured: 353px
           of content at 1200px tall, ~840px at 1690px). Subtracting a constant
           makes the peek identical at every height instead of emergent.
           This is the vh baseline; the svh upgrade is in an @supports block
           below, and that gating is MANDATORY rather than tidy — see there. */
        height: calc(100vh - var(--header-h) - var(--hero-peek));
        min-height: 470px;
        overflow: hidden;

        /* Static poster behind the canvas. Two jobs: it is what WebGL-less or
           WebGL-blocked visitors see instead of an empty band, and it is the
           first-paint placeholder, since the canvas holds nothing until the
           shaders compile. The canvas is opaque and covers it whenever the
           renderer works, so this costs those visitors one cached image.
           Regenerate with design/tools/build-poster.sh. */
        background-color: var(--bg);
        background-image: url("/assets/hero-poster.jpg");
        background-size: cover;
        background-position: center;
        background-repeat: no-repeat;
      }

      /* svh, not vh or dvh: vh includes the mobile URL bar, so a vh-sized hero
         overflows and the page jumps when the bar collapses; dvh re-lays-out
         continuously during scroll. svh is the "always fits" one.
         This MUST be an @supports block and not simply a second `height:`
         declaration after the vh one. A declaration containing var() cannot be
         validated at parse time — var() could expand to anything — so it is
         accepted and WINS THE CASCADE, and only later, at computed-value time,
         does calc(100svh - …) fail on an engine without svh. A property that is
         invalid at computed-value time falls back to its INITIAL value (auto for
         height), not to the previous declaration. So the plain two-declaration
         fallback pattern silently discards the vh line: measured on Chrome 98,
         the hero collapsed to `auto` — 0px of absolutely-positioned children —
         and only min-height kept it visible at 470px. */
      @supports (height: 1svh) {
        .hero {
          height: calc(100svh - var(--header-h) - var(--hero-peek));
        }
      }

      #gl {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        display: block;
      }

      /* Top haze, so the field emerges rather than starting at the nav. */
      .hero::before {
        content: "";
        position: absolute;
        inset: 0 0 auto 0;
        height: 40%;
        background: linear-gradient(
          to bottom,
          rgba(6, 11, 17, 0.94) 22%,
          rgba(32, 140, 183, 0.06) 76%,
          transparent
        );
        pointer-events: none;
        z-index: 1;
      }

      /* The bottom third dissolves into the page. */
      .hero::after {
        content: "";
        position: absolute;
        inset: auto 0 0 0;
        height: 38%;
        background: linear-gradient(
          to bottom,
          transparent,
          rgba(6, 11, 17, 0.72) 52%,
          var(--bg) 96%
        );
        pointer-events: none;
        z-index: 1;
      }

      .copy {
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        /* 50rem/62vw: the headline's longest line is 11.5em and has to sit on
           ONE line, and the column — not the h1 — is the binding measure. The
           lede keeps its own 34ch cap, so reading width is unchanged. */
        width: min(50rem, 62vw);
        z-index: 2;
        display: flex;
        flex-direction: column;
        justify-content: center;
        padding: 0 clamp(1.5rem, 6vw, 5.5rem);
        /* Part of the composition, not a patch over it. */
        background: linear-gradient(
          to right,
          rgba(6, 11, 17, 0.94) 0%,
          rgba(6, 11, 17, 0.84) 46%,
          transparent 100%
        );
        pointer-events: none;
      }

      .eyebrow {
        display: flex;
        align-items: center;
        gap: 0.6rem;
        font-family: var(--mono);
        font-size: 0.72rem;
        letter-spacing: 0.17em;
        text-transform: uppercase;
        color: var(--brand-bright);
        margin-bottom: 1rem;
      }

      .eyebrow::before {
        content: "";
        width: 26px;
        height: 2px;
        background: linear-gradient(to right, var(--aqua), var(--brand));
      }

      h1 {
        margin: 0 0 1.1rem;
        /* Sized so the longest sentence ("Command the device.") stays on ONE
           line inside the .copy measure — the two-line stack IS the structure,
           and a third line breaks it mid-clause.
           The vw term matters as much as the cap: .copy content scales at
           0.50W while this line needs ~11.5em, so any coefficient above
           ~0.0434 overflows at EVERY width in the fluid band no matter what
           the cap is. Measure both terms, not just the cap.
           text-wrap: balance is deliberately absent, but NOT for the reason
           first recorded here: it was blamed for wrapping a line that fit, and
           that was a misdiagnosis made on a Chrome 98 verification browser
           where `text-wrap: balance` is not supported at all and was therefore
           inert. The third line was plain overflow. It stays off because
           explicit block spans below already pin the breaks, which leaves
           balance nothing legitimate to do. */
        font-size: clamp(1.95rem, 4.1vw, 3.15rem);
        line-height: 1;
        letter-spacing: -0.036em;
        font-weight: 700;
      }

      /* The headline is two phrases, so the break belongs between them —
         never mid-phrase ("…IoT / platform").
         Blocks pin it instead of leaving it to the wrap algorithm.
         nowrap is a metric guard, not a layout choice: the sizing above was
         tuned against whatever a headless Chrome 98 on Linux resolves system-ui
         to, which is NOT the face a Windows or macOS visitor gets. If their
         metrics run wider this spills a little further over the (empty) field
         rather than breaking mid-clause; even +15% stays inside the .copy box at
         every width measured. Re-tune once a real webfont is picked — at which
         point the metrics are known and this guard can go. */
      h1 span {
        display: block;
        white-space: nowrap;
      }

      .lede {
        margin: 0;
        font-size: clamp(0.98rem, 1.2vw, 1.08rem);
        color: var(--ink-2);
        max-width: 34ch;
      }

      .hero-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 0.7rem;
        margin-top: 1.7rem;
        /* .copy is pointer-events: none so the field stays inspectable behind
           the scrim; the buttons have to opt back in. */
        pointer-events: auto;
      }

      .btn {
        font-size: 0.92rem;
        font-weight: 600;
        text-decoration: none;
        border-radius: 9px;
        padding: 0.62rem 1.15rem;
        border: 1px solid transparent;
      }

      .btn-primary {
        color: #fff;
        background: var(--brand);
      }

      .btn-primary:hover {
        background: var(--brand-bright);
      }

      .btn-ghost {
        color: var(--ink);
        border-color: var(--rule-strong);
      }

      .btn-ghost:hover {
        border-color: var(--brand-bright);
      }

      a:focus-visible {
        outline: 2px solid var(--brand-bright);
        outline-offset: 3px;
      }

      .fallback {
        position: absolute;
        inset: 0;
        display: none;
        place-items: center;
        padding: 2rem;
        text-align: center;
        color: var(--muted);
        font-family: var(--mono);
        font-size: 0.8rem;
        z-index: 4;
      }

      /* ── Page body ───────────────────────────────────────────────────── */

      main {
        max-width: 68rem;
        margin: 0 auto;
        padding: 0 clamp(1.2rem, 5vw, 3.5rem) 5rem;
      }

      section {
        padding-top: 4.5rem;
      }

      /* The header is sticky, so an anchor jump would otherwise land the
         section heading UNDERNEATH it. scroll-margin-top offsets the scroll
         target by the header's own height. */
      section[id] {
        scroll-margin-top: calc(var(--header-h) + 1rem);
      }

      html {
        scroll-behavior: smooth;
      }

      @media (prefers-reduced-motion: reduce) {
        html {
          scroll-behavior: auto;
        }
      }

      h2 {
        font-size: clamp(1.6rem, 3vw, 2.2rem);
        line-height: 1.12;
        letter-spacing: -0.024em;
        margin: 0 0 0.9rem;
        text-wrap: balance;
      }

      section > p {
        color: var(--ink-2);
        max-width: 62ch;
        margin: 0 0 1.4rem;
      }

      /* The facet: a chamfered corner on the mark's 30° isometric grid, with a
         2px accent rule naming the area. The accent is identification only —
         nothing here is interactive. */
      .facets {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
        gap: 1rem;
        margin-top: 1.8rem;
      }

      .facet {
        --f: var(--brand);
        position: relative;
        background: var(--panel);
        border: 1px solid var(--rule);
        clip-path: polygon(
          0 0,
          calc(100% - 18px) 0,
          100% 18px,
          100% 100%,
          0 100%
        );
        padding: 1.05rem 1.1rem 1.15rem;
      }

      .facet::after {
        content: "";
        position: absolute;
        inset: 0 0 auto 0;
        height: 2px;
        background: var(--f);
      }

      .facet h3 {
        margin: 0 0 0.3rem;
        font-size: 1.02rem;
        letter-spacing: -0.005em;
      }

      .facet p {
        margin: 0;
        font-size: 0.875rem;
        color: var(--muted);
      }

      @media (prefers-reduced-motion: reduce) {
        /* The field's motion is ambient, not informational, so honouring the
           preference costs nothing. The renderer reads this flag too; the rule
           here covers the CSS transitions. */
        * {
          animation-duration: 0.001ms !important;
          transition-duration: 0.001ms !important;
        }
      }

      @media (max-width: 780px) {
        /* The scrim turns from a left-to-right wipe into a top-to-bottom one,
           because there is no room for a side-by-side composition. The copy
           stays VERTICALLY CENTERED as on desktop: it used to be top-aligned
           with 2.5rem of padding, which was fine while the hero was ~570px but
           left a large empty stretch of bare field under the buttons once the
           band grew to fill the viewport. Centred, the field reads as being
           behind the text rather than stacked beneath it — the same composition
           as desktop.
           The gradient holds near-opaque through the upper two thirds where the
           text sits, then releases so the bright near-field tiles at the bottom
           of the band still come through. */
        .copy {
          width: 100%;
          background: linear-gradient(
            to bottom,
            rgba(6, 11, 17, 0.94) 0%,
            rgba(6, 11, 17, 0.9) 56%,
            rgba(6, 11, 17, 0.42) 80%,
            transparent 100%
          );
        }

        /* Phones are too narrow for 11.5em on one line at any readable size,
           so release the metric guard and let the headline take three lines. */
        h1 span {
          white-space: normal;
        }

        header nav {
          display: none;
        }

        /* The nav is gone here, so the header is just brand + CTA. Tighten the
           gap so narrow phones are not relying on it. */
        header {
          gap: 0.9rem;
        }
      }

      /* Narrow phones: keep the wordmark, buy the room elsewhere.
         The budget at 320px was the problem — a 206px lockup plus a 123px CTA
         plus the gap wanted 322px inside 281px of usable width, which wrapped
         the button and scrolled the document sideways. Scaling the lockup to
         15px tall (155px wide), compacting the CTA and trimming the header's
         own padding brings it to ~262px inside 288px. */
      @media (max-width: 480px) {
        header {
          padding-left: 1rem;
          padding-right: 1rem;
          gap: 0.75rem;
        }

        /* Fluid rather than a fixed 15px: at a flat 15px the header budget came
           out to -1px of slack at 320 and -21px at 300, which does not scroll
           the document (it spills into the header's own padding) but leaves the
           CTA jammed against the edge. Scaling with the viewport keeps real
           slack all the way down: ~49px at 360, ~25px at 320, ~4px at 300. */
        .lockup {
          height: clamp(12px, 3.9vw, 15px);
        }

        .cta {
          padding: 0.4rem 0.7rem;
          font-size: 0.82rem;
        }
      }

      /* ── Page content components ─────────────────────────────────────── */

      /* ── See it run ──────────────────────────────────────────────────── */

      /* The frame reserves a 16:9 box before the video loads, so nothing
         shifts when the poster or the first frames arrive. */
      .run {
        margin: 1.8rem 0 0;
      }

      .run-frame {
        position: relative;
        aspect-ratio: 16 / 9;
        background: var(--panel);
        border: 1px solid var(--rule);
        overflow: hidden;
      }

      .run-frame video {
        display: block;
        width: 100%;
        height: 100%;
        object-fit: cover;
      }

      /* Bottom-right: the burned-in caption chips sit bottom-left. */
      .run-toggle {
        position: absolute;
        right: 0.75rem;
        bottom: 0.75rem;
        min-width: 44px;
        min-height: 44px;
        padding: 0.4rem 0.9rem;
        font: inherit;
        font-size: 0.85rem;
        font-weight: 600;
        color: var(--ink);
        background: rgba(6, 11, 17, 0.78);
        border: 1px solid var(--rule-strong);
        cursor: pointer;
      }

      .run-toggle:hover {
        border-color: var(--brand-bright);
      }

      .run-toggle:focus-visible {
        outline: 2px solid var(--brand-bright);
        outline-offset: 2px;
      }

      .run figcaption {
        margin-top: 0.9rem;
        max-width: 62ch;
        font-size: 0.875rem;
        color: var(--muted);
      }

      /* ── Dashboards ──────────────────────────────────────────────────── */

      /* The dashboard loop is a 960x1080 capture: portrait-ish, so it sits beside
         the copy on a wide screen and below it on a narrow one. It reuses the See
         it run frame, toggle and caption; only the box's aspect ratio differs. */
      .dash-split {
        display: grid;
        gap: 1.4rem 2.5rem;
        align-items: start;
      }

      .dash-copy > p {
        color: var(--ink-2);
        max-width: 62ch;
        margin: 0 0 1.4rem;
      }

      .dash-split .run {
        margin: 0;
        width: 100%;
        max-width: 30rem;
      }

      .dash-split .run-frame {
        aspect-ratio: 8 / 9;
      }

      @media (min-width: 900px) {
        .dash-split {
          grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
        }
      }

      /* The eyebrow above each section is the spine motif from the brand
         direction, reduced to a node: a short accent rule plus the area name.
         --f carries that area's accent, so the rule, the facet cards and the
         section marker all agree without repeating the colour three times. */
      .kicker {
        display: flex;
        align-items: center;
        gap: 0.6rem;
        font-family: var(--mono);
        font-size: 0.7rem;
        letter-spacing: 0.16em;
        text-transform: uppercase;
        color: var(--muted);
        margin-bottom: 0.85rem;
      }

      .kicker::before {
        content: "";
        width: 22px;
        height: 2px;
        flex: none;
        background: var(--f, var(--brand));
      }

      /* A compact labelled list for "what this actually is" detail. dl rather
         than a table: these are term/definition pairs, not tabular data. */
      .spec {
        display: grid;
        grid-template-columns: minmax(9rem, 14rem) 1fr;
        gap: 0.05rem 1.6rem;
        margin: 1.6rem 0 0;
        border-top: 1px solid var(--rule);
      }

      .spec dt {
        padding: 0.7rem 0 0.2rem;
        font-weight: 600;
        font-size: 0.9rem;
        color: var(--ink);
        border-bottom: 1px solid var(--rule);
      }

      .spec dd {
        margin: 0;
        padding: 0.7rem 0 0.75rem;
        font-size: 0.9rem;
        color: var(--muted);
        border-bottom: 1px solid var(--rule);
      }

      @media (max-width: 620px) {
        /* Two columns at phone width would leave ~4 words per line in each.
           Stack, and drop the rule between a term and its own definition so the
           pair still reads as one unit. */
        .spec {
          grid-template-columns: 1fr;
        }

        .spec dt {
          border-bottom: 0;
          padding-bottom: 0;
        }
      }

      /* Everything that is real but does not warrant its own section. */
      .caps {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
        gap: 1.6rem 2rem;
        margin-top: 1.9rem;
      }

      .cap h3 {
        margin: 0 0 0.3rem;
        font-size: 0.95rem;
        letter-spacing: -0.005em;
      }

      .cap p {
        margin: 0;
        font-size: 0.875rem;
        color: var(--muted);
      }

      /* Closing band. Sits on the panel tint rather than the page so the page
         has a bottom edge without needing a rule. */
      .closing {
        margin-top: 5.5rem;
        padding: 2.6rem clamp(1.3rem, 4vw, 2.6rem);
        background: var(--panel);
        border: 1px solid var(--rule);
        clip-path: polygon(
          0 0,
          calc(100% - 26px) 0,
          100% 26px,
          100% 100%,
          0 100%
        );
      }

      .closing h2 {
        margin-bottom: 0.7rem;
      }

      .closing p {
        color: var(--ink-2);
        max-width: 56ch;
        margin: 0 0 1.6rem;
      }

      .closing .hero-actions {
        margin-top: 0;
      }

      /* Email capture — the only low-cost action on the page.
         Deliberately sitting with the two CTAs above it rather than in the
         footer: "Read the docs" and "View on GitHub" both ultimately ask for a
         cluster and an afternoon, which is a great deal to ask of someone who
         has been here ninety seconds. This asks for one line.

         The field is --bg-deep rather than --panel because .closing is ALREADY
         --panel; a panel-coloured input inside a panel-coloured card is an
         invisible text box. */
      .signup {
        margin-top: 1.8rem;
        padding-top: 1.4rem;
        border-top: 1px solid var(--rule);
      }

      /* Both of these override the .closing p rule above, which sets a 1.6rem
         bottom margin to space the CTA row — inside the form things sit closer.
         Class-qualified so they win on specificity. */
      .closing .signup p.ask {
        font-weight: 600;
        color: var(--ink);
        font-size: 0.95rem;
        max-width: 52ch;
        margin: 0 0 0.4rem;
      }

      .closing .signup p.hint {
        color: var(--muted);
        font-size: 0.85rem;
        max-width: 52ch;
        margin: 0 0 0.9rem;
      }

      /* The real <label> for the field, kept out of sight because the visible
         copy above it is a <p>. See the form for why that split exists — the
         label text becomes the field's name in Netlify's notification emails,
         so it has to read like a field name and not like a sales pitch.
         Same technique as .foot-social .vh below. */
      .signup .vh {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
      }

      .signup .row {
        display: flex;
        flex-wrap: wrap;
        gap: 0.6rem;
      }

      .signup input[type="email"] {
        flex: 1 1 16rem;
        min-width: 0;
        font-family: inherit;
        font-size: 0.92rem;
        color: var(--ink);
        background: var(--bg-deep);
        border: 1px solid var(--rule-strong);
        border-radius: 9px;
        padding: 0.62rem 0.8rem;
      }

      .signup input[type="email"]::placeholder {
        color: var(--muted);
      }

      .signup input[type="email"]:focus-visible,
      .signup button:focus-visible {
        outline: 2px solid var(--brand-bright);
        outline-offset: 2px;
      }

      /* .btn was written for <a>; a <button> inherits neither the page font nor
         a pointer cursor. */
      .signup button.btn {
        font-family: inherit;
        cursor: pointer;
      }

      /* Honeypot. Positioned off-screen rather than display:none — a bot reading
         the DOM fills it, a person never sees it. aria-hidden keeps it out of the
         accessibility tree, and tabindex="-1" is what makes that safe: an
         aria-hidden subtree containing a focusable control is otherwise a real
         a11y defect, because keyboard focus can land somewhere screen readers
         have been told does not exist. */
      .signup .hp {
        position: absolute;
        left: -9999px;
      }

      /* Pre-1.0 status. Deliberately plain and adjacent to the CTAs rather than
         hidden at the bottom — someone deciding whether to try this needs it
         before they click, not after. */
      .status {
        margin-top: 1.7rem;
        padding-top: 1.3rem;
        border-top: 1px solid var(--rule);
        font-size: 0.85rem;
        color: var(--muted);
        max-width: 62ch;
      }

      .status strong {
        color: var(--ink-2);
        font-weight: 600;
      }

      footer {
        border-top: 1px solid var(--rule);
        margin-top: 4.5rem;
        padding: 2.2rem clamp(1.2rem, 5vw, 3.5rem);
        font-size: 0.85rem;
        color: var(--muted);
      }

      .foot-grid {
        max-width: 68rem;
        margin: 0 auto;
        display: flex;
        flex-wrap: wrap;
        gap: 1.4rem 3rem;
        align-items: baseline;
      }

      .foot-grid nav {
        display: flex;
        flex-wrap: wrap;
        gap: 0.5rem 1.5rem;
      }

      /* Social icons. Drawn as a currentColor mask rather than an <img> so they
         inherit the footer's muted ink and its :hover brightening, instead of
         carrying two baked-in copies. The SVG is an inline data: URI — the site
         CSP is `default-src 'self'`, so anything fetched from a CDN would be
         blocked, and `img-src 'self' data:` is what makes this form legal.

         Each link keeps a visually-hidden text label: the accessible name of an
         icon-only link cannot come from the glyph, and a mask has no alt. */
      .foot-social {
        display: flex;
        align-items: center;
        gap: 1rem;
        align-self: center;
      }

      .foot-social a {
        display: block;
        width: 1.15rem;
        height: 1.15rem;
        background-color: currentColor;
        -webkit-mask: var(--social-icon) center / contain no-repeat;
        mask: var(--social-icon) center / contain no-repeat;
      }

      .foot-social a:hover {
        color: var(--ink);
      }

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

      footer a {
        color: var(--muted);
        text-decoration: none;
      }

      footer a:hover {
        color: var(--ink);
      }

      .foot-legal {
        margin-left: auto;
        display: flex;
        flex-wrap: wrap;
        gap: 0.5rem 1.2rem;
      }
