/* ===========================================================================
   PIONEERING — shared stylesheet.

   Every page links this. The nav, the footer, the buttons, the type scale and
   the feature grid are defined once here so the three pages cannot drift apart.
   Page-specific rules live in a <style> block on the page itself.
   ========================================================================= */

/* ---------------------------------------------------------------- tokens */
/* ===========================================================================
   THE ACCENT, AND WHY IT IS NOT BLUE

   It used to be #516CF0 — hue 230, saturation 84%. That is not "near
   Tailwind's default", it is the middle of it: blue-500 is 217/91 and
   indigo-500 is 239/84, and current measurements put ~78% of AI-generated
   interfaces inside the 200–290 hue band against ~22% of human work, with
   31% of AI landing pages shipping #3B82F6 exactly.

   Every hue-carrying token here was inside that band, five of them at 100%
   saturation, and the hero gradient ran blue → violet, which is the single
   most-cited generated-design tell of 2026.

   The reference this site borrowed its particle field from is in the band too
   — Antigravity's field is #7189ff / #3074f9, 230 and 220 degrees. That is
   fine for Google, which owns that blue. The tell was never the colour; it is
   an *unowned* colour. Copying the technique was right, copying the hue was
   not.

   So: rust, hue 16, saturation 62 rather than 84. Chosen to sit clear of the
   three signal colours it has to coexist with —

     error 348   accent 16   warning 48   success 142
             28d         32d          94d

   The error red moved off 0 and the warning off 39 to open that room; at
   16/0/39 the three warm signals blurred into each other.
   ========================================================================= */
