/* Page composition for every page on the site.
   Sits on top of tokens.css, base.css and components.css: those hold the
   design tokens, the reset and the component layer. What is here is how the
   pages are put together.

   One file rather than one per page: measured over gzip, splitting it cost
   the homepage more than it saved, because six small streams compress worse
   than one larger one. The only visitor a split helps is one who lands on a
   legal page and never sees the homepage. */

/* ======================================================================
   Cross-page: nav offset, anchor clearance, cookie notice
   ====================================================================== */
/* The bar floats on the hero plate rather than sitting on a strip of page
     ground above it. Sticky behaviour is untouched; the negative margin only
     stops the nav reserving flow height, so the plate starts at y=0 and the
     bar overlays it. Page composition, so it stays here and out of the nav
     component — a page whose first section is light still wants the bar in
     flow. The hero pads its content clear of the overlaid bar. */
  .nav { margin-bottom: calc(-1 * (var(--nav-height) + var(--sp-3))); }

  /* The bar overlays the page, so an anchor scrolled flush to the viewport top
     lands underneath it. Every jump target reserves the bar's full height plus
     the same clearance the hero uses, so in-page links stop mid-heading instead
     of mid-nothing. Same expression as the hero's padding, for the same reason:
     retune the bar and both follow. */
  :where(section[id], [id="main"]) {
    scroll-margin-top: calc(var(--sp-3) * 2 + var(--nav-height) + var(--sp-6));
  }

  /* ---- Privacy notice -------------------------------------------------
     Anchored bottom-left rather than stretched across the viewport: a full
     width bar reads as a system alert and covers the first CTA on a phone.
     Above the sticky bar in stacking order, below a modal, so the request
     dialog still wins when both are on screen. */
  .consent {
    position: fixed; z-index: calc(var(--z-sticky) + 1);
    left: var(--sp-4); bottom: var(--sp-4);
    width: min(26rem, calc(100% - var(--sp-8)));
    display: grid; gap: var(--sp-4);
    padding: var(--sp-5);
    background: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-3);
  }
  .consent[hidden] { display: none; }
  .consent__title { font-size: var(--fs-body-lg); font-weight: var(--fw-semibold); }
  .consent__body {
    margin-top: var(--sp-2); color: var(--fg-secondary);
    font-size: var(--fs-body-sm); line-height: var(--lh-relaxed);
  }
  .consent__body a { color: var(--fg-brand); text-decoration: underline; text-underline-offset: 2px; }
  .consent__actions { display: flex; gap: var(--sp-3); justify-content: flex-end; }
  @media (max-width: 30rem) {
    .consent__actions { flex-direction: column-reverse; }
    .consent__actions .btn { width: 100%; }
  }
  /* Entrance only, and only once — it appears on first visit, never again. */
  @media (prefers-reduced-motion: no-preference) {
    .consent { animation: consent-in var(--dur-slow) var(--ease-out) both; }
  }
  @keyframes consent-in { from { opacity: 0; transform: translateY(12px); } }

/* ======================================================================
   Legal documents: privacy.html and terms.html
   ====================================================================== */
  /* ---- Legal document ------------------------------------------------
     One column, no cards, no panels. A policy is read top to bottom and
     cited by section, so it is set as a document: a measure that stays
     inside the readable range, headings that cut the scroll into the
     sections the text itself names, and hairlines where the source breaks.
     Top padding clears the overlaid bar, same expression as the hero. */
  .doc { padding-block: calc(var(--sp-3) * 2 + var(--nav-height) + var(--sp-12)) var(--sp-20); }
  /* Two widths, not one. The shell is wide so the title, the rules and the
     contents index have room and the page does not read as a narrow ribbon
     on a desktop screen; the prose inside is capped separately, because a
     72-character line is a readability limit and not a layout choice.
     Headings and hairlines run the full shell, which is what makes the
     document feel wide even though nothing you read is. */
  .doc__inner { max-width: 52rem; margin-inline: auto; }
  .doc p, .doc li, .doc__lede { max-width: 68ch; }
  .doc__eyebrow {
    font-size: var(--fs-body-sm); color: var(--fg-muted);
    letter-spacing: var(--ls-wide); text-transform: uppercase;
  }
  .doc__title {
    margin-top: var(--sp-3);
    font-size: clamp(2rem, 1.3rem + 2.2vw, 3rem);
    font-weight: var(--fw-semibold); letter-spacing: var(--ls-tight);
    text-wrap: balance;
  }
  .doc__meta {
    margin-top: var(--sp-4); padding-bottom: var(--sp-8);
    border-bottom: 1px solid var(--border-subtle);
    color: var(--fg-secondary); font-size: var(--fs-body-sm);
  }
  .doc__lede {
    margin-top: var(--sp-8); color: var(--fg-body);
    font-size: var(--fs-body-lg); line-height: var(--lh-relaxed);
    text-wrap: pretty;
  }
  .doc h2 {
    margin-top: var(--sp-12); padding-top: var(--sp-8);
    border-top: 1px solid var(--border-subtle);
    font-size: var(--fs-h3); font-weight: var(--fw-semibold);
    letter-spacing: var(--ls-snug);
    scroll-margin-top: calc(var(--sp-3) * 2 + var(--nav-height) + var(--sp-6));
  }
  .doc p  { margin-top: var(--sp-4); color: var(--fg-body); line-height: var(--lh-relaxed); }
  .doc ul { margin-top: var(--sp-4); padding-left: var(--sp-5); display: grid; gap: var(--sp-3); }
  .doc li { color: var(--fg-body); line-height: var(--lh-relaxed); }
  .doc li::marker { color: var(--fg-brand); }
  .doc a  { color: var(--fg-brand); text-decoration: underline; text-underline-offset: 2px; }
  .doc strong { font-weight: var(--fw-semibold); color: var(--fg-default); }

  /* A policy is navigated by section far more than read straight through,
     so the headings double as the index. */
  .doc__toc { margin-top: var(--sp-8); }
  .doc__toc ol {
    margin-top: var(--sp-4); padding: 0; list-style: none;
    columns: 2; column-gap: var(--sp-8); counter-reset: toc;
  }
  @media (max-width: 40rem) { .doc__toc ol { columns: 1; } }
  .doc__toc li { break-inside: avoid; padding-block: var(--sp-2); counter-increment: toc; }
  .doc__toc a { text-decoration: none; color: var(--fg-body); font-size: var(--fs-body-sm); }
  .doc__toc a::before { content: counter(toc) ". "; color: var(--fg-muted); }
  .doc__toc a:hover { color: var(--fg-brand); text-decoration: underline; }

  .doc__contact { margin-top: var(--sp-4); display: grid; gap: var(--sp-2); }

/* ======================================================================
   Homepage
   ====================================================================== */
/* Page-level composition only. Everything visual comes from tokens. */
  /* Dark hero on the brand plate.
     Ground is pinned left: that is the darkest region of the artwork, so a
     narrow viewport crops toward safety rather than toward the bright patch
     at 58% width. Measured white contrast inside the text column: 5.25:1
     worst case (lead line), 5.40:1 under the headline. The flat colour is
     the artwork's own average, so the hero stays legible before it loads. */

  .hero {
    position: relative; isolation: isolate; overflow: hidden;
    background-color: #376973;
    min-height: 32rem;
    /* Written as the bar's real geometry rather than a round number: the
       frame's sticky offset plus its own padding put the bar's underside at
       sp-3*2 + nav-height, and sp-10 is the clearance below it. Retune the
       nav and this follows instead of silently touching again. */
    padding-top: calc(var(--sp-3) * 2 + var(--nav-height) + var(--sp-10));
    display: flex; align-items: center;
  }
  /* Same crop the background shorthand did: slice == cover, xMin == left. */
  .hero__art { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; }
  .hero > .container { position: relative; z-index: 1; }

  /* The two marks drift on their own long, non-harmonic cycles, so they never
     fall into step and the eye reads a slow breath rather than a loop. The
     amplitude is a dozen user units against a 1280-wide plate: noticed only
     when the page is still, never while reading. Transform only, so this is
     compositor work and the hero text stays sharp. */
  @media (prefers-reduced-motion: no-preference) {
    .hero__mark { animation: hero-drift 13s var(--ease-in-out) infinite; }
    .hero__mark--b { animation-duration: 17s; animation-delay: -6s; }
  }
  @keyframes hero-drift {
    0%, 100% { transform: translate(0, 0); }
    50%      { transform: translate(-5px, -14px); }
  }
  /* The text column must not reach the bright patch. 57% is the ceiling, not
     a taste call: the left mark's box starts at 814px on a 1440 viewport and
     the column's right edge lands at 801px. Past this, white body copy sits
     on the lit strokes and drops under 4.5:1. */
  .hero .container > * { max-width: 57%; }
  @media (max-width: 900px) { .hero .container > * { max-width: 100%; } .hero { min-height: 26rem; } }

  .hero__title { color: #FFFFFF; font-weight: var(--fw-regular); }
  /* Emphasis by weight, not colour. On a teal plate every lighter teal loses
     its hue against white and reads as dimmed text, which de-emphasises the
     one phrase that carries the pitch. Weight works on any ground. */
  .hero__title em { font-style: normal; font-weight: var(--fw-bold); }
  /* One phrase, not the whole headline: "Your keys." is the clause that
     carries the pitch, so it is the only thing that moves. A clip-path wipe
     rather than a fade or a slide — the edge travels like a plotter drawing
     the line, which suits an infrastructure brand better than something
     springy, and it moves no pixels around it.
     The animation lives entirely inside the reduced-motion query: with motion
     reduced there is no clip-path at all and the phrase is simply there, so
     the visible default never depends on an animation running. inline-block
     gives the clip one box to work on; as a plain inline it would span line
     boxes and clip against their union. The -6% right inset is slack for the
     full stop's overhang. */
  @media (prefers-reduced-motion: no-preference) {
    .hero__title em {
      display: inline-block;
      animation: keys-wipe 620ms var(--ease-out) 240ms both;
    }
    @keyframes keys-wipe {
      from { clip-path: inset(-10% 100% -10% 0); }
      to   { clip-path: inset(-10% -6% -10% 0); }
    }
  }
  .hero__sub { color: var(--c-neutral-200); }     /* 4.72:1 */
  /* Teal-300, chosen for hue over ratio. Sampling the artwork under this line
     puts its lightest pixel at rgb(52,117,125), which leaves 3.13:1 at
     16px/500 — under the 4.5:1 AA floor for body text. It is the eyebrow, not
     copy that carries meaning on its own, and the same sentence is the page
     title and the meta description. Deeper tints on this ramp all read as
     white on a teal plate; this is where the colour starts to be visible. */
  .hero__lead { color: var(--c-teal-300); }       /* 3.13:1 — see note */

  .sec-head { max-width: 46ch; }
  /* Fluid so the middle of the hierarchy keeps presence on wide viewports.
     Ceiling stays under the hero's new 68px cap. */
  .sec-head { --sec-head-size: clamp(2rem, 1.3rem + 2.2vw, 3rem); }
  .sec-head__title {
    font-size: var(--sec-head-size, clamp(2rem, 1.3rem + 2.2vw, 3rem));
    font-weight: var(--fw-bold);
    letter-spacing: var(--ls-tight);
  }
  .sec-head__sub { margin-top: var(--sp-3); color: var(--fg-secondary); }

  /* Editorial rows on hairlines, not a card grid. Four capabilities are
     not a sequence, so they carry no numbers and no accent stripe. */
  .capabilities {
    display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
    column-gap: var(--sp-12); margin-top: var(--sp-8);
  }
  .capability { padding-block: var(--sp-6); border-top: 1px solid var(--border-subtle); }
  /* The top row leads the stack, so it carries no hairline above it and sits
     tight under the section head. */
  .capability:nth-child(-n+2) { border-top: 0; padding-top: 0; }
  .capability h3 { font-size: var(--fs-h3); }
  .capability p { margin-top: var(--sp-2); color: var(--fg-secondary); max-width: 46ch; }
  @media (max-width: 780px) {
    .capabilities { grid-template-columns: minmax(0,1fr); }
    /* One column: only the first row leads, the second gets its hairline back. */
    .capability:nth-child(2) { border-top: 1px solid var(--border-subtle); padding-top: var(--sp-6); }
  }

  .hero__lead {
    font-size: var(--fs-body); font-weight: var(--fw-medium);
    margin-bottom: var(--sp-4);
  }

  /* Darker end of the brand gradient: white body copy needs to clear 4.5:1
     across the whole band, and teal-500 only carries 3.4:1. */
  .cta-band {
    /* Three layers instead of a flat two-stop ramp: light gathers in the
       top-right, weight settles bottom-left, and the diagonal underneath
       still carries the brand. The lightest point composites to #38727B —
       5.4:1 against white, so body copy stays AA anywhere on the band. */
    background:
      radial-gradient(105% 130% at 86% 4%,   rgba(111,182,187,.30), transparent 58%),
      radial-gradient( 85% 115% at 2% 100%,  rgba(3,42,52,.55),     transparent 62%),
      linear-gradient(135deg, #2E7178 0%, #16404C 100%);
    border-radius: var(--radius-2xl);
    padding: var(--sp-12);
    display: grid; justify-items: center; text-align: center;
    gap: var(--sp-5);
    position: relative; overflow: hidden; isolation: isolate;
  }
  /* Centred copy only holds together when the rag is even, so let the
     browser balance both blocks rather than pinning a max-width that is
     right at one size and wrong at the next. */
  .cta-band__title { text-wrap: balance; max-width: 22ch; }
  .cta-band__lede { color: var(--fg-on-brand-muted); max-width: 56ch; text-wrap: pretty; }
  .cta-band .btn { margin-top: var(--sp-3); }
  @media (max-width: 700px) { .cta-band { padding: var(--sp-8); border-radius: var(--radius-xl); } }

  /* A wrapping flex row right-aligns each line on its own, so stacked
     buttons end up ragged. Below the split breakpoint they go full-width. */
  /* Stacking is for when the pair genuinely cannot share a row. Two
     full-width bars of equal size read as two equal choices, which loses
     the primary. Hold them side by side as long as they fit. */
  @media (max-width: 560px) {
    .cta-band .btn, .hero__actions .btn { width: 100%; }

    /* The plate's usable line is the viewport less the container gutters and
       the card's own padding — around 260px on a small phone. Two things were
       wider than that and, because a grid column cannot be narrower than its
       widest item's minimum, each one pushed the whole column past the card's
       right padding and dragged the heading, the lede and the button out with
       it. Hence the same three elements all ending 45px too far right.

       First: the h2 floor is 2rem, sized for a full-width section head, and at
       that size the word "architecture," alone is wider than the line. */
    .cta-band { padding: var(--sp-6); }
    .cta-band__title { font-size: clamp(1.25rem, 0.25rem + 5vw, 2rem); }

    /* Second: white-space: nowrap made the button's entire one-line width its
       minimum. Let the label wrap, and let the button grow to hold it rather
       than keeping a fixed height the second line would spill out of. */
    .cta-band .btn {
      white-space: normal;
      height: auto; min-height: var(--btn-height-md);
      padding-block: var(--sp-3);
      /* The bar is full-width here, so the wide side padding buys nothing and
         only costs the label lines. */
      padding-inline: var(--sp-4);
    }
  }

  /* ---- Request dialog -------------------------------------------------
     Composed from the system's own primitives — .modal, .modal__actions,
     .field, .input, .textarea, .checkbox, .btn — so this is one more use of
     the form language, not a second one. Nothing here is decorative: the
     surface is the white card the evidence panel already uses, the only rule
     is the same hairline, and the only colour is the contract button that
     carries this action everywhere else on the page. */
  /* The second term is the phone case: .modal's width:100% plus the UA's auto
     margins left a 3px gutter at 375px, which reads as a broken full-screen
     sheet rather than a dialog. Capping against the viewport restores it. */
  .request { max-width: min(34rem, 100% - var(--sp-8)); padding: var(--sp-8); }
  @media (max-width: 30rem) { .request { padding: var(--sp-6); } }

  .request__head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--sp-4);
  }
  /* Body size, not label size. At 14px this sat on exactly the same step as
     the field labels in the same secondary grey, so prose and chrome read as
     one tier. 16px separates them and matches .sec-head__sub, which is how a
     lede under a heading is already set everywhere else on the page.
     Tiers here, largest down: title 22 / lede 16 / label 14 / hint 13. */
  /* No max-width: the dialog's own content box is already inside the readable
     range (~55ch at 16px), so capping at 46ch only made the lede rag short
     against full-width fields below it. */
  .request__lede {
    margin-top: var(--sp-2); color: var(--fg-secondary);
    font-size: var(--fs-body); line-height: var(--lh-relaxed);
  }
  /* .btn--icon supplies the box; this only pulls it back into the corner so
     its optical edge lines up with the title rather than its padding. */
  .request__close { margin: calc(var(--sp-2) * -1) calc(var(--sp-3) * -1) 0 0; }
  @media (pointer: coarse) {
    .request__close { width: 44px; height: var(--btn-height-lg); }
  }

  /* ---- Nutshell embed --------------------------------------------------
     The embed renders plain DOM into our container: no shadow root, and the
     only iframe is hCaptcha's invisible one. So the form styling the site
     already had can be put back on it, instead of living with Nutshell's look.

     Everything here is matched on structure and input type, never on
     Nutshell's own class names -- those are emotion hashes (css-s998u6,
     css-1ub0j67) regenerated whenever they rebuild, so a stylesheet written
     against them would silently fall off at their next deploy.

     The values are not reinvented: each rule mirrors the component it stands
     in for -- .input, .field__label, .field__hint, .checkbox, .btn. */
  .request__embed { margin-top: var(--sp-6); }
  /* The action row already carries a rule above it; the legal line needs room
     of its own below the button, or the two read as one block.

     `~` and not `+`: the two Nutshell <script> tags sit between the embed and
     this paragraph, and they are element siblings, so an adjacent selector
     never matches. */
  .request__embed ~ .request__legal { margin-top: var(--sp-8); }
  /* Nutshell writes width/height:100% inline on our own container. */
  .request__embed { height: auto !important; }

  /* Their wrapper and form carry a ground and padding of their own. */
  .request__embed > div,
  .request__embed form { background: none; padding: 0; margin: 0; }

  /* The field stack, at the rhythm .request__form used. */
  .request__embed form > div { display: grid; gap: var(--sp-5); }

  /* Name and company share a row again, as the hand-built form had them: both
     are short and read as one line of identification. Nutshell emits every
     field as its own wrapper in source order, so the pairing is done here --
     two columns, everything spanning both, except the first two.

     Same 30rem breakpoint .request__row used, and for the same reason: below
     it the dialog is too narrow to keep both labels and both values legible. */
  @media (min-width: 30rem) {
    .request__embed form > div { grid-template-columns: 1fr 1fr; }
    .request__embed form > div > * { grid-column: 1 / -1; }
    .request__embed form > div > *:nth-child(1),
    .request__embed form > div > *:nth-child(2) { grid-column: auto; }
  }
  /* Nutshell spaces the stack itself, with
     `.css-29pv05.css-29pv05 > :not(:last-child):not(.hidden-field)` --
     margin-bottom: 1.8em on every row. That lands on top of the grid gap, so
     fields sat 48px apart instead of 20px.

     Its specificity is 0-4-0: the class written twice, plus a class inside
     :not(). Nothing sane on our side reaches that, hence !important. Only the
     bottom margin is forced -- the hint below still needs its negative top
     margin to work. */
  .request__embed form > div > div { margin: 0; }
  .request__embed form > div > * { margin-bottom: 0 !important; }

  /* The hint belongs to the field above it, but Nutshell gives it a wrapper of
     its own, which the grid then spaces as a separate row. Pull it back to the
     sp-2 .field__hint used. */
  .request__embed form > div > div:has(> p) { margin-top: calc(var(--sp-2) - var(--sp-5)); }

  /* hCaptcha runs invisible here, but its wrapper is still a grid item and
     takes a whole row of gap. display:contents removes the box without hiding
     anything -- so if the widget ever becomes visible it still renders, which
     display:none would have silently broken. */
  .request__embed form > div > div:has(> iframe[data-hcaptcha-widget-id]) { display: contents; }

  /* Field labels carry `for`; the consent checkbox label does not. */
  .request__embed label[for] {
    display: block; margin-bottom: var(--sp-2);
    font-size: var(--fs-body-sm); font-weight: var(--fw-semibold);
    color: var(--fg-default); letter-spacing: var(--ls-snug);
  }
  .request__embed label[for] span { color: var(--fg-danger); margin-left: 2px; }

  /* aria-hidden excludes the submission-id field Nutshell keeps in the form:
     it is a real text input, and dressing it up would leave a stray empty box
     sitting under the button. */
  .request__embed input[type="text"]:not([aria-hidden="true"]),
  .request__embed input[type="email"],
  .request__embed textarea {
    width: 100%; min-height: var(--control-height);
    padding: 0 var(--sp-5);
    background: var(--bg-surface-alt);
    /* Nutshell now themes the fields too -- border, radius, colour and
       font-size -- via `form #form-fields-container .css-X.css-X`. The id
       puts it out of reach, so these four are forced. The border is kept,
       not dropped: without it the field loses its edge against the dialog. */
    border: var(--border-thin) solid var(--border-subtle) !important;
    border-radius: var(--radius-lg) !important;
    color: var(--fg-default) !important;
    font-family: inherit;
    font-size: var(--fs-body) !important; line-height: var(--lh-normal);
    transition: border-color var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out),
                background var(--dur-fast) var(--ease-out);
  }
  .request__embed textarea {
    padding: var(--sp-4) var(--sp-5); min-height: 8rem;
    resize: vertical; line-height: var(--lh-relaxed);
  }
  .request__embed input::placeholder,
  .request__embed textarea::placeholder { color: var(--fg-placeholder); opacity: 1; }
  .request__embed input[type="text"]:not([aria-hidden="true"]):hover,
  .request__embed input[type="email"]:hover,
  .request__embed textarea:hover { border-color: var(--border-default) !important; }
  .request__embed input[type="text"]:not([aria-hidden="true"]):focus,
  .request__embed input[type="email"]:focus,
  .request__embed textarea:focus {
    outline: none; background: var(--bg-surface);
    border-color: var(--c-teal-500) !important; box-shadow: var(--focus-ring);
  }

  /* The one loose <p> in the stack is the hint under the notes field. */
  .request__embed p {
    margin: 0; font-size: var(--fs-caption); color: var(--fg-muted);
  }

  /* Consent group. The sentence is a <legend>, so it takes the checkbox text
     treatment rather than a field label's. */
  .request__embed fieldset { border: 0; margin: 0; padding: 0; }
  .request__embed legend {
    padding: 0; margin-bottom: var(--sp-3);
    font-size: var(--fs-body-sm); line-height: var(--lh-normal);
    color: var(--fg-body); font-weight: var(--fw-regular);
  }
  .request__embed fieldset label {
    display: flex; align-items: flex-start; gap: var(--sp-3); cursor: pointer;
    font-size: var(--fs-body-sm); line-height: var(--lh-normal); color: var(--fg-body);
  }
  /* Nutshell ships its controls as `form #form-fields-container .css-X.css-X`
     -- an id plus its own class written twice, specificity 1-2-1, done that way
     precisely to outrank whatever the host page has. Matching it would only
     tie, and a tie loses here: emotion injects its sheet at runtime, after
     ours. Hence !important, and only on the properties Nutshell actually sets.

     This is also why the form's own theme settings now fight us: the border
     radius set over there lands on every control, which is what turned the
     checkbox into a circle. */
  .request__embed input[type="checkbox"] {
    appearance: none !important;
    margin: 0; flex: none;
    width: 18px !important; height: 18px !important; margin-top: 2px;
    border: var(--border-thin) solid var(--border-strong) !important;
    border-radius: var(--radius-xs) !important;
    background-color: var(--bg-surface) !important;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out);
  }
  .request__embed input[type="checkbox"]:checked {
    background-color: var(--c-teal-700) !important;
    border-color: var(--c-teal-700) !important;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10' viewBox='0 0 12 10'%3E%3Cpath d='M1 5 4.4 8.4 11 1.6' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: center;
  }
  .request__embed input[type="checkbox"]:focus-visible {
    outline: none; box-shadow: var(--focus-ring);
  }

  /* Submit. An <input>, not a <button>, so the flex centring .btn relies on
     does not apply -- the value is centred with text-align instead.

     Sized to .btn--lg rather than the default: this is the one action the
     dialog exists for, and at the md height it read as a minor control beside
     the field stack. Same 48px / sp-8 / fs-body-lg the page CTA uses.

     Height and padding are forced too. Nutshell does not list them on the
     button rule itself, but something in their sheet still reaches the
     padding -- measured 8px against the 32px asked for -- so both are pinned
     rather than left to lose again. */
  .request__embed input[type="submit"] {
    appearance: none !important;
    width: auto !important; min-width: 0 !important; min-height: 0;
    height: var(--btn-height-lg) !important;
    padding-inline: var(--sp-8) !important;
    border: var(--border-thin) solid transparent !important;
    border-radius: var(--radius-md) !important;
    background-color: var(--btn-primary-bg) !important;
    color: var(--btn-primary-fg) !important;
    box-shadow: var(--btn-primary-shadow, none);
    font-family: var(--font-action) !important;
    font-size: var(--fs-body-lg) !important;
    font-weight: var(--btn-fw) !important;
    letter-spacing: var(--ls-snug); text-align: center;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease-out);
  }
  .request__embed input[type="submit"]:hover {
    background-color: var(--btn-primary-bg-hover) !important;
  }
  .request__embed input[type="submit"]:focus-visible {
    outline: none; box-shadow: var(--focus-ring);
  }

  /* The rule above the action row, as .request__actions drew it. :has() is the
     only way to reach that wrapper without naming a hashed class; where it is
     unsupported the row just loses its rule and nothing else changes. */
  .request__embed div:has(> input[type="submit"]) {
    margin-top: var(--sp-2); padding-top: var(--sp-5);
    border-top: 1px solid var(--border-subtle);
    display: flex; justify-content: flex-end;
  }
  @media (max-width: 30rem) {
    .request__embed input[type="submit"] { width: 100%; }
  }

  /* Kept rather than deleted: the hand-built form is one revert away while the
     embed is being evaluated, and it needs these back. */

  .request__form { display: grid; gap: var(--sp-5); margin-top: var(--sp-6); }
  /* Name and company are short and read as one line of identification, so they
     share a row until the dialog is too narrow to keep both legible. */
  .request__row { display: grid; gap: var(--sp-5); }
  @media (min-width: 30rem) { .request__row { grid-template-columns: 1fr 1fr; } }

  .request__consent { display: grid; gap: var(--sp-3); }
  /* The consent line is long and sits next to a 20px box, so it needs the
     tighter leading of small text, not body leading, to stay one block. */
  .request__consent .checkbox__text {
    font-size: var(--fs-body-sm); line-height: var(--lh-normal);
    color: var(--fg-body);
  }
  .request__consent.is-invalid .checkbox input { border-color: var(--c-danger-600); }

  .request__legal {
    font-size: var(--fs-caption); line-height: var(--lh-relaxed);
    color: var(--fg-muted); max-width: 52ch;
  }
  .request__legal a { color: var(--fg-brand); text-decoration: underline; text-underline-offset: 2px; }

  /* .modal__actions gives the row; the rule and the stacking are what this
     dialog adds, because its actions close a long form rather than a short
     confirmation. */
  .request__actions {
    margin-top: var(--sp-2); padding-top: var(--sp-5);
    border-top: 1px solid var(--border-subtle);
  }
  @media (max-width: 30rem) {
    .request__actions { flex-direction: column-reverse; }
    .request__actions .btn { width: 100%; }
  }

  .request__done { margin-top: var(--sp-6); }
  .request__doneTitle { font-size: var(--fs-body-lg); font-weight: var(--fw-semibold); }
  .request__doneBody {
    margin-top: var(--sp-2); color: var(--fg-secondary);
    font-size: var(--fs-body-sm); line-height: var(--lh-relaxed); max-width: 44ch;
  }

  /* ---- Mark ------------------------------------------------------------
     Permanent, not hover-revealed: a hover-only reveal is invisible to any
     touch visitor and to a static screenshot alike, which made it read as
     simply missing rather than as a subtle detail. Baked into the plate the
     same way .ownership__mark is baked into its section — always there,
     behind the copy, out of the pointer path.
     Sized by height, not width: at a fixed width the mark either bled off
     the band's edge (clipped by overflow:hidden, reading as a shape chopped
     in half) or sat shrunk in the middle of its own clear zone with dead
     padding on every side. Height keyed to the band's own height and
     centred vertically has no top/bottom margin to leave empty, and stays
     inside the box at any band size — nothing to clip. This mark carries
     its own teal gradient, so unlike a white watermark it needs real
     opacity to separate from the plate; it stays off the text by
     clearance, not by being faint. */
  .cta-band__mark {
    position: absolute; z-index: -1; pointer-events: none;
    top: 50%; right: 2%;
    height: 116%; width: auto;
    opacity: 0;
    transform: translate(2.5rem, -50%);
    /* Both properties on one clock: at 480ms opacity against 760ms transform
       the fade finished while the mark was still sliding, so it read as two
       events rather than one arrival. Exit is the base rule and enter is the
       :hover rule, so the pair below also gives the exit its own faster
       timing — the system responding should always outrun the user deciding. */
    transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out);
  }
  @media (hover: hover) and (pointer: fine) {
    .cta-band:hover .cta-band__mark {
      opacity: .8; transform: translate(0, -50%);
      transition-duration: 260ms;
    }
  }
  /* Below this the centred copy's widest line reaches the mark's column;
     hide it before the two touch rather than shrink it back down. */
  @media (max-width: 1180px) { .cta-band__mark { display: none; } }
  @media (prefers-reduced-motion: reduce) {
    .cta-band__mark { transform: translate(0, -50%); transition: opacity 200ms linear; }
  }

  /* ---- Tenancy / Ownership ----------------------------------------------
     A labelled boundary, not an illustration: the source sketch drew a
     literal roof over the asset list, but the actual information is "these
     eight things live inside the one boundary named below" — a bordered
     group with its label cut into the top edge (the classic fieldset/legend
     trick) says that directly, in the page's own materials, rather than as
     a separate custom diagram. */
  .tenancy {
    position: relative;
    margin-top: var(--sp-10);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-2xl);
    padding: var(--sp-10) var(--sp-8) var(--sp-8);
  }
  .tenancy__label {
    position: absolute; top: 0; left: var(--sp-8); transform: translateY(-50%);
    margin: 0; padding-inline: var(--sp-3);
    background: var(--bg-canvas);
    font-size: var(--fs-body-sm); font-weight: var(--fw-semibold);
    letter-spacing: var(--ls-wide); text-transform: uppercase;
    color: var(--fg-brand);
  }
  .tenancy__grid {
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--sp-3);
  }
  @media (max-width: 780px) { .tenancy__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
  /* Grid cells stretch to the tallest in their row. When a longer label wraps
     to two lines its neighbour's chip grows with it, and a single line of text
     left in a taller box sits at the top of it. Centring the content puts that
     line on the chip's own middle instead. */
  .tenancy__grid li {
    background: var(--bg-brand-soft); color: var(--fg-brand);
    border-radius: var(--radius-lg);
    padding: var(--sp-3) var(--sp-4);
    display: grid; place-items: center;
    text-align: center;
    font-size: var(--fs-body-sm); font-weight: var(--fw-medium);
  }

  /* ---- Awards ----------------------------------------------------------
     Light section, full-bleed like #platform above it. A horizontal
     timeline rather than a stacked list: three real, dated results read
     better as a rail with a marker per year than as three rows that only
     differ by a label at the front. The rail is a single shared border on
     the list; each item's dot is pulled up onto it with a negative margin
     so it appears to sit ON the line rather than floating near it.
     Numerals are solid brand teal, not the gradient-outline treatment a
     reference example used — gradient text is decorative-only and already
     off-limits on this page; colour here still does the same job of
     making the year the thing the eye lands on first. */
  /* Head and seal share the top row and the timeline spans the full width
     beneath them, so the badge fills the space the three dates leave empty
     instead of hanging off the bottom of the plate. */
  .awards {
    display: grid;
    grid-template-columns: minmax(0,1fr) clamp(15rem, 24vw, 20rem);
    column-gap: var(--sp-10);
    align-items: start;
  }
  .awards__intro { max-width: 60ch; }
  .awards__list {
    grid-column: 1 / -1;
    position: relative;
    /* Clears the seal card, not the intro text: the card is a filled plate
       and the row's tallest element, so the gap that reads as generous under
       a line of type reads as a collision under it. */
    margin: var(--sp-16) 0 0; list-style: none; padding: 0;
    display: grid; grid-template-columns: repeat(3, minmax(0,1fr));
    gap: var(--sp-8);
  }
  /* The rail is a direction, not just a divider: an arrowhead at the far end
     says the dates run oldest to newest.

     Line and head are cut from ONE box rather than drawn as a border plus a
     triangle on top of it. Both are translucent on this ground, so anywhere
     they overlapped the two alphas composited to roughly double the opacity
     and the line showed straight through the head as a brighter band. A
     single fill cannot overlap itself, at any width, in either orientation.

     Geometry: the box is 14px tall, the line occupies y 6–8, the head spans
     the last 14px and tapers to a point at the box's right edge, mid-height. */
  .awards__list::after {
    content: ""; position: absolute;
    left: 0; right: 0; top: -7px; height: 14px;
    background: var(--border-default);
    clip-path: polygon(
      0 6px,
      calc(100% - 14px) 6px,
      calc(100% - 14px) 0,
      100% 7px,
      calc(100% - 14px) 14px,
      calc(100% - 14px) 8px,
      0 8px
    );
  }
  .awards__item::before {
    content: ""; display: block;
    width: 22px; height: 22px; margin-top: -11px; margin-bottom: var(--sp-6);
    background: var(--fg-brand); border-radius: 50%;
  }
  /* Below the row breakpoint the rail turns vertical instead of disappearing:
     a running border-left on the list, with each dot pulled onto it via
     negative margin — the same "pull the marker onto the shared line" move
     the desktop rail uses, rotated 90°. Content sits in the remaining
     column to the dot's right rather than stacking under it.
     This block must come AFTER the base ::before rule above: both target
     the identical selector at identical specificity, so whichever is later
     in source wins per property — written first, its margin reset was
     silently overruled by the base rule's margin-top/-bottom every time
     both matched, which is what was throwing the dot's vertical centring
     off against the year next to it. */
  @media (max-width: 900px) {
    .awards { grid-template-columns: minmax(0,1fr); }
    .awards__list {
      grid-template-columns: minmax(0,1fr); gap: var(--sp-8);
      /* Stacked, the rail follows the intro text and the card has moved to
         the bottom, so the wider clearance above has nothing left to clear. */
      margin-top: var(--sp-10);
      /* The dots overhang the rail by 11px, so the rail needs at least that
         much clearance from the container edge to keep them off it. */
      margin-left: 12px;
    }
    /* The rail runs vertically here — same single shape, turned a quarter. */
    .awards__list::after {
      left: -7px; right: auto; top: 0; bottom: 0;
      width: 14px; height: auto;
      clip-path: polygon(
        6px 0,
        8px 0,
        8px calc(100% - 14px),
        14px calc(100% - 14px),
        7px 100%,
        0 calc(100% - 14px),
        6px calc(100% - 14px)
      );
    }
    .awards__item {
      display: grid; grid-template-columns: 22px 1fr; column-gap: var(--sp-4);
      align-items: start;
    }
    .awards__item::before {
      grid-row: 1; align-self: center; margin: 0 0 0 -11px;
    }
    .awards__year { grid-column: 2; }
    .awards__item p { grid-column: 2; }
  }
  /* Sized well past body text on purpose: a timeline's dates are read as a
     sequence first and sentences second, so each numeral has to work at a
     glance, not just carry slightly more weight than the line beside it. */
  .awards__year {
    display: block;
    font-size: clamp(1.75rem, 1.45rem + 1.2vw, 2.25rem); line-height: 1;
    font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
    font-weight: var(--fw-bold); letter-spacing: var(--ls-tight);
    color: var(--fg-brand);
  }
  .awards__item p { margin-top: var(--sp-5); color: var(--fg-body); line-height: var(--lh-relaxed); }
  .awards__item a { color: var(--fg-brand); text-decoration: underline; text-underline-offset: 2px; }

  /* Top-right of the plate, level with the section head — the timeline
     below spans the full width, so this is the only room the badge can take
     without standing up as a fourth column against the dates. */
  /* Same three-layer gradient the CTA band uses further down — a shade
     lighter than this section's flat petrol-800, so the card reads as its
     own object sitting on the ground rather than a seam in it. */
  .awards__seal-card {
    grid-column: 2; grid-row: 1;
    margin: 0;
    width: 100%;
    /* --sp-7 doesn't exist on this scale (6 -> 8) — that's an invalid custom
       property with no fallback, which drops the whole padding declaration
       at computed-value time, so the card was rendering with no padding at
       all rather than the tight-but-present gap it looked like. */
    padding: var(--sp-8);
    border-radius: var(--radius-xl);
    background:
      radial-gradient(120% 160% at 90% 0%, rgba(111,182,187,.28), transparent 60%),
      linear-gradient(135deg, #2E7178 0%, #16404C 100%);
  }
  .awards__seal { display: block; width: 100%; height: auto; }
  /* Must follow the rule above rather than join the earlier media block:
     equal specificity means source order decides, and a stale grid-column: 2
     in a one-column grid opens an implicit second column that crushes the
     head next to it. Stacked, the badge closes the section instead of
     wedging itself between the intro and the dates. */
  @media (max-width: 900px) {
    .awards__seal-card {
      grid-column: 1; grid-row: auto; order: 1;
      margin-top: var(--sp-10); max-width: 24rem;
    }
  }

  /* On-dark: the section now carries the same petrol band the commitments
     section above it does. Everything below has explicit light-mode colours
     (brand teal, fg-body, a light hairline) rather than inherited `color`,
     so .on-dark's own cascade doesn't reach them — each needs its own
     dark-ground value. The dot's ring switches from a white cutout to a
     petrol one, since white would float as its own bright shape rather than
     reading as a gap in the line. */
  .on-dark .awards__list::after { background: rgba(255,255,255,.16); }
  .on-dark .awards__year { color: var(--c-teal-300); }
  .on-dark .awards__item::before { background: var(--c-teal-300); }
  .on-dark .awards__item p { color: rgba(255,255,255,.78); }
  .on-dark .awards__item a { color: var(--c-teal-300); text-decoration-color: rgba(165,207,211,.5); }
  .on-dark .awards__intro { color: rgba(255,255,255,.72); }

  /* ---- Network Operator Hub ---------------------------------------------
     Copy vertically centred against the figure rather than top-aligned: the
     artwork is a near-square with its subject in the middle, so aligning to
     its top edge would leave the sentence floating against the globe's empty
     upper curve. */
  .hub__split { align-items: center; gap: var(--sp-16); }
  .hub__copy .sec-head__sub { max-width: 46ch; }
  /* Square box so the mark can be centred against the artwork's own centre
     with a plain inset, no magic offsets: the SVG is 557x563, near enough to
     1:1 that a square frame crops nothing. */
  .hub__figure { position: relative; aspect-ratio: 1; margin-inline: auto; width: 100%; max-width: 30rem; }
  .hub__globe { display: block; width: 100%; height: 100%; object-fit: contain; }
  @media (max-width: 860px) { .hub__figure { max-width: 22rem; } }

  /* ---------- Motion -------------------------------------------------
     The pre-animation state is applied only when JS is running (.js on
     <html>), so a headless renderer, a crawler or a failed script gets the
     page fully visible instead of a blank column. Reduced motion opts out
     of the whole thing rather than shortening it. */
  @media (prefers-reduced-motion: no-preference) {
    .js .reveal { opacity: 0; transform: translateY(14px); }
    .js .reveal.is-in {
      opacity: 1; transform: none;
      transition: opacity 320ms var(--ease-out), transform 320ms var(--ease-out);
      transition-delay: calc(var(--i, 0) * 45ms);
    }
  }

  /* ---------- Commitment cards ---------------------------------------
     Four equal boxes with icon + heading + text is the stock grid, so the
     interest has to come from the surface: a lit top edge, a ground that
     brightens toward the light, and an icon tile that fills on hover. */
  .commitments {
    display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
    gap: var(--sp-4);
  }
  @media (max-width: 640px) { .commitments { grid-template-columns: minmax(0,1fr); } }

  /* Decorative mark, out of flow on purpose: in normal flow its height would
     push the band taller, and the point is a large mark the band crops. */
  #ownership { position: relative; overflow: hidden; }
  #ownership > .container { position: relative; }
  #ownership .split { position: relative; z-index: 1; }
  .ownership__mark {
    position: absolute; z-index: 0;
    left: -4rem; top: 50%; transform: translateY(-50%);
    width: 64rem; height: auto;
    opacity: .22; pointer-events: none; user-select: none;
  }
  @media (max-width: 900px) { .ownership__mark { display: none; } }

  /* Solid white, not the translucent glass tint the dark band used before —
     the cards read as objects placed on the band now, not as a slightly
     lighter patch of the same surface. Title and body flip to dark text to
     stay readable on white; the icon chip becomes a light brand-tint rather
     than a glow, which is what makes it visible against a white ground. */
  .commitment-card {
    position: relative;
    display: grid; gap: var(--sp-4);
    padding: var(--sp-6);
    border-radius: var(--radius-lg);
    background: var(--c-neutral-0);
    box-shadow: var(--shadow-2);
    transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
  }
  /* Touch fires :hover on tap and leaves it stuck until the next tap
     elsewhere, so the lift is gated to real pointers. */
  @media (hover: hover) and (pointer: fine) {
    .commitment-card:hover {
      transform: translateY(-3px);
      box-shadow: var(--shadow-3);
    }
  }

  .commitment-card__icon {
    display: grid; place-items: center;
    width: 40px; height: 40px; border-radius: var(--radius-md);
    background: var(--bg-brand-soft);
    color: var(--fg-brand);
    transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
  }
  @media (hover: hover) and (pointer: fine) {
    .commitment-card:hover .commitment-card__icon {
      background: var(--fg-brand); color: #FFFFFF;
    }
  }
  .commitment-card__title {
    font-size: var(--fs-body-lg); font-weight: var(--fw-semibold);
    letter-spacing: var(--ls-snug);
  }
  /* base.css's `.on-dark h1/h2/h3/h4 { color:#fff }` is class+type — higher
     specificity than a single class, so it was beating this rule and the
     white-card title kept rendering white-on-white. Matching its
     specificity here (class+class) is what actually wins. */
  .on-dark .commitment-card__title { color: var(--fg-default); }
  .commitment-card__body {
    margin-top: var(--sp-2); font-size: var(--fs-body-sm);
    color: var(--fg-secondary); line-height: var(--lh-relaxed);
  }