:root[data-theme="dark"]{
  color-scheme:dark;
  --bg:#090B11; --card:#13151B; --secondary:#1C1F26; --border:#292C32;
  --fg:#F3F4F6; --muted:#AAAEB5; --ink-soft:#AAAEB5;
  /* Taken from the logo rather than chosen alongside it. The mark is a
     gradient from #E97A45 down to #BE4526; --primary is a solid drawn from the
     middle of that ramp so type and UI match the mark without competing with
     it. 5.0:1 as text on --bg — the blue it replaced was 4.45:1 and quietly
     failed AA on every 12px label. */
  --primary:#D55A34; --primary-hover:#E97A45; --primary-soft:rgba(213,90,52,.14);
  /* The mark's own two stops, so anything drawing the logo reads them from
     here instead of hardcoding a second copy that can drift. */
  --brand-top:#E97A45; --brand-bottom:#BE4526;
  --good:#4ADE80; --warn:#DBBD43; --err:#E8738A;
  --glow-a:18 68% 56%; --glow-b:14 58% 45%;
  --glow-a-o:.20; --glow-b-o:.14;
  /* One restrained gradient, analogous rather than a sweep across the wheel:
     10 -> 24 -> 30 degrees. The blue-to-violet version travelled 23 degrees
     into a different colour family, which is the part that read as generated. */
  --hero-grad:linear-gradient(120deg,#D66851,#EAA77B 34%,#F4DBC2 62%,#D66851);
  /* Particle field, kept exactly as it works — only the hue moved. The third
     colour is the page ground on purpose: a particle that lands on it
     vanishes, which is what varies the density. */
  --field-1:#EAAE86; --field-2:#D26B4B; --field-3:#090B11;
  --lit-ink:#FFF3EB;
  /* Selected text is text. --primary behind white lands at 4.42:1, which is
     under the floor, so selecting a paragraph made it harder to read than
     leaving it alone. A deeper step of the same hue clears it — 7.3:1 now. */
  --select:#8E3B23;
  --focus:#757A85;
}
:root[data-theme="light"]{
  color-scheme:light;
  /* The light theme was grown here and grown again in the Hub, and every
     token came out a near-miss of the other: #FAFCFF against #FAFAF9,
     #B24524 against #B84A24, #16A34A against #15803D. The two dark palettes
     were identical, which is what gave it away — nobody chose to differ,
     the two files simply never met. Canonical values, docs/design-system.md.

     What is NOT unified, and deliberately: the brand gradient darkens in
     light mode. #E97A45 -> #BE4526 is legible on a near-black and washes out
     on a near-white, and the same is true of the hover. Those two stay
     adapted; the ramp underneath them does not. */
  --bg:#FAFAF9; --card:#FFFFFF; --secondary:#F2F1EF; --border:#E0DEDA;
  --fg:#16181C; --muted:#5A5F68; --ink-soft:#525860;
  --primary:#B84A24; --primary-hover:#993F29; --primary-soft:rgba(184,74,36,.10);
  --brand-top:#D8632F; --brand-bottom:#A63A1E;
  --good:#15803D; --warn:#8A6D10; --err:#B3243F;
  --glow-a:18 68% 56%; --glow-b:14 58% 45%;
  --glow-a-o:.12; --glow-b-o:.08;
  --hero-grad:linear-gradient(120deg,#B84A24,#8A2F14 35%,#3A1608 65%,#B84A24);
  /* --field-3 IS the page ground, exactly: a particle that lands on it
     vanishes, and that is what varies the field's density. It has to
     track --bg or the trick stops working. */
  --field-1:#DC8960; --field-2:#CC5233; --field-3:#FAFAF9;
  --lit-ink:#FFF3EB;
  --select:#993F29;
  --focus:#6B707C;
}

/* ---- type scale ----------------------------------------------------------
   Large, tight, and light. The display face carries every page; everything
   else stays deliberately quiet so it can. */
:root{
  --t-hero:clamp(42px,7.6vw,98px);   --lh-hero:.86;  --ls-hero:-.019em;
  --t-h2:clamp(30px,4.4vw,54px);     --lh-h2:1.04;   --ls-h2:-.018em;
  --t-h3:clamp(26px,3.2vw,42px);     --lh-h3:1.04;   --ls-h3:-.017em;
  --t-h4:clamp(20px,2.1vw,28px);     --lh-h4:1.08;   --ls-h4:-.004em;
  --t-body:17.5px;                   --lh-body:1.45; --ls-body:.011em;
  --t-cap:14.5px;                    --lh-cap:1.45;  --ls-cap:.011em;

  /* ---- space ------------------------------------------------------------
     One ladder, used everywhere, so grouping is never ambiguous:
       inside a component  <  between components  <  between sections
     Getting these equal is what makes a layout read as flat and crowded at
     the same time. */
  --s-1:8px; --s-2:14px; --s-3:20px; --s-4:28px;
  --s-5:44px;                                   /* between components */
  --s-6:clamp(56px,7vw,88px);                   /* inside a section    */
  --s-7:clamp(80px,11vw,150px);                 /* between sections    */

  /* The footer wordmark. MEASURED, NOT CHOSEN. "PIONEERING" in Cabinet Grotesk at this weight and
     -.04em tracking is 6.0584em wide — measured in the browser with the real
     font loaded, not estimated from metrics. At the old 27.5vw the word came
     to 2133px inside a 1280px window: 853px of it, most of two letters at each
     end, simply was not there. That is not the "side cropping is intended"
     the footer rule claims; it is the wordmark being unreadable.

     15.4vw x 6.0584 = 93.3vw, so the whole word lands with about 3.3vw of air
     on each side at every viewport, and the cap keeps it from becoming absurd
     on a very wide screen. Change one of these two numbers and you must
     re-measure the other. */
  --t-mark:clamp(44px,15.4vw,264px);
  --gut:clamp(16px,4vw,35px);
  --ease-out:cubic-bezier(.16,1,.3,1);
  --ease-ui:cubic-bezier(.4,0,.2,1);
  /* ONE LADDER, STEPS OF 1.5. These were 8/14/20 here and 8/12/18 in the Hub
     app, and 12 against 14 is not a decision anyone reads as one — a value
     within about 10% of another reads as a mistake. Geometric wins because it
     is a rule; +6/+6 is a habit. docs/design-system.md. */
  --r-sm:8px; --r-md:12px; --r-lg:18px; --r-pill:999px;
}

*{box-sizing:border-box}
/* The two ground glows, named rather than written out, because the canvas
   below needs the top one as well and a second literal copy of a gradient is
   a copy that drifts. Both stay viewport-anchored, so the glow sits in the
   corners of the screen rather than the corners of a 6000px document. */
:root{
  --ground-top:radial-gradient(1200px 800px at 0% 0%, hsl(var(--glow-a) / var(--glow-a-o)), transparent 62%);
  --ground-bottom:radial-gradient(1000px 700px at 100% 100%, hsl(var(--glow-b) / var(--glow-b-o)), transparent 62%);
}
/* The canvas, painted explicitly.
   Two things were leaking Chrome's own near-black at the ends of the page.
   The root element had no background of its own, so the overscroll region
   fell through to whatever the browser paints there; and `color-scheme` was
   `normal`, which lets the browser pick that colour from the OS rather than
   from the page. Setting both means the rubber band at the top and bottom is
   the page's own background, and the scrollbars match the theme too.

   Giving html a background costs something that has to be paid back here:
   body's background only reaches the canvas while html's is transparent AND
   image-less (CSS Backgrounds 3, "The Canvas Background"). The colour above
   ends that, so the canvas went flat --bg and stopped carrying body's glows.
   Flat is right at the bottom, where the footer paints its own opaque --bg
   over the glow anyway. It is wrong at the top, where the glow is at full
   strength: the page edge measured rgb(41,27,26) against a rubber band of
   rgb(9,11,17), which is the black band people were seeing.

   So the canvas repeats the top glow — and only the top one. --ground-bottom
   deliberately stays off it: that gutter has to match the flat footer, not
   the glow the footer covers up. body keeps its own copy of both because its
   opaque background-color hides the canvas everywhere the document reaches;
   the layers here are seen only in the overscroll gutters. */
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;
  background-color:var(--bg);
  background-image:var(--ground-top);
  background-attachment:fixed;
  /* No elastic bounce, and this is the actual fix for the peel.
     Matching the canvas colour to the page edge was not enough, and neither
     was clamping the scroller: during a rubber band the browser drags the
     BODY down while a position:fixed wrapper stays pinned to the viewport.
     The hero's particles are inside that pinned wrapper so they stay put,
     while body's opaque background — the fade — slides away underneath them.
     Nothing paints in the gap, so it reads as the fade peeling down off the
     particles. That seam exists only because the scroller is custom; a normal
     page has no fixed full-viewport layer for the body to slide out from
     under. Since the smooth scroller already owns the scrolling feel, the
     native bounce it fights with is not something this page gives up. */
  overscroll-behavior-y:none}
body{
  margin:0; background:var(--bg); color:var(--fg);
  overscroll-behavior-y:none;
  font-family:"Inter",system-ui,-apple-system,sans-serif;
  font-size:var(--t-body); line-height:var(--lh-body); letter-spacing:var(--ls-body);
  font-feature-settings:"ss01","cv11";
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
  background-image:var(--ground-top),var(--ground-bottom);
  background-attachment:fixed;
}
a{color:inherit;text-decoration:none}
img,svg,canvas{display:block;max-width:100%}
/* The UA gives figure 40px side margins, which silently ate 80px out of a
   grid track that had been measured to the pixel. */
figure{margin:0}
/* ---------------------------------------------------------------- focus
   Neutral, not the accent — and that is the decision, not an oversight.

   An accent-coloured border with a soft accent glow is the default focus
   state that Bootstrap and Tailwind both ship, which makes it the most
   reproduced input state on the web and the first thing that marks a form as
   generated rather than designed.

   It also costs something real. --primary is already the CTA, the links, the
   step counter, the featured tier and the hover colour. Adding "focused" to
   that list makes the accent mean one more thing, and every extra job dilutes
   the one blue button that is supposed to be the loudest object on the page.

   So focus is a neutral, chosen for contrast rather than for hue:

     vs the same control unfocused   3.5
     vs the field fill               4.6
     vs the card behind it           4.2

   — all clear of the 3.0 floor, so it is *more* findable than the blue it
   replaces (3.4), not less. Hue only helps someone who already knows which
   hue to look for; contrast helps everyone. The grey carries the palette's
   blue (b−r of 16, against 8–11 across the neutrals) so it belongs here
   rather than reading as a flat grey dropped in from somewhere else.

   One treatment for one state, everywhere: buttons, links, inputs, the docs
   search. If inputs went neutral and buttons stayed blue, focus would be one
   role wearing two sets of clothes. */
:focus-visible{outline:2px solid var(--focus);outline-offset:3px;border-radius:6px}
/* Visible to a screen reader, not to the eye. For labels whose job is done
   visually by position or by a placeholder — the label still has to exist. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
::selection{background:var(--select);color:#fff}

.wrap{max-width:1400px;margin:0 auto;padding:0 var(--gut)}
.section{position:relative;z-index:2;background:var(--bg)}
.section-pad{padding:var(--s-7) 0}

/* ---------------------------------------------------------------- type */
.eyebrow{font-family:"JetBrains Mono",ui-monospace,Menlo,monospace;
  font-size:12px;font-weight:500;letter-spacing:.025em;line-height:1;color:var(--muted);margin:0}
/* Was --primary. An eyebrow is a label, not a signal — it says "this section
   is about pricing", it does not indicate state or invite a click. Weight is
   enough to lift it off the muted default without spending the accent. */
.eyebrow.on-accent{color:var(--fg)}
.display{font-family:"Cabinet Grotesk","Inter",system-ui,sans-serif;font-weight:500;
  color:var(--fg);margin:0;text-wrap:balance}
.h2{font-size:var(--t-h2);line-height:var(--lh-h2);letter-spacing:var(--ls-h2)}
.h3{font-size:var(--t-h3);line-height:var(--lh-h3);letter-spacing:var(--ls-h3)}
.h4{font-size:var(--t-h4);line-height:var(--lh-h4);letter-spacing:var(--ls-h4)}
.cap{font-size:var(--t-cap);line-height:var(--lh-cap);letter-spacing:var(--ls-cap);color:var(--muted)}
.lead{color:var(--ink-soft);max-width:52ch;margin:0;text-wrap:pretty}
p{text-wrap:pretty}

/* ---------------------------------------------------------------- buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.55rem;
  min-height:50px;padding:0 28px;border-radius:var(--r-pill);
  font-weight:500;font-size:16px;cursor:pointer;border:1px solid transparent;white-space:nowrap;
  transition:background .3s var(--ease-ui),color .3s var(--ease-ui),
    border-color .3s var(--ease-ui),opacity .3s var(--ease-ui),transform .12s ease}
.btn svg{width:18px;height:18px;flex:none}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--fg);color:var(--bg)}
.btn-primary:hover{opacity:.88}
.btn-outline{border-color:var(--border);background:color-mix(in srgb,var(--card) 60%,transparent);color:var(--fg)}
.btn-outline:hover{border-color:color-mix(in srgb,var(--fg) 45%,transparent)}
.btn-sm{min-height:40px;padding:0 20px;font-size:14.5px}
.btn .arrow{transition:transform .3s var(--ease-out)}
.btn:hover .arrow{transform:translateX(4px)}

.pill{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:var(--r-pill);
  font-family:"JetBrains Mono",monospace;font-size:11.5px;letter-spacing:.02em;
  color:var(--primary);background:var(--primary-soft)}

/* ---------------------------------------------------------------- nav */
.nav{position:fixed;inset:0 0 auto;z-index:50;padding:clamp(12px,2vw,20px) var(--gut)}
.nav-pill{max-width:1400px;margin:0 auto;min-height:56px;padding:0 20px;
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  background:color-mix(in srgb,var(--card) 78%,transparent);
  border:1px solid color-mix(in srgb,var(--border) 50%,transparent);
  border-radius:16px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}
.brand{display:flex;align-items:center;gap:9px;font-family:"Cabinet Grotesk",sans-serif;
  font-weight:700;font-size:15px;letter-spacing:.14em;color:var(--fg)}
.brand .mark{width:21px;height:21px;flex:none}
/* The mark is stroked, not filled, and both of its paths take the brand
   colour. currentColor rather than a hardcoded value so a mark sitting on an
   inverted surface follows the text it belongs to. */
.brand .mark{color:var(--primary)}
.brand .mark path{stroke:currentColor}
.nav-links{display:flex;align-items:center;gap:clamp(14px,2vw,30px);list-style:none;margin:0;padding:0}
/* :not(.btn) matters. `.nav-links a` is specificity 0,1,1 and `.btn-primary`
   is 0,1,0, so without this the generic nav rule wins and the Download button
   renders as a white pill with mono-grey text on it — invisible. Same trap the
   reference site fell into with its own CTA. */
.nav-links a:not(.btn){font-family:"JetBrains Mono",monospace;font-size:.88rem;
  color:color-mix(in srgb,var(--fg) 70%,transparent);transition:color .2s}
.nav-links a:not(.btn):hover,
.nav-links a:not(.btn)[aria-current="page"]{color:var(--primary)}
.nav-links > li{position:relative}

/* A grouped nav rather than a flat one.
   Seven flat links make the reader scan and choose; three groups make them
   pick a category first, which is one decision instead of seven. The groups
   also give new pages somewhere obvious to go, so the bar stops growing.

   Hover alone would be a trap: it does not exist on touch and it is
   unreachable by keyboard, so the trigger is a real <button> that toggles,
   and hover is only an accelerator on top of it. */
.nav-group > button{display:inline-flex;align-items:center;gap:6px;
  font-family:"JetBrains Mono",monospace;font-size:.88rem;
  color:color-mix(in srgb,var(--fg) 70%,transparent);
  background:none;border:0;padding:6px 0;cursor:pointer;transition:color .2s}
.nav-group > button:hover,.nav-group[data-open] > button{color:var(--primary)}
.nav-group > button svg{width:11px;height:11px;transition:transform .25s var(--ease-ui)}
.nav-group[data-open] > button svg{transform:rotate(180deg)}

.nav-menu{position:absolute;top:calc(100% + 14px);left:50%;transform:translateX(-50%) translateY(-6px);
  min-width:230px;padding:8px;margin:0;list-style:none;z-index:60;
  background:color-mix(in srgb,var(--card) 96%,transparent);
  border:1px solid color-mix(in srgb,var(--border) 70%,transparent);
  border-radius:14px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  box-shadow:0 18px 40px -18px rgb(0 0 0 / .55);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .22s var(--ease-ui),transform .22s var(--ease-out),visibility .22s}
.nav-group[data-open] .nav-menu{opacity:1;visibility:visible;pointer-events:auto;
  transform:translateX(-50%) translateY(0)}
/* A bridge across the gap, so the pointer does not fall out of the menu on
   the way to it. */
.nav-menu::before{content:"";position:absolute;left:0;right:0;top:-14px;height:14px}
.nav-menu a{display:block;padding:9px 12px;border-radius:9px;font-family:inherit;
  font-size:14px;color:var(--fg);transition:background .18s,color .18s}
.nav-menu a:hover,.nav-menu a:focus-visible{background:var(--primary-soft);color:var(--primary)}
.nav-menu .k{display:block;font-family:"JetBrains Mono",monospace;font-size:11px;
  color:var(--muted);margin-top:2px}
.nav-menu a:hover .k{color:color-mix(in srgb,var(--primary) 70%,transparent)}

/* Below 760 the dropdown stops being a floating panel and becomes part of the
   row, because there is no room to float one.

   `display:none` IS LOAD-BEARING HERE and its absence was a real bug. Above
   this breakpoint the closed panel is `position:absolute`, so hiding it with
   visibility/opacity costs no layout. In flow, those two properties hide the
   panel and KEEP ITS BOX — so the nav pill was sized around five invisible
   menu items and stood 368px tall on every phone, roughly a third of the
   screen of nothing, permanently, with no way to close it because it was
   never open.

   The fade goes with it: you cannot transition `display`. That is the correct
   trade — a menu that animates and a nav that is 368px tall is not a better
   nav. */
/* ===========================================================================
   THE NAV ON A SMALL SCREEN

   Measured on a 375-wide phone before this existed: `.nav-links` was 371px
   wide with its right edge at 561, so `pricing`, the Download button and the
   theme toggle were all PAST the edge of the viewport — not wrapped, not
   scrolled, simply unreachable. The body had no horizontal overflow, so
   nothing about the page looked wrong; the controls were just gone.

   The list becomes a panel under the pill rather than a second copy of the
   nav in different markup. One <ul>, two layouts: a row above the breakpoint
   and a stack below it. A duplicated mobile menu is the thing that ends up
   missing the link somebody added to the other one.

   The groups keep the accordion they already had — tapping `projects` opens
   its four links in place. Expanding every group by default would put eleven
   rows on screen at once, and the two-tap cost buys a panel a thumb can read.
   ========================================================================= */
.nav-toggle{display:none;width:38px;height:38px;flex:none;place-items:center;
  border:0;background:transparent;color:var(--fg);border-radius:var(--r-sm);
  cursor:pointer;transition:background .2s,color .2s}
.nav-toggle:hover{background:var(--primary-soft);color:var(--primary)}
.nav-toggle svg{width:19px;height:19px}
.nav-toggle .x{display:none}
.nav-pill[data-menu] .nav-toggle svg{display:none}
.nav-pill[data-menu] .nav-toggle .x{display:block}

@media (max-width:860px){
  .nav-menu{position:static;transform:none;min-width:0;padding:0 0 0 12px;background:none;
    border:0;box-shadow:none;backdrop-filter:none;display:none}
  .nav-group[data-open] .nav-menu{display:block;transform:none}

  .nav-toggle{display:grid}
  /* The panel hangs off the pill, so the pill has to be the containing block. */
  .nav-pill{position:relative}

  .nav-links{display:none;position:absolute;top:calc(100% + 10px);left:0;right:0;
    flex-direction:column;align-items:stretch;gap:2px;
    padding:10px;max-height:min(70vh,520px);overflow-y:auto;overscroll-behavior:contain;
    background:color-mix(in srgb,var(--card) 96%,transparent);
    border:1px solid color-mix(in srgb,var(--border) 60%,transparent);
    border-radius:14px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
    box-shadow:0 18px 40px rgb(0 0 0 / .28)}
  .nav-pill[data-menu] .nav-links{display:flex}

  /* Every row is the same 44px target. Apple and Google both put the minimum
     at 44 and 48 CSS pixels respectively; a 20px text link in a stack is the
     thing a thumb misses. */
  .nav-links > li{width:100%}
  .nav-links a:not(.btn),.nav-group > button{
    display:flex;align-items:center;width:100%;min-height:44px;
    padding:0 12px;border-radius:10px;font-size:.95rem}
  .nav-group > button{justify-content:space-between}
  .nav-links a:not(.btn):hover{background:var(--primary-soft)}
  .nav-menu a{min-height:40px;display:flex;flex-direction:column;justify-content:center}

  /* The one filled control, full width and last — the shape every application
     sheet on both operating systems uses for its confirming action. */
  .nav-links > li:last-child{margin-top:6px}
  .nav-links a.btn{display:flex;justify-content:center;width:100%;min-height:44px}
}
.icon-btn{width:38px;height:38px;display:grid;place-items:center;border:0;background:transparent;
  color:var(--fg);border-radius:var(--r-sm);cursor:pointer;transition:background .2s,color .2s}
.icon-btn:hover{background:var(--primary-soft);color:var(--primary)}
.icon-btn svg{width:18px;height:18px}
.icon-btn .sun{display:none}
:root[data-theme="light"] .icon-btn .sun{display:block}
:root[data-theme="light"] .icon-btn .moon{display:none}

/* ---------------------------------------------------------------- hero */
.hero{position:relative;display:grid;place-items:center;overflow:hidden;text-align:center}
.hero-full{height:100svh;min-height:560px}
.hero-short{min-height:min(76svh,760px);padding:calc(84px + var(--s-6)) 0 var(--s-6)}
#field{position:absolute;inset:0;width:100%;height:100%;z-index:0;pointer-events:none}
.hero-glow{position:absolute;inset:0;z-index:1;pointer-events:none;background:
  radial-gradient(52% 50% at 50% 46%, hsl(var(--glow-a) / .22), transparent 72%),
  radial-gradient(42% 42% at 50% 48%, hsl(var(--glow-b) / .16), transparent 72%)}
.hero-inner{position:relative;z-index:2;width:100%;max-width:1180px;padding:0 var(--gut)}
.hero-brand{display:inline-flex;align-items:center;gap:10px;margin:0 0 clamp(20px,3vh,34px);
  font-family:"Cabinet Grotesk",sans-serif;font-weight:700;font-size:15px;letter-spacing:.16em;color:var(--fg)}
.hero-brand svg{width:22px;height:22px}
.hero-brand svg{color:var(--primary)}
.hero-brand svg path{stroke:currentColor}
.hero-title{font-family:"Cabinet Grotesk","Inter",sans-serif;font-weight:500;
  font-size:var(--t-hero);line-height:var(--lh-hero);letter-spacing:var(--ls-hero);
  margin:0 auto;max-width:15ch;
  background-image:var(--hero-grad);background-size:280% 280%;background-position:0% 50%;
  -webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
  animation:hero-pan 12s ease-in-out infinite}
@keyframes hero-pan{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
.hero-sub{margin:clamp(18px,2.6vh,26px) auto 0;max-width:44ch;color:var(--ink-soft);
  font-size:clamp(16px,1.1vw,18px)}
.hero-actions{margin:clamp(26px,4vh,40px) auto 0;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
.scroll-cue{position:absolute;left:50%;bottom:clamp(20px,3.4vh,34px);transform:translateX(-50%);
  z-index:2;display:flex;flex-direction:column;align-items:center;gap:8px;
  font-family:"JetBrains Mono",monospace;font-size:11px;letter-spacing:.14em;color:var(--muted)}
.scroll-cue i{width:1px;height:26px;background:linear-gradient(var(--muted),transparent)}

/* ---------------------------------------------------------------- section head */
.section-head{display:flex;align-items:end;justify-content:space-between;gap:24px;
  margin-bottom:var(--s-6);flex-wrap:wrap}
.section-head .eyebrow{margin-bottom:var(--s-2)}

/* ===========================================================================
   FEATURE ROW

   A twelve-column grid: copy on 1–4, media on 7–12. The empty two columns
   between them are the point — a sixth of the container. At a smaller gap the
   two halves read as one crowded block, because proximity is a stronger
   grouping cue than any amount of styling, and the copy ends up looking like
   a caption for the picture instead of its own column.
   ========================================================================= */
.feature{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  align-items:center;padding:var(--s-6) 0}
.feature + .feature{border-top:1px solid color-mix(in srgb,var(--border) 45%,transparent)}
.feature-copy{grid-column:1 / span 4;min-width:0}
.feature-media{grid-column:7 / span 6;min-width:0}
.feature.flip .feature-copy{grid-column:9 / span 4}
.feature.flip .feature-media{grid-column:1 / span 6}

.feature-title{display:block;margin:0 0 var(--s-3)}
.feature-desc{color:var(--ink-soft);margin:0}
.feature-meta{display:flex;flex-wrap:wrap;gap:var(--s-1);margin:var(--s-4) 0 0;padding:0;list-style:none}
.feature-meta li{font-family:"JetBrains Mono",monospace;font-size:11px;letter-spacing:.04em;
  color:var(--muted);border:1px solid color-mix(in srgb,var(--border) 80%,transparent);
  border-radius:var(--r-pill);padding:5px 11px}
.feature-cta{margin:var(--s-5) 0 0;display:flex;gap:10px;flex-wrap:wrap}

.feature-media-inner{position:relative;aspect-ratio:1/1;width:100%;border-radius:var(--r-lg);
  overflow:hidden;border:1px solid color-mix(in srgb,var(--border) 60%,transparent);
  background:var(--card);display:grid;place-items:center}
.feature-media-inner img{width:100%;height:100%;object-fit:cover;object-position:top center}
.feature-media-inner.lit{background:
  radial-gradient(60% 60% at 50% 48%, hsl(var(--glow-a) / .55), transparent 68%),
  radial-gradient(48% 48% at 50% 52%, hsl(var(--glow-b) / .45), transparent 70%),
  #05070d}
.feature-media-inner.lit span{position:relative;font-family:"Cabinet Grotesk",sans-serif;
  font-weight:500;font-size:clamp(22px,2.6vw,36px);letter-spacing:-.015em;color:var(--lit-ink);
  text-shadow:0 2px 30px rgba(0,0,0,.5)}
.feature-media-inner.soon{background:
  radial-gradient(60% 60% at 50% 50%, color-mix(in srgb,var(--fg) 7%,transparent), transparent 70%),
  var(--card);border-style:dashed}
.feature-media-inner.soon span{font-family:"JetBrains Mono",monospace;font-size:13px;
  letter-spacing:.12em;color:var(--muted);text-transform:uppercase}
.feature-media figcaption{margin:var(--s-2) 0 0;font-family:"JetBrains Mono",monospace;
  font-size:11.5px;color:var(--muted)}

/* ---------------------------------------------------------------- rail */
.rail-wrap{position:relative}
.rail{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(260px,300px);
  gap:clamp(14px,1.6vw,22px);overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-padding-left:var(--gut);
  padding:4px var(--gut) 22px;margin:0 calc(var(--gut) * -1);scrollbar-width:none}
.rail::-webkit-scrollbar{display:none}
.note{scroll-snap-align:start;display:flex;flex-direction:column;gap:var(--s-2)}
.note-media{aspect-ratio:1/1;border-radius:var(--r-md);overflow:hidden;position:relative;
  border:1px solid color-mix(in srgb,var(--border) 60%,transparent);
  background:radial-gradient(65% 65% at 50% 45%, hsl(var(--glow-a) / .40), transparent 70%),#05070d;
  display:grid;place-items:center;padding:22px;text-align:center;
  transition:border-color .35s var(--ease-ui),transform .45s var(--ease-out)}
.note:hover .note-media{border-color:color-mix(in srgb,var(--primary) 40%,transparent);
  transform:translateY(-3px)}
.note-media b{font-family:"Cabinet Grotesk",sans-serif;font-weight:500;font-size:20px;
  letter-spacing:-.012em;color:var(--lit-ink);line-height:1.14}
.note-title{font-family:"Cabinet Grotesk",sans-serif;font-weight:500;font-size:20px;
  line-height:1.16;letter-spacing:-.008em;color:var(--fg);margin:var(--s-1) 0 0;
  transition:color .3s var(--ease-ui)}
.note:hover .note-title{color:var(--primary)}
.note-meta{display:flex;gap:var(--s-2);flex-wrap:wrap;margin:0}
.note-go{font-family:"JetBrains Mono",monospace;font-size:12px;color:var(--primary);
  display:inline-flex;align-items:center;gap:7px;margin-top:auto;padding-top:var(--s-1)}
.note-go .arrow{transition:transform .3s var(--ease-out)}
.note:hover .note-go .arrow{transform:translateX(4px)}
.rail-controls{display:flex;gap:8px}
.rail-btn{width:40px;height:40px;border-radius:var(--r-pill);display:grid;place-items:center;
  cursor:pointer;background:transparent;color:var(--fg);
  border:1px solid color-mix(in srgb,var(--border) 80%,transparent);
  transition:border-color .25s var(--ease-ui),opacity .25s,background .25s}
.rail-btn:hover:not(:disabled){border-color:color-mix(in srgb,var(--fg) 45%,transparent);
  background:var(--primary-soft)}
.rail-btn:disabled{opacity:.32;cursor:default}
.rail-btn svg{width:17px;height:17px}

/* ---------------------------------------------------------------- footer */
footer{position:relative;z-index:2;overflow:hidden;background:var(--bg);
  border-top:1px solid color-mix(in srgb,var(--border) 40%,transparent);
  /* Derived from the wordmark rather than guessed: the mark's box is
     line-height x font-size, so the footer has to clear that or the letters
     are clipped top and bottom. Horizontal clipping is no longer possible
     either — --t-mark is now sized from the word's measured width. */
  padding:clamp(72px,11vw,150px) 0 clamp(64px,9vw,120px);
  min-height:calc(var(--t-mark) * .96)}
/* The short mark, at 650px.
   This used to be the full wordmark, cropped at both edges on purpose so it
   read as ground texture rather than as a heading nobody can finish. That
   works at six letters. "PIONEERING" is ten, and at the same size only about
   40% of it survives the crop — you get "NEER", which is neither a word nor a
   texture. So the footer carries the short mark instead: two glyphs, whole,
   at the size the crop was tuned for. */
/* Sized so the whole word fits — see --t-mark. `nowrap` stays: the failure
   mode to avoid is the word breaking across two lines, not overflow, because
   overflow is no longer possible. */
.foot-mark{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  max-width:100%;
  font-family:"Cabinet Grotesk",sans-serif;font-weight:700;letter-spacing:-.04em;
  font-size:var(--t-mark);line-height:.78;color:var(--fg);opacity:.05;
  text-align:center;pointer-events:none;user-select:none;white-space:nowrap}
.foot-inner{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;
  gap:var(--s-3);text-align:center}
.foot-nav{display:flex;flex-wrap:wrap;justify-content:center;gap:6px 22px;
  font-family:"JetBrains Mono",monospace;font-size:12px;color:var(--muted)}
.foot-nav a:hover{color:var(--fg)}
.foot-legal{font-size:12px;color:var(--muted);margin:0}

/* ===========================================================================
   SMOOTH SCROLL

   The wrapper is a fixed window; the content inside it is what moves. Body
   carries the real scroll height, so the scrollbar, keyboard scrolling and
   find-in-page all keep working — only the paint lags.

   The fixed header deliberately lives OUTSIDE the wrapper: position:fixed
   inside a transformed ancestor is positioned against that ancestor instead
   of the viewport, so a nav in here would scroll away.
   ========================================================================= */
/* `clip`, NOT `hidden`, and the difference is the whole bug.

   `overflow:hidden` still creates a SCROLL CONTAINER — it hides the scrollbar
   and stops the user scrolling, and leaves the box perfectly scrollable to the
   browser itself. So a fragment navigation to /download#hub scrolled THIS box
   by 783px to bring the target into view, while window.scrollY stayed 0.
   Measured: wrapper.scrollTop 783, content transform identity, scrollY 0.

   The window is what the wheel talks to, and it was already at 0, so the hero
   above could not be reached; and because the offset lives inside the wrapper
   it survives every window scroll, so at the bottom the content ran out 783px
   early and left that much empty page under the footer. One offset, both
   symptoms, and smooth.js never moved a pixel of it.

   `overflow:clip` creates no scroll container at all, so there is nothing for
   the browser to scroll and the fragment navigation goes to the viewport where
   it belongs. `hidden` stays first as the fallback for engines without `clip`;
   smooth.js zeroes the wrapper defensively for those. */
#smooth-wrapper.smoothing{position:fixed;top:0;left:0;width:100%;height:100%;
  overflow:hidden;overflow:clip}
#smooth-wrapper.smoothing #smooth-content{will-change:transform}
/* Touch, reduced motion, or no JS: ordinary document flow, nothing moved. */
#smooth-wrapper.no-smooth,#smooth-wrapper:not(.smoothing){position:static;overflow:visible}

/* ===========================================================================
   CHARACTER REVEAL
   ========================================================================= */
[data-chars]{--c-lit:var(--primary)}
.js [data-chars] .w{display:inline-block;white-space:nowrap}
.js [data-chars] .c{display:inline-block;white-space:pre}
/* Characters condense rather than fade: they rise a little and resolve out of
   a blur, and the one at the leading edge carries the accent and settles to
   its final colour behind the sweep. */
.js [data-chars]:not(.chars-in) .c{opacity:0;transform:translateY(.34em);filter:blur(5px);color:var(--c-lit)}
.js [data-chars].chars-in .c{opacity:1;transform:none;filter:blur(0);color:inherit;
  transition:opacity .34s var(--ease-out),transform .5s var(--ease-out),
    filter .38s var(--ease-out),color .9s ease}

/* ===========================================================================
   TYPED REVEAL  [data-typed]

   The sibling of the sweep above, and deliberately plainer than it: characters
   arrive one at a time, opacity only. Measured off the reference — no caret
   exists in its markup, and its characters carry no transform and no filter.
   The rise, the blur and the accent leading edge belong to [data-chars]; using
   them here too would make two effects that should feel different feel alike.
   ========================================================================= */
/* Hidden with VISIBILITY, not opacity — and this is the difference between
   the effect working and the caret sliding across a line that was already
   there.

   The hero titles paint their gradient on the PARENT, via background-clip:text
   with a transparent -webkit-text-fill-color. The visible pixels therefore
   belong to the h1, clipped to the union of every glyph inside it; the
   character spans paint nothing at all. Setting opacity:0 on something that
   was never painting hides nothing, so all 38 characters stayed on screen and
   only the caret moved.

   visibility:hidden takes the glyph out of the parent's clip region, which is
   the only thing that actually removes a character from that gradient.
   Verified: hiding 33 of 38 removed them from the rendered heading. Headings
   with a solid colour never showed the bug, which is why it looked like the
   typing worked in some places and not others. */
/* AND THE SECOND HALF OF THE SAME BUG, which shipped for weeks: a character
   that arrives cannot fade either, for exactly the reason it cannot be hidden
   with opacity.

   `.c.on` carried `animation: typed-in .1s both`, animating opacity from 0 to
   1. Animating a compositable property promotes the span to a layer of its
   own — and a glyph in its own layer is a glyph the PARENT's background-clip
   region no longer covers. So every character was removed from the gradient
   the instant it was revealed, and every `.hero-title` on the site rendered
   as an empty space with a caret blinking in it. Seven pages, the H1 of each.

   Measured, in the browser, on the live site: with `animation:none` forced on
   `.c.on` the headline appears immediately and completely. Nothing else was
   changed.

   There is no per-character fade to be had here, and that is a fact about
   background-clip rather than a thing to work around: the visible pixels
   belong to the h1, so nothing done to a child can fade them. What is left is
   what a typewriter actually does — the character is there or it is not — and
   the caret gliding ahead of it is the effect. */
.js [data-typed] .c{visibility:hidden}
.js [data-typed] .c.on{visibility:visible}

/* The caret, copied from the reference's own numbers rather than invented.
   It GLIDES between characters — transitioning the transform is what stops a
   24ms cadence looking like a strobe — and its blink is asymmetric: full at
   10% of the cycle, fading across the rest. A square 50/50 flash reads
   mechanical; this reads like a cursor waiting. */
/* Always relative, not only while typing: the caret stays afterwards, so the
   box it is positioned against has to stay too. */
.js [data-typed]{position:relative}
.caret{position:absolute;left:0;top:0;width:2px;
  background:var(--primary);border-radius:1px;pointer-events:none;
  opacity:0;
  /* NO transition on transform. The caret jumps to each character the instant
     it lands.

     It used to glide over 120ms, on the reading that the reference sets
     `transition: all` on its cursor. It does — with a duration of 0s, which
     means no transition at all. Measured, after being caught out by inferring
     it from the property name.

     The glide was not a stylistic difference, it was a bug: characters arrive
     every ~24ms, so a 120ms glide leaves the caret permanently about five
     characters behind, drifting further back through a long line and then
     snapping forward at the end. That lag is exactly what reads as fake —
     a real cursor is never anywhere except at the insertion point. */
  transition:opacity .35s var(--ease-ui)}
@keyframes caret-blink{0%{opacity:0}10%{opacity:1}100%{opacity:0}}
/* Solid while writing, blinking once it comes to rest.
   This is the one place that deliberately departs from the reference, which
   blinks throughout. The blink spends most of its cycle faded out, so during
   typing the caret is dim or gone at the exact moments characters are landing
   — the cursor and the text stop looking connected, which is the other half of
   why it read as fake. Every real cursor does this: solid while keys are
   going in, blinking when it is waiting for you. */
.caret.lit{opacity:1;transition:opacity .35s var(--ease-ui)}
.js [data-typed]:not(.typing) .caret.lit{animation:caret-blink .5s infinite}
/* Every path that gives up on the animation must still leave the text on the
   page. The sweep above gets this free because its rule keys off .chars-in;
   this one keys off .on, which only the typing loop sets — so without this the
   backstop, a missing IntersectionObserver, or reduced motion would each leave
   the heading permanently invisible. A reveal that fails has to fail into
   "visible", never into "gone". */
.js [data-typed].chars-in .c{visibility:visible}

/* Held back until the heading above them has finished typing.
   A rise as well as a fade, because these are arriving *after* something
   rather than alongside it — a pure fade at the same moment reads as the page
   still loading, while a short rise reads as a consequence. The particle
   canvas is in here too, so the hero writes its line onto an empty ground and
   the field comes up underneath it. */
.js [data-after]{opacity:0;transform:translateY(8px)}
.js [data-after].after-in{opacity:1;transform:none;
  transition:opacity .55s var(--ease-out),transform .55s var(--ease-out)}
/* The canvas fills the hero and is decorative; moving it would drag the whole
   field sideways under the text. It only fades. */
.js canvas[data-after]{transform:none}
.js canvas[data-after].after-in{transition:opacity .9s var(--ease-out)}

/* ---------------------------------------------------------------- motion */
.js .reveal{opacity:0;transform:translateY(16px)}
.js .reveal.in{opacity:1;transform:none;
  transition:opacity .7s var(--ease-out),transform .7s var(--ease-out)}
.js .type-in .w{opacity:0;transition:opacity .5s var(--ease-out)}
.js .type-in.in .w{opacity:1}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .hero-title{animation:none}
  .js .reveal,.js .type-in .w{opacity:1;transform:none;transition:none}
  .js [data-chars] .c{opacity:1 !important;transform:none !important;
    filter:none !important;color:inherit !important;transition:none !important}
  .js [data-typed] .c{visibility:visible !important}
  .caret{display:none !important}
  .js [data-after]{opacity:1 !important;transform:none !important;transition:none !important}
  .rail{scroll-snap-type:none}
  *{scroll-behavior:auto !important}
}

/* ---------------------------------------------------------------- responsive */
@media (max-width:900px){
  /* Stacked, so the column gutter becomes a vertical one. It still has to be
     visibly larger than the gaps inside the copy block, or the picture reads
     as part of the paragraph above it. */
  .feature{grid-template-columns:1fr;gap:var(--s-5);padding:var(--s-6) 0}
  .feature-copy,.feature-media,
  .feature.flip .feature-copy,.feature.flip .feature-media{grid-column:1 / -1}
  .feature-copy{order:2}
  .feature-media{order:1}
  .nav-links .hide-sm{display:none}
  .hero-title{max-width:13ch}
}
@media (max-width:560px){
  .nav-links{gap:14px}
  .section-head{align-items:start}
}

/* ---------------------------------------------------------------------
   THE TASK-PAGE HEADER. Set a password, sign in, pay, or be told it
   worked — five pages that exist to finish one thing, so they carry a
   brand, a label naming the task, and the theme toggle. Nothing else.

   It was `.pay-top`, duplicated in three page-local style blocks, and the
   name was the same lie as the copy: /reset and /activate both rendered
   "Secure checkout" on a page where nothing is bought, because the pay
   page's header had been pasted in whole. One component, one place, and a
   label each page has to state for itself.
   ------------------------------------------------------------------- */
.task-top{position:sticky;top:0;z-index:40;
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:clamp(13px,1.8vw,18px) var(--gut);
  background:color-mix(in srgb,var(--bg) 86%,transparent);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  border-bottom:1px solid color-mix(in srgb,var(--border) 45%,transparent)}
.task-top-right{display:flex;align-items:center;gap:14px}
.task-label{display:inline-flex;align-items:center;gap:7px;margin:0;
  font-family:"JetBrains Mono",monospace;font-size:11.5px;letter-spacing:.03em;color:var(--muted)}
/* The lock is --good and appears on ONE page. A padlock repeated on every
   credential screen is security theatre; on the payment page it names a real
   property of that page and nothing else. */
.task-label svg{width:13px;height:13px;color:var(--good)}
@media (max-width:620px){.task-label span{display:none}}

/* The foot of a single-task page. Two links, mono, --muted rather than --dim
   because these have to BE read rather than merely support a line already
   read (design-system.md §11, the contrast note). Centred, because a
   single-task page has no other column to align to. */
.task-fine{display:flex;justify-content:center;gap:var(--s-4);
  margin:var(--s-6) 0 var(--s-5);
  font-family:"JetBrains Mono",monospace;font-size:11.5px;letter-spacing:.03em}
.task-fine a{color:var(--muted);text-decoration:none;transition:color .2s}
.task-fine a:hover,.task-fine a:focus-visible{color:var(--primary)}
