/*!************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[14].oneOf[10].use[3]!./app/tokens.css ***!
  \************************************************************************************************************************************************************************************************************************************************************/
/* ============================================================================
   BinaryRise — design tokens
   Implements .docs/plans/design-system.md §1.1–1.5. This file is the ONLY place
   a raw colour, size, duration or z-index literal may be written. Everything
   else in the codebase reads var(--token).

   Imported before globals.css in app/layout.tsx, so the cascade order is
   tokens → component CSS.

   THEME MODEL (§0 header: "Default theme: DARK")
     :root                                     → dark. No data-theme attribute set.
     @media (prefers-color-scheme: light)
       :root:not([data-theme="dark"])          → light, first visit, OS says light.
     :root[data-theme="light"]                 → light, chosen explicitly.
   An explicit data-theme always wins over the OS preference, in both directions.
   The two light blocks below are byte-identical by necessity: a media query
   cannot be folded into a selector list. EDIT BOTH OR NEITHER.
   ============================================================================ */

/* --------------------------------------------------------------------------
   Theme-invariant tokens. Declared once; the light blocks never touch these.
   -------------------------------------------------------------------------- */
:root {
  /* ---- Typography: families (§1.3) ---------------------------------------
     The var(--font-*) references are emitted by next/font/local (app/fonts.ts)
     and are scoped to <html> by the class layout.tsx applies. Each already ends
     in its own generic, so the trailing literals here only ever come into play
     when the variable is MISSING — which is why the family name sits inside the
     var() fallback rather than after it. Written as `var(--x), "Name", ...` the
     literals would be dead: an undefined var() with no fallback makes the whole
     declaration invalid at computed-value time and font-family unsets entirely,
     so the tail is never reached. Inside the fallback it actually degrades. */
  --font-display: var(--font-space-grotesk, "Space Grotesk"), "SF Pro Display", system-ui, sans-serif;
  --font-body: var(--font-inter, "Inter"), system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: var(--font-jetbrains-mono, "JetBrains Mono"), ui-monospace, SFMono-Regular, Menlo, monospace;

  /* ---- Typography: modular scale (§1.3) ----------------------------------
     Mobile ratio 1.2 / desktop 1.25, base 16px, fluid between 320 and 960px.
     --fs-small and --fs-eyebrow are fixed by the spec, not fluid. */
  --fs-h1: clamp(2.25rem, 1.5rem + 3.75vw, 3.75rem);
  --fs-h2: clamp(1.75rem, 1.25rem + 2.5vw, 2.75rem);
  --fs-h3: clamp(1.375rem, 1.2rem + 0.88vw, 1.75rem);
  --fs-h4: clamp(1.125rem, 1.06rem + 0.31vw, 1.25rem);
  --fs-lead: clamp(1.0625rem, 0.97rem + 0.47vw, 1.25rem);
  --fs-body: clamp(1rem, 0.97rem + 0.16vw, 1.0625rem);
  --fs-small: 0.875rem;
  --fs-eyebrow: 0.75rem;

  /* ---- Spacing, 4px base (§1.4) ------------------------------------------ */
  --space-1: 0.25rem;  /* 4  */
  --space-2: 0.5rem;   /* 8  */
  --space-3: 0.75rem;  /* 12 */
  --space-4: 1rem;     /* 16 */
  --space-5: 1.5rem;   /* 24 */
  --space-6: 2rem;     /* 32 */
  --space-7: 2.5rem;   /* 40 */
  --space-8: 3rem;     /* 48 */
  --space-9: 4rem;     /* 64 */
  --space-10: 5rem;    /* 80 */
  --space-11: 6rem;    /* 96 */
  --space-12: 8rem;    /* 128 */

  /* ---- Radii (§1.5) ------------------------------------------------------ */
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --radius-xl: 24px;
  --radius-pill: 999px;

  /* ---- Layout (§1.5) ----------------------------------------------------- */
  --container: 1200px;
  --container-wide: 1360px;
  --measure: 68ch;
  --gutter: 20px; /* stepped by viewport below, per §1.4 */

  /* ---- Motion (§1.5) ----------------------------------------------------- */
  --dur-1: 120ms;
  --dur-2: 180ms;
  --dur-3: 240ms;
  --dur-4: 320ms;
  --dur-5: 480ms;
  --dur-6: 700ms;
  --ease-out: cubic-bezier(0.33, 1, 0.68, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-emph: cubic-bezier(0.2, 0, 0, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* ---- Z-index (§1.5) ---------------------------------------------------- */
  --z-base: 0;
  --z-raised: 10;
  --z-sticky-nav: 100;
  --z-nav-sheet: 200;
  --z-toast: 300;
  --z-modal: 400;
  /* §1.5 does not name the mobile sticky CTA bar (§2.1). It must sit above page
     content but below the nav bar and the nav sheet, which is the ordering the
     mockup shipped (55 < 60 < 70). */
  --z-sticky-cta: 90;
}

/* Page gutter (§1.4): 20px <480 · 24px 480–767 · 32px 768–1023 · 48px ≥1024 */
@media (min-width: 480px)  { :root { --gutter: 24px } }
@media (min-width: 768px)  { :root { --gutter: 32px } }
@media (min-width: 1024px) { :root { --gutter: 48px } }

/* --------------------------------------------------------------------------
   DARK theme — the default (§1.1). Contrast ratios in the comments are the
   spec's tabulated figures, re-verified against the WebAIM contrast checker.
   -------------------------------------------------------------------------- */
:root {
  color-scheme: dark;

  --bg: #0A0E14;
  --surface: #111825;
  --elevated: #182132;
  --border: #26314A;
  --border-strong: #61708C;       /* interactive borders — 3.56:1 on surface */

  --primary: #4C8DFF;             /* 6.04:1 on bg */
  --primary-hover: #6EA2FF;
  --primary-pressed: #3A78E0;
  --primary-subtle: rgba(76, 141, 255, 0.12);
  --accent: #5EEAD4;              /* 13.07:1 on bg */
  --accent-subtle: rgba(94, 234, 212, 0.10);

  --text-primary: #F3F6FB;        /* 17.85:1 on bg */
  --text-secondary: #AEBACD;      /*  9.86:1 on bg */
  --text-muted: #8A96A9;          /*  6.46:1 on bg */

  --success: #3DDC97;
  --error: #FF6B6B;
  --focus-ring: #7DA9FF;          /* 2px ring + 2px offset on every focusable */

  /* §1.2 rule: button ink is a token, never a literal. Dark theme puts DARK ink
     on the blue fill — 6.04:1. White on that fill is 3.20:1 and fails. */
  --on-primary: #0A0E14;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .40);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, .45);
  --shadow-lg: 0 20px 48px rgba(0, 0, 0, .55);
  --edge-hi: inset 0 1px 0 rgba(255, 255, 255, .05); /* card top highlight */
  --glow-primary: 0 0 0 1px rgba(76, 141, 255, .35), 0 8px 32px rgba(76, 141, 255, .18);

  --logo-treat: grayscale(1);   /* no opacity(): it crushed logo-name contrast */

  /* Hero backdrop scrim. Not a §1 token — it belongs to the hero treatment
     (see globals.css) — but it varies by theme, so it lives with the theme
     blocks rather than being a fourth place that has to know about theming. */
  --hero-scrim: linear-gradient(100deg,
    rgba(10, 14, 20, .96) 0%, rgba(10, 14, 20, .88) 38%,
    rgba(10, 14, 20, .62) 68%, rgba(10, 14, 20, .70) 100%);
}

/* --------------------------------------------------------------------------
   LIGHT theme (§1.2) — via OS preference, until the toggle says otherwise.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;

    --bg: #F7F9FC;
    --surface: #FFFFFF;
    --elevated: #FFFFFF;          /* separated by shadow, not tint */
    --border: #DFE5EF;
    --border-strong: #7E8CA3;     /* 3.41:1 on white */

    --primary: #1D4FD8;           /* 6.30:1 on bg */
    --primary-hover: #1740B5;
    --primary-pressed: #12328F;
    --primary-subtle: rgba(29, 79, 216, 0.08);
    --accent: #0F766E;            /* 5.47:1 on white */
    --accent-subtle: rgba(15, 118, 110, 0.08);

    --text-primary: #0B1220;      /* 17.75:1 on bg */
    --text-secondary: #48566E;    /*  7.03:1 on bg */
    --text-muted: #5E6C84;        /*  5.04:1 on bg */

    --success: #067A52;
    --error: #C1382F;
    --focus-ring: #1D4FD8;

    /* Light theme flips the ink to white — 6.64:1 on the blue fill. */
    --on-primary: #FFFFFF;

    --shadow-sm: 0 1px 2px rgba(11, 18, 32, .06);
    --shadow-md: 0 8px 24px rgba(11, 18, 32, .08);
    --shadow-lg: 0 24px 56px rgba(11, 18, 32, .12);
    --edge-hi: none;
    --glow-primary: 0 0 0 1px rgba(29, 79, 216, .25), 0 8px 28px rgba(29, 79, 216, .14);

    --logo-treat: grayscale(1);   /* no opacity(): measured 2.90:1 in light theme */

    /* Opacities raised from .985/.95/.88/.92. The ghost button's border is
       --border-strong, documented at 3.41:1 on white — but it sits on this scrim, not
       on white, and the plate showing through dropped it to a measured 2.93:1, under
       the 3:1 that WCAG 1.4.11 requires for a control boundary. With no fill, that
       border is the only thing marking the button. */
    --hero-scrim: linear-gradient(100deg,
      rgba(247, 249, 252, .995) 0%, rgba(247, 249, 252, .985) 38%,
      rgba(247, 249, 252, .96) 68%, rgba(247, 249, 252, .975) 100%);
  }
}

/* --------------------------------------------------------------------------
   LIGHT theme — chosen explicitly by the toggle. Identical to the block above.
   -------------------------------------------------------------------------- */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #F7F9FC;
  --surface: #FFFFFF;
  --elevated: #FFFFFF;
  --border: #DFE5EF;
  --border-strong: #7E8CA3;

  --primary: #1D4FD8;
  --primary-hover: #1740B5;
  --primary-pressed: #12328F;
  --primary-subtle: rgba(29, 79, 216, 0.08);
  --accent: #0F766E;
  --accent-subtle: rgba(15, 118, 110, 0.08);

  --text-primary: #0B1220;
  --text-secondary: #48566E;
  --text-muted: #5E6C84;

  --success: #067A52;
  --error: #C1382F;
  --focus-ring: #1D4FD8;

  --on-primary: #FFFFFF;

  --shadow-sm: 0 1px 2px rgba(11, 18, 32, .06);
  --shadow-md: 0 8px 24px rgba(11, 18, 32, .08);
  --shadow-lg: 0 24px 56px rgba(11, 18, 32, .12);
  --edge-hi: none;
  --glow-primary: 0 0 0 1px rgba(29, 79, 216, .25), 0 8px 28px rgba(29, 79, 216, .14);

  --logo-treat: grayscale(1);   /* no opacity(): measured 2.90:1 in light theme */

  /* Keep in step with the media-query block above — see the note there. */
  --hero-scrim: linear-gradient(100deg,
    rgba(247, 249, 252, .995) 0%, rgba(247, 249, 252, .985) 38%,
    rgba(247, 249, 252, .96) 68%, rgba(247, 249, 252, .975) 100%);
}

/*!*************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[14].oneOf[10].use[3]!./app/globals.css ***!
  \*************************************************************************************************************************************************************************************************************************************************************/
*, ::before, ::after {
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  --tw-skew-x: 0;
  --tw-skew-y: 0;
  --tw-scale-x: 1;
  --tw-scale-y: 1;
  --tw-pan-x:  ;
  --tw-pan-y:  ;
  --tw-pinch-zoom:  ;
  --tw-scroll-snap-strictness: proximity;
  --tw-gradient-from-position:  ;
  --tw-gradient-via-position:  ;
  --tw-gradient-to-position:  ;
  --tw-ordinal:  ;
  --tw-slashed-zero:  ;
  --tw-numeric-figure:  ;
  --tw-numeric-spacing:  ;
  --tw-numeric-fraction:  ;
  --tw-ring-inset:  ;
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgb(59 130 246 / 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
  --tw-blur:  ;
  --tw-brightness:  ;
  --tw-contrast:  ;
  --tw-grayscale:  ;
  --tw-hue-rotate:  ;
  --tw-invert:  ;
  --tw-saturate:  ;
  --tw-sepia:  ;
  --tw-drop-shadow:  ;
  --tw-backdrop-blur:  ;
  --tw-backdrop-brightness:  ;
  --tw-backdrop-contrast:  ;
  --tw-backdrop-grayscale:  ;
  --tw-backdrop-hue-rotate:  ;
  --tw-backdrop-invert:  ;
  --tw-backdrop-opacity:  ;
  --tw-backdrop-saturate:  ;
  --tw-backdrop-sepia:  ;
  --tw-contain-size:  ;
  --tw-contain-layout:  ;
  --tw-contain-paint:  ;
  --tw-contain-style:  ;
}

::backdrop {
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  --tw-skew-x: 0;
  --tw-skew-y: 0;
  --tw-scale-x: 1;
  --tw-scale-y: 1;
  --tw-pan-x:  ;
  --tw-pan-y:  ;
  --tw-pinch-zoom:  ;
  --tw-scroll-snap-strictness: proximity;
  --tw-gradient-from-position:  ;
  --tw-gradient-via-position:  ;
  --tw-gradient-to-position:  ;
  --tw-ordinal:  ;
  --tw-slashed-zero:  ;
  --tw-numeric-figure:  ;
  --tw-numeric-spacing:  ;
  --tw-numeric-fraction:  ;
  --tw-ring-inset:  ;
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgb(59 130 246 / 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
  --tw-blur:  ;
  --tw-brightness:  ;
  --tw-contrast:  ;
  --tw-grayscale:  ;
  --tw-hue-rotate:  ;
  --tw-invert:  ;
  --tw-saturate:  ;
  --tw-sepia:  ;
  --tw-drop-shadow:  ;
  --tw-backdrop-blur:  ;
  --tw-backdrop-brightness:  ;
  --tw-backdrop-contrast:  ;
  --tw-backdrop-grayscale:  ;
  --tw-backdrop-hue-rotate:  ;
  --tw-backdrop-invert:  ;
  --tw-backdrop-opacity:  ;
  --tw-backdrop-saturate:  ;
  --tw-backdrop-sepia:  ;
  --tw-contain-size:  ;
  --tw-contain-layout:  ;
  --tw-contain-paint:  ;
  --tw-contain-style:  ;
}/*
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
*//*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/

*,
::before,
::after {
  box-sizing: border-box; /* 1 */
  border-width: 0; /* 2 */
  border-style: solid; /* 2 */
  border-color: currentColor; /* 2 */
}

::before,
::after {
  --tw-content: '';
}

/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/

html,
:host {
  line-height: 1.5; /* 1 */
  -webkit-text-size-adjust: 100%; /* 2 */
  -moz-tab-size: 4; /* 3 */
  -o-tab-size: 4;
     tab-size: 4; /* 3 */
  font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
  font-feature-settings: normal; /* 5 */
  font-variation-settings: normal; /* 6 */
  -webkit-tap-highlight-color: transparent; /* 7 */
}

/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/

body {
  margin: 0; /* 1 */
  line-height: inherit; /* 2 */
}

/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/

hr {
  height: 0; /* 1 */
  color: inherit; /* 2 */
  border-top-width: 1px; /* 3 */
}

/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/

abbr:where([title]) {
  -webkit-text-decoration: underline dotted;
          text-decoration: underline dotted;
}

/*
Remove the default font size and weight for headings.
*/

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: inherit;
  font-weight: inherit;
}

/*
Reset links to optimize for opt-in styling instead of opt-out.
*/

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

/*
Add the correct font weight in Edge and Safari.
*/

b,
strong {
  font-weight: bolder;
}

/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/

code,
kbd,
samp,
pre {
  font-family: var(--font-mono); /* 1 */
  font-feature-settings: normal; /* 2 */
  font-variation-settings: normal; /* 3 */
  font-size: 1em; /* 4 */
}

/*
Add the correct font size in all browsers.
*/

small {
  font-size: 80%;
}

/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/

table {
  text-indent: 0; /* 1 */
  border-color: inherit; /* 2 */
  border-collapse: collapse; /* 3 */
}

/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/

button,
input,
optgroup,
select,
textarea {
  font-family: inherit; /* 1 */
  font-feature-settings: inherit; /* 1 */
  font-variation-settings: inherit; /* 1 */
  font-size: 100%; /* 1 */
  font-weight: inherit; /* 1 */
  line-height: inherit; /* 1 */
  letter-spacing: inherit; /* 1 */
  color: inherit; /* 1 */
  margin: 0; /* 2 */
  padding: 0; /* 3 */
}

/*
Remove the inheritance of text transform in Edge and Firefox.
*/

button,
select {
  text-transform: none;
}

/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/

button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
  -webkit-appearance: button; /* 1 */
  background-color: transparent; /* 2 */
  background-image: none; /* 2 */
}

/*
Use the modern Firefox focus style for all focusable elements.
*/

:-moz-focusring {
  outline: auto;
}

/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/

:-moz-ui-invalid {
  box-shadow: none;
}

/*
Add the correct vertical alignment in Chrome and Firefox.
*/

progress {
  vertical-align: baseline;
}

/*
Correct the cursor style of increment and decrement buttons in Safari.
*/

::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
  height: auto;
}

/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/

[type='search'] {
  -webkit-appearance: textfield; /* 1 */
  outline-offset: -2px; /* 2 */
}

/*
Remove the inner padding in Chrome and Safari on macOS.
*/

::-webkit-search-decoration {
  -webkit-appearance: none;
}

/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/

::-webkit-file-upload-button {
  -webkit-appearance: button; /* 1 */
  font: inherit; /* 2 */
}

/*
Add the correct display in Chrome and Safari.
*/

summary {
  display: list-item;
}

/*
Removes the default spacing and border for appropriate elements.
*/

blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
  margin: 0;
}

fieldset {
  margin: 0;
  padding: 0;
}

legend {
  padding: 0;
}

ol,
ul,
menu {
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
Reset default styling for dialogs.
*/
dialog {
  padding: 0;
}

/*
Prevent resizing textareas horizontally by default.
*/

textarea {
  resize: vertical;
}

/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/

input::-moz-placeholder, textarea::-moz-placeholder {
  opacity: 1; /* 1 */
  color: #9ca3af; /* 2 */
}

input::placeholder,
textarea::placeholder {
  opacity: 1; /* 1 */
  color: #9ca3af; /* 2 */
}

/*
Set the default cursor for buttons.
*/

button,
[role="button"] {
  cursor: pointer;
}

/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
  cursor: default;
}

/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
   This can trigger a poorly considered lint error in some tools but is included by design.
*/

img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
  display: block; /* 1 */
  vertical-align: middle; /* 2 */
}

/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/

img,
video {
  max-width: 100%;
  height: auto;
}

/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) {
  display: none;
}
.container {
  width: 100%;
}
@media (min-width: 480px) {

  .container {
    max-width: 480px;
  }
}
@media (min-width: 768px) {

  .container {
    max-width: 768px;
  }
}
@media (min-width: 1024px) {

  .container {
    max-width: 1024px;
  }
}
@media (min-width: 1280px) {

  .container {
    max-width: 1280px;
  }
}
@media (min-width: 1536px) {

  .container {
    max-width: 1536px;
  }
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
.visible {
  visibility: visible;
}
.invisible {
  visibility: hidden;
}
.collapse {
  visibility: collapse;
}
.static {
  position: static;
}
.fixed {
  position: fixed;
}
.relative {
  position: relative;
}
.sticky {
  position: sticky;
}
.block {
  display: block;
}
.inline {
  display: inline;
}
.flex {
  display: flex;
}
.table {
  display: table;
}
.grid {
  display: grid;
}
.hidden {
  display: none;
}
.shrink {
  flex-shrink: 1;
}
.grow {
  flex-grow: 1;
}
.transform {
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.resize {
  resize: both;
}
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.border {
  border-width: 1px;
}
.uppercase {
  text-transform: uppercase;
}
.italic {
  font-style: italic;
}
.ring {
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.blur {
  --tw-blur: blur(8px);
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.filter {
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.backdrop-filter {
  backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}
.transition {
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
  transition-timing-function: var(--ease-out);
  transition-duration: var(--dur-2);
}

/* ============================================================
   Component styles, ported from .docs/design/mockup.html (<style>
   block). Do not reformat, rename or "improve" — the ported JS and
   the theme toggle depend on these exact selectors.

   Design tokens are NOT here: they live in ./tokens.css, which
   app/layout.tsx imports immediately before this file. Every colour,
   size, duration and z-index below must be a var(--token) read from
   there — see design-system.md §1.
   ============================================================ */

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion: no-preference){html{scroll-behavior:smooth}}
body{
  margin:0; background:var(--bg); color:var(--text-primary);
  font-family:var(--font-body); font-size:var(--fs-body); line-height:1.65;
  overflow-x:clip;
}
h1,h2,h3{font-family:var(--font-display); text-wrap:balance; margin:0}
h1{font-size:var(--fs-h1);font-weight:700;line-height:1.05;letter-spacing:-.02em;max-width:20ch}
h2{font-size:var(--fs-h2);font-weight:700;line-height:1.12;letter-spacing:-.015em;max-width:26ch}
h3{font-size:var(--fs-h3);font-weight:500;line-height:1.25;letter-spacing:-.01em}
h4{font-size:var(--fs-h4);font-weight:600;line-height:1.35;margin:0}
p{margin:0;max-width:var(--measure)}
a{color:var(--primary)}
/* THE focus ring. §1.1 gives it 2px + 2px offset on --focus-ring (8.28:1 dark /
   6.30:1 light). Defined exactly once and reached two ways: the bare :focus-visible
   catches anything that forgets, and `.focus-ring` is opted into explicitly by every
   ui/ primitive so "never a bare outline:none" is enforced at the source rather than
   depending on nobody overriding it later. */
:focus-visible,
.focus-ring:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
/* border-radius only where the element has none of its own. Applied to the shared
   rule it overrode .btn's --radius-md and visibly squared every focused button
   (measured 10px -> 4px in all three engines). */
:focus-visible:not(.btn):not(.control):not(.iconbtn):not(.card-link){border-radius:4px}

.wrap{max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}

/* scroll-padding-BOTTOM only. It keeps anything the browser scrolls to on its own —
   most importantly a focused control — clear of the fixed CTA bar, which was covering
   focused links outright (WCAG 2.4.11). Mobile-only, because that bar is.

   Keeping content clear of the two fixed bars, per element rather than per scroller.

   scroll-padding-top on :root looks like the obvious fix and is not: it applies to
   the scroller, so focusing any control INSIDE the sticky header made the browser
   try to push that control below the padding — which it cannot, because the header
   is sticky — and the page jumped from y=500 to y=83. It also failed to clear flow
   controls anyway (measured rectTop 6 vs header bottom 56). scroll-margin on the
   targets themselves has neither problem, and header controls simply opt out.

   scroll-padding-BOTTOM is safe and stays: nothing lives inside the sticky CTA bar
   that can take focus. */
@media(max-width:1023.98px){:root{scroll-padding-bottom:96px}}

/* Anchor targets — §2.3. #process is an <ol>, so it needs this explicitly or the
   hero's "See how we work" drops the reader behind the bar. */
section,#process{scroll-margin-top:76px}
/* Focus targets: everything the browser may scroll to on focus, EXCLUDING the header
   (WCAG 2.4.11 — reverse Tab was parking form fields and card links under the bar). */
main a,main button,main input,main select,main textarea,main summary,main [tabindex],
footer a,footer button,footer summary{scroll-margin-top:76px}
@media(min-width:1024px){
  section,#process{scroll-margin-top:92px}
  main a,main button,main input,main select,main textarea,main summary,main [tabindex],
  footer a,footer button,footer summary{scroll-margin-top:92px}
}
/* Sections are focused programmatically after a nav click so the focus point follows
   the viewport (see lib/nav.ts). They are not interactive, so suppress the ring that
   would otherwise outline an entire section. */
section[tabindex="-1"]:focus,section[tabindex="-1"]:focus-visible{outline:none}

section{padding-block:var(--space-9)}
@media(min-width:768px){section{padding-block:var(--space-10)}}
@media(min-width:1024px){section{padding-block:var(--space-12)}}

.eyebrow{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);font-weight:500;
  text-transform:uppercase;letter-spacing:.12em;color:var(--text-secondary);
  display:flex;align-items:center;gap:var(--space-3);margin:0 0 var(--space-3)
}
.eyebrow::before{content:"";width:24px;height:2px;background:var(--accent);flex:none}
.sec-head{margin-bottom:var(--space-7)}
/* :not(.eyebrow) matters — this rule is more specific than .eyebrow, so without it
   every section-head eyebrow rendered at --fs-lead (17-20px) instead of the §1.3
   12px mono. */
.sec-head p:not(.eyebrow){color:var(--text-secondary);font-size:var(--fs-lead);margin-top:var(--space-3)}

/* ---- buttons ---- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);
  border-radius:var(--radius-md);   /* sizing lives on .btn-sm/-md/-lg only */
  font-family:var(--font-body);font-size:1rem;font-weight:600;
  text-decoration:none;border:1px solid transparent;cursor:pointer;
  -webkit-tap-highlight-color:transparent;   /* §4.2 "Touch" — real :active replaces it */
  transition:background-color var(--dur-2) var(--ease-out),
             transform var(--dur-2) var(--ease-out),
             border-color var(--dur-2) var(--ease-out);
}
.btn-primary{background:var(--primary);color:var(--on-primary)}
.btn-ghost{background:transparent;color:var(--text-primary);border-color:var(--border-strong)}
@media(hover:hover) and (pointer:fine){
  .btn-primary:hover{background:var(--primary-hover);transform:translateY(-1px)}
  /* §4.2 ghost row: border -> --border-strong AND bg -> --primary-subtle. The fill
     half was missing, which left the hover state reading as a colour shift only. */
  .btn-ghost:hover{border-color:var(--primary);color:var(--primary);background:var(--primary-subtle)}
}
/* §4.2: active is scale(0.98) at 120ms, on buttons AND cards, touch included. */
.btn:active{transform:scale(.98);transition-duration:var(--dur-1)}
.btn-primary:active{background:var(--primary-pressed)}
.card:active{transform:scale(.98);transition-duration:var(--dur-1)}
/* Three sizes, §8. md is the spec'd default: §5.1's 52px submit and §6's "Primary
   CTAs are 52px tall". sm is the 40px nav-bar button. lg is not given a value by the
   spec — 60px keeps the 4px rhythm and is the only invented number here. */
/* §5.3 needs a disabled/loading submit; nothing rendered one before — a disabled
   Button was pixel-identical to an enabled one. */
.btn:disabled,.btn[aria-disabled="true"]{
  opacity:.55;cursor:not-allowed;transform:none;
  background:var(--elevated);color:var(--text-muted);border-color:var(--border)
}
.control:disabled{opacity:.6;cursor:not-allowed}
/* Horizontal padding is one step up the space scale from where it started (16/32/40):
   at the old values the label sat too close to the edge, most visibly on .btn-sm in the
   header. Heights are unchanged — they are spec'd (52px, and the 44px tap floor). */
.btn-sm{min-height:44px;   /* §6 tap-target floor; was 40px */
  padding:0 var(--space-5);font-size:var(--fs-small)}
.btn-md{min-height:52px;padding:0 var(--space-7)}
.btn-lg{min-height:60px;padding:0 var(--space-8);font-size:var(--fs-lead)}
.tlink{
  font-family:var(--font-body);font-size:var(--fs-small);font-weight:600;
  color:var(--primary);text-decoration:none;display:inline-flex;gap:var(--space-2);align-items:center;
  min-height:44px   /* §6 tap-target floor — the text alone measured 35px */
}
.tlink::after{content:"→";transition:transform 160ms var(--ease-out)}
/* §4.2 scopes the entire hover table to "desktop pointer only". Guarding at the
   primitive means no section has to remember to. */
@media(hover:hover) and (pointer:fine){
  .tlink:hover{color:var(--primary-hover)}
  .tlink:hover::after{transform:translateX(3px)}
}

/* ---- mockup banner ---- */
/* Height reserved for two lines: this banner sits above the sticky header, so when
   the mono webfont swaps in and the text re-wraps it moved the entire document down
   17-18px — measured CLS 0.296 at 390px against a 0.05 budget. */
.mockbar{
  min-height:52px;display:flex;align-items:center;justify-content:center;
  background:var(--accent-subtle);border-bottom:1px solid var(--border);
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);letter-spacing:.08em;text-transform:uppercase;
  color:var(--text-secondary);text-align:center;padding:var(--space-2) var(--space-4)
}

/* ---- header ----------------------------------------------------------------
   Two elements, deliberately:
     .hdr        the flow box. Sticky, CONSTANT height, no background. Its height
                 never animates, so the page never reflows when the bar condenses.
     .hdr-inner  the painted bar. Absolutely positioned inside .hdr, and the only
                 thing whose height changes (72 -> 60px on desktop, §2.2).
   §4.3 gets the same no-reflow property by assuming position:fixed. Sticky keeps the
   draft MockBar above us working and leaves the hero's own spacing untouched; the
   cost is a 12px dead strip below the condensed bar, which is why .hdr sets
   pointer-events:none and .hdr-inner puts them back. */
.hdr{
  position:sticky;top:0;z-index:var(--z-sticky-nav);
  height:56px;pointer-events:none
}
.hdr-inner{
  position:absolute;inset:0 0 auto 0;height:56px;pointer-events:auto;
  display:flex;align-items:center;
  border-bottom:1px solid transparent;
  transition:background-color var(--dur-3) var(--ease-out),
             border-color var(--dur-3) var(--ease-out),
             height var(--dur-3) var(--ease-out)
}
/* §2.2: over the hero the bar is transparent with no border. After scrollY > 8:
   --elevated at 88%, 1px bottom --border, --shadow-sm, and 72 -> 60px. */
.hdr.is-scrolled .hdr-inner{
  background:color-mix(in srgb,var(--elevated) 88%,transparent);
  border-bottom-color:var(--border);
  box-shadow:var(--shadow-sm)
}
@media(min-width:1024px){
  .hdr{height:72px}
  .hdr-inner{height:72px}
  .hdr.is-scrolled .hdr-inner{height:60px}
}
.hdr .wrap{display:flex;align-items:center;justify-content:space-between;width:100%;gap:var(--space-4)}
.brand{font-family:var(--font-display);font-weight:700;font-size:1.125rem;letter-spacing:-.02em;color:var(--text-primary);text-decoration:none;white-space:nowrap}
.brand span{color:var(--primary)}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 8px; 
  text-decoration: none;
}

.brand-logo {
  width: 24px; 
  height: 24px; 
  -o-object-fit: contain; 
     object-fit: contain;
}

.brand-text span {
  color: #6ddbdb;
  font-weight: bold;
}
.navlinks{display:none}
@media(min-width:1024px){
  .navlinks{display:flex;position:relative;gap:var(--space-6);list-style:none;margin:0;padding:0}
  .navlinks a{
    font-size:var(--fs-small);font-weight:500;color:var(--text-secondary);text-decoration:none;
    padding-block:var(--space-2);transition:color var(--dur-3) var(--ease-emph)  /* §4.2 */
  }
  .navlinks a:hover{color:var(--text-primary)}
  /* Covers both aria-current="true" (scroll-spy) and "page" (blog route, §2.4). */
  .navlinks a[aria-current]{color:var(--text-primary)}
  /* §2.3: "a 2px --primary bar under the desktop link, moved with transform
     translateX() + width on a single shared element (240ms --ease-emph), not
     per-link borders." Position and width are set inline by NavBar; everything
     that moves is animated here. */
  .navlinks-indicator{
    position:absolute;left:0;bottom:0;height:2px;width:0;
    background:var(--primary);border-radius:var(--radius-pill);pointer-events:none;
    transition:transform var(--dur-3) var(--ease-emph),
               width var(--dur-3) var(--ease-emph),
               opacity var(--dur-2) var(--ease-out)
  }
}
.hdr-actions{display:flex;align-items:center;gap:var(--space-2)}
/* 44x44 is §2.1's explicit number for these two controls ("theme toggle (44x44) +
   hamburger (44x44)"). §6 asks for 48x48 for "anything in the sticky bars", which
   these are. The two clauses conflict; the more specific one wins. 48x48 would fit
   (48+8+48+gutters = 124px of 320), so this is a decision, not a constraint. */
.iconbtn{
  width:44px;height:44px;display:grid;place-items:center;background:none;border:1px solid transparent;
  border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer
}
@media(hover:hover) and (pointer:fine){.iconbtn:hover{color:var(--text-primary);border-color:var(--border)}}

/* ---- theme toggle ----------------------------------------------------------
   Which icon is visible is decided here, not in JS, and the selectors mirror the
   three-way theme cascade in tokens.css exactly. That is deliberate: the server
   render cannot know the visitor's theme, so if the icon depended on JS state it
   would flash the wrong glyph on every load. Keep these three rule groups in step
   with the three token blocks.

   Motion is §4's spec line for this control: rotate 0->180deg + crossfade, 320ms,
   --ease-spring. */
.theme-toggle .theme-icon{
  grid-area:1/1;                       /* .iconbtn is a grid — stack both icons */
  transition:opacity var(--dur-4) var(--ease-spring),transform var(--dur-4) var(--ease-spring)
}
.theme-toggle .theme-icon-moon{opacity:1;transform:rotate(0)}
.theme-toggle .theme-icon-sun{opacity:0;transform:rotate(-180deg)}
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]) .theme-toggle .theme-icon-moon{opacity:0;transform:rotate(180deg)}
  :root:not([data-theme="dark"]) .theme-toggle .theme-icon-sun{opacity:1;transform:rotate(0)}
}
:root[data-theme="light"] .theme-toggle .theme-icon-moon{opacity:0;transform:rotate(180deg)}
:root[data-theme="light"] .theme-toggle .theme-icon-sun{opacity:1;transform:rotate(0)}
@media (prefers-reduced-motion: reduce){
  .theme-toggle .theme-icon{transition:none}
}
/* §2.1: "No CTA button in the 56px bar — it crowds the tap targets." Desktop only. */
.hdr .btn{display:none}
@media(min-width:1024px){.hdr .btn{display:inline-flex}}

/* ---- hamburger -> X (§4.3: 300ms, --ease-emph) ------------------------------
   300ms is not on the §1.5 duration scale (120/180/240/320/480/700). §4.3 states it
   outright, so it is kept as a named component-scoped value rather than being
   rounded to --dur-4 or left as a bare magic number. Same reasoning applies to the
   200ms backdrop and 280ms link timings below. */
.burger{--dur-burger:300ms}
@media(min-width:1024px){.burger{display:none}}
.burger-box{position:relative;display:block;width:20px;height:14px}
.burger-bar{
  position:absolute;left:0;width:20px;height:2px;border-radius:var(--radius-pill);
  background:currentColor;
  transition:transform var(--dur-burger) var(--ease-emph),opacity var(--dur-burger) var(--ease-emph)
}
.burger-bar:nth-child(1){top:0}
.burger-bar:nth-child(2){top:6px}
.burger-bar:nth-child(3){top:12px}
/* Two bars rotate +/-45deg into the X; the middle one fades and collapses. */
.burger[aria-expanded="true"] .burger-bar:nth-child(1){transform:translateY(6px) rotate(45deg)}
.burger[aria-expanded="true"] .burger-bar:nth-child(2){opacity:0;transform:scaleX(0)}
.burger[aria-expanded="true"] .burger-bar:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

/* ---- mobile sheet (§2.1 / §4.3) ---------------------------------------------
   Open and close are different animations, so the closed state carries the CLOSE
   transition (180ms --ease-inout) and .open overrides it with the OPEN one
   (320ms --ease-emph). visibility is stepped, delayed on close so the fade shows. */
.sheet{
  position:fixed;inset:0;z-index:var(--z-nav-sheet);
  visibility:hidden;pointer-events:none;
  transition:visibility 0s linear var(--dur-2)
}
/* `transition:none`, NOT `transition:visibility 0s`. The global reduced-motion reset
   rewrites every transition-duration to .01ms !important, which turns a 0s no-op into
   a genuinely RUNNING transition — and a running transition's value has not advanced
   yet on the frame it starts, so computed visibility stays `hidden` and the sheet is
   unfocusable exactly when it appears. transition-property:none has nothing for the
   reset to lengthen, so visibility flips synchronously. */
.sheet.open{visibility:visible;pointer-events:auto;transition:none}
@media(min-width:1024px){.sheet{display:none}}

.sheet-backdrop{
  position:absolute;inset:0;
  background:color-mix(in srgb,var(--bg) 98%,transparent);
  opacity:0;transition:opacity var(--dur-2) var(--ease-inout)
}
/* §4.5 #1: backdrop-filter is permitted HERE and nowhere else — "Acceptable: the nav
   sheet while open. Forbidden: the sticky bar, cards, hero." */
@supports (backdrop-filter:blur(8px)){.sheet-backdrop{backdrop-filter:blur(8px)}}
.sheet.open .sheet-backdrop{opacity:1;transition:opacity 200ms var(--ease-emph)}

.sheet-panel{
  position:absolute;inset:0;
  /* §2.1: "Links stack from the vertical middle downward" (§6: never from the top).
     margin-top:auto on .sheet-nav (below) bottom-aligns the stack just above the
     pinned CTA. Deliberately NOT justify-content:flex-end: when the list overflows a
     short viewport, flex-end pushes the overflow off the TOP of a fixed inset:0
     panel where it cannot be scrolled to — at 740x360 the first link was cut in half
     and unreachable. An auto margin collapses to 0 instead and lets the list scroll. */
  display:flex;flex-direction:column;
  /* Bottom padding reserves the pinned CTA's row so the links can never sit under it. */
  padding:var(--space-6) var(--gutter) calc(52px + 24px + var(--space-5) + env(safe-area-inset-bottom));
  opacity:0;transform:translateY(-8px);
  transition:opacity var(--dur-2) var(--ease-inout),transform var(--dur-2) var(--ease-inout)
}
.sheet.open .sheet-panel{
  opacity:1;transform:none;
  transition:opacity var(--dur-4) var(--ease-emph),transform var(--dur-4) var(--ease-emph)
}
/* The panel is full-bleed, so it covers the backdrop and "backdrop tap closes" is
   handled by an onClick in NavSheet that treats any click outside a control as a
   backdrop tap. (It used to be pointer-events:none here, but that also made the link
   list unscrollable, which is what broke short viewports.) */
.sheet-close{position:absolute;top:6px;right:var(--gutter)}

/* §2.1: 20px links in 56px rows, stacked from the vertical middle downward. */
.sheet-nav{
  list-style:none;margin:0;padding:0;
  margin-top:auto;                     /* bottom-align, above the pinned CTA */
  min-height:0;                        /* let it shrink below content height... */
  overflow-y:auto;overscroll-behavior:contain   /* ...and scroll when it must */
}
.sheet-nav li{
  opacity:0;transform:translateY(12px);
  transition:opacity var(--dur-2) var(--ease-inout),transform var(--dur-2) var(--ease-inout)
}
/* §4.3: links follow the panel from 120ms, 280ms each, 45ms stagger. */
.sheet.open .sheet-nav li{
  opacity:1;transform:none;
  transition:opacity 280ms var(--ease-out) calc(120ms + var(--i) * 45ms),
             transform 280ms var(--ease-out) calc(120ms + var(--i) * 45ms)
}
.sheet-nav a{
  display:flex;align-items:center;min-height:56px;font-size:1.25rem;font-weight:600;
  color:var(--text-primary);text-decoration:none;border-bottom:1px solid var(--border)
}
/* §2.1: "primary CTA pinned 24px above the safe-area bottom inset" — thumb zone (§6). */
.sheet-cta{
  position:absolute;left:var(--gutter);right:var(--gutter);
  bottom:calc(24px + env(safe-area-inset-bottom));width:auto
}

/* ---- sticky mobile CTA (§2.1 / §4.3: 320ms --ease-emph) ---- */
.stickycta{
  position:fixed;left:0;right:0;bottom:0;z-index:var(--z-sticky-cta);
  /* §2.1 specifies a 64px bar; §6 puts a 52px button in it. Expressed as a min-height
     rather than padding arithmetic so the 1px top border is accounted for by
     border-box and the total is exactly 64px — padding of 6px measured 65px. The
     safe-area inset is additive on notched devices, per §6. */
  min-height:64px;display:flex;align-items:center;
  padding:0 var(--gutter) env(safe-area-inset-bottom);
  background:color-mix(in srgb,var(--elevated) 96%,transparent);border-top:1px solid var(--border);
  transform:translateY(110%);transition:transform var(--dur-4) var(--ease-emph)
}
.stickycta.show{transform:translateY(0)}
/* §2.1: "Hides ... when the nav sheet is open." NavBar publishes the state on <html>,
   so this stays pure CSS and StickyCTABar needs no knowledge of the sheet. */
:root[data-nav-open="true"] .stickycta{transform:translateY(110%)}
.stickycta .btn{width:100%}
@media(min-width:1024px){.stickycta{display:none}}

/* ---- hero ---- */
.hero{padding-block:var(--space-10) var(--space-9);position:relative;overflow:hidden}
@media(min-width:1024px){.hero{padding-block:var(--space-12)}}
.hero::before{
  content:"";position:absolute;inset:-20% -10% auto -10%;height:70%;pointer-events:none;
  background:radial-gradient(50% 60% at 30% 40%,var(--primary-subtle),transparent 70%);
  opacity:.9
}
.hero .wrap{position:relative}
@media(min-width:1024px){
  .hero .wrap{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-9);align-items:center}
}
.hero p.lead{font-size:var(--fs-lead);color:var(--text-secondary);margin-top:var(--space-5)}
.hero-cta{display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-7)}
.hero-cta .btn{width:100%}
@media(min-width:560px){.hero-cta{flex-direction:row}.hero-cta .btn{width:auto}}
.micro{font-family:var(--font-mono);font-size:var(--fs-small);color:var(--text-muted);margin-top:var(--space-4)}
.capstrip{
  display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
  margin-top:var(--space-8);padding-top:var(--space-5);border-top:1px solid var(--border)
}
@media(min-width:768px){.capstrip{grid-template-columns:repeat(4,1fr)}}
.capstrip span{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;
  letter-spacing:.1em;color:var(--text-secondary);display:flex;align-items:center;gap:var(--space-2)
}
.capstrip span::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none}

/* ---- cards ---- */
.grid{display:grid;gap:var(--space-4)}
@media(min-width:768px){.grid{gap:var(--space-5)}}
.g2{grid-template-columns:1fr}
@media(min-width:768px){.g2{grid-template-columns:1fr 1fr}}
.g3{grid-template-columns:1fr}
@media(min-width:1024px){.g3{grid-template-columns:repeat(3,1fr)}}
.gblog{grid-template-columns:1fr}
@media(min-width:768px){.gblog{grid-template-columns:1fr 1fr}}
@media(min-width:1024px){.gblog{grid-template-columns:repeat(3,1fr)}}

.card{
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);
  padding:20px;box-shadow:var(--edge-hi);
  display:flex;flex-direction:column;gap:var(--space-3);
  transition:transform var(--dur-3) var(--ease-out),
             border-color var(--dur-3) var(--ease-out),
             box-shadow var(--dur-3) var(--ease-out)
}
@media(min-width:768px){.card{padding:28px}}   /* §1.4 card padding */
/* §4.2 card row: translateY(-4px), border -> --primary at 40%, --shadow-md, and the
   icon tile scales. The doubled `.card.card` wins the cascade back from the .reveal
   rules further down, which a card also carries. */
@media(hover:hover) and (pointer:fine){
  .card.card:hover{
    transform:translateY(-4px);
    border-color:color-mix(in srgb,var(--primary) 40%,transparent);
    box-shadow:var(--shadow-md)
  }
  .card.card:hover .tile{transform:scale(1.06)}
}
.card p{color:var(--text-secondary);font-size:var(--fs-small)}
.tile{
  width:44px;height:44px;border-radius:var(--radius-md);background:var(--primary-subtle);
  display:grid;place-items:center;color:var(--primary);flex:none
}
/* Safari + VoiceOver drop list semantics when list-style is none; the components
   carry role="list" to keep the "list, N items" announcement. */
.bullets{list-style:none;margin:var(--space-2) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--space-2)}
.bullets li{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);color:var(--text-secondary);
  padding-left:var(--space-4);position:relative;line-height:1.5
}
.bullets li::before{content:"·";position:absolute;left:4px;color:var(--accent);font-weight:700}
.card .tlink{margin-top:auto;padding-top:var(--space-3)}

.pkg .meta{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;
  letter-spacing:.1em;color:var(--accent)
}

/* ---- process ---- */
.steps{list-style:none;margin:var(--space-8) 0 0;padding:0 0 0 var(--space-7);position:relative;display:flex;flex-direction:column;gap:var(--space-6)}
.steps::before{content:"";position:absolute;left:15px;top:8px;bottom:8px;width:2px;background:var(--accent);opacity:.35}
/* isolation:isolate keeps the node/rail layering below scoped to the step — the
   z-indexes are ordering .num against this step's own rail segment, not competing
   with the sticky nav or the CTA bar. */
.steps li{position:relative;isolation:isolate}
.steps .num{
  position:absolute;left:calc(-1 * var(--space-7));top:0;width:32px;height:32px;border-radius:50%;
  display:grid;place-items:center;background:var(--bg);border:2px solid var(--accent);
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);font-weight:500;color:var(--accent);
  /* The rail runs behind the nodes, never across them: ::after is generated as the
     li's last child and would otherwise paint over .num in tree order. */
  z-index:var(--z-raised)
}
.steps p{color:var(--text-secondary);font-size:var(--fs-small);margin-top:var(--space-2)}
.artifact{
  display:block;margin-top:var(--space-2);font-family:var(--font-mono);font-size:var(--fs-eyebrow);
  color:var(--text-secondary)
}
@media(min-width:1024px){
  .steps{flex-direction:row;padding:var(--space-7) 0 0}
  .steps::before{left:0;right:0;top:15px;bottom:auto;width:auto;height:2px}
  .steps li{flex:1;padding-right:var(--space-5)}
  .steps .num{ /* 12px floor */
left:0;top:calc(-1 * var(--space-7))}
}

/* §3.3 rail as a step progress bar: pointing at a step fills the rail through it
   and lights every node up to and including it, so the four steps read as progress
   rather than as four unrelated columns.

   Each <li> owns the segment of rail running from its own node to the next one,
   rather than one bar spanning the whole rail scaled to a percentage. The steps are
   different heights (mobile) and separated by a gap (both axes), so a percentage
   fill would only land on the nodes if every step were the same size. Per-segment
   geometry lands on them at any breakpoint and survives a step being added or its
   copy getting longer.

   Two states, not one, because a step's node and the connector leaving it fill at
   different moments. Hovering step 3 lights nodes 1-3 (`reached`) but only the two
   connectors *between* them (`passed`), so the bar stops on the node you are
   pointing at. Filling step 3's own outgoing connector too would run the bar all
   the way to node 4 and read as "step 4 done" while node 4 sat unlit.

   Both classes are applied by components/motion/MockupBehaviour.tsx rather than
   being derived here, because the cumulative part needs to select the *earlier*
   siblings of the hovered step and CSS only offers `li:has(~ li:hover)` for that.
   :has() is missing from Firefox ESR 115, and an unparseable selector invalidates
   the whole comma-separated rule it appears in — so the plain `li:hover` half would
   be dropped along with it and the effect would be absent, not degraded.

   Hover-only and purely decorative: no content is hidden behind it, so there is
   nothing here for a keyboard or touch user to miss and no case for making
   non-interactive <li>s focusable to expose it. */
@media(hover:hover) and (pointer:fine){
  .steps li::after{
    content:"";position:absolute;
    left:calc(15px - var(--space-7));top:0;width:2px;height:calc(100% + var(--space-6));
    background:var(--accent);
    transform:scaleY(0);transform-origin:top;
    transition:transform var(--dur-3) var(--ease-out);
    z-index:var(--z-base)
  }
  /* First segment starts where the rail does (top:8px), the last stops where it
     ends (bottom:8px); the middle ones run node-to-node across the flex gap. The
     last one is never `passed` — no step follows it — but it stays bounded so that
     adding a fifth step needs no new geometry. */
  .steps li:first-child::after{top:8px;height:calc(100% - 8px + var(--space-6))}
  .steps li:last-child::after{height:calc(100% - 8px)}
  .steps li.passed::after{transform:scaleY(1)}

  .steps .num{
    transition:background-color var(--dur-3) var(--ease-out),color var(--dur-3) var(--ease-out)
  }
  .steps li.reached .num{background:var(--accent);color:var(--bg)}
}
@media(min-width:1024px) and (hover:hover) and (pointer:fine){
  /* The same segments turned horizontal. first/last repeat top and height because
     their rules above outrank the base one and would otherwise leak the vertical
     geometry into the row layout. */
  .steps li::after{
    left:0;top:calc(15px - var(--space-7));width:calc(100% + var(--space-6));height:2px;
    transform:scaleX(0);transform-origin:left
  }
  .steps li:first-child::after{top:calc(15px - var(--space-7));height:2px}
  .steps li:last-child::after{top:calc(15px - var(--space-7));height:2px;width:100%}
  .steps li.passed::after{transform:scaleX(1)}
}

/* ---- risk reversal ---- */
.risk{display:grid;gap:0;border-top:1px solid var(--border)}
@media(min-width:768px){.risk{grid-template-columns:repeat(3,1fr)}}
.risk div{padding:var(--space-5) 0;border-bottom:1px solid var(--border)}
@media(min-width:768px){
  .risk div{padding:var(--space-5);border-bottom:none;border-left:1px solid var(--border)}
  .risk div:first-child{border-left:none;padding-left:0}
}
.risk dt{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;
  letter-spacing:.1em;color:var(--text-primary);font-weight:500;margin-bottom:var(--space-2)
}
.risk dd{margin:0;color:var(--text-secondary);font-size:var(--fs-small)}

/* ---- logo wall ---- */
.logogroup+.logogroup{margin-top:var(--space-6)}
.logolabel{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;
  letter-spacing:.12em;color:var(--text-secondary);margin-bottom:var(--space-3)
}
.logos{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-4)}  /* §3.4: 16px */
@media(min-width:480px){.logos{grid-template-columns:repeat(4,1fr)}}
@media(min-width:768px){.logos{grid-template-columns:repeat(6,1fr)}}
/* §3.4 specifies a 56px tile around a 32px logo, but that assumed the mark ALONE; this
   wall shows mark + name, so the fixed height becomes a min-height that a two-line
   label ("Google Cloud" at the 3-col mobile width) can push taller. The grid stretches
   the rest of the row to match, so tiles stay flush. */
.logos li{
  list-style:none;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);
  min-height:76px;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--space-2);padding:var(--space-2);
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);letter-spacing:.04em;color:var(--text-secondary);
  text-align:center;filter:var(--logo-treat);transition:filter 200ms var(--ease-out)
}
/* currentColor + the inherited filter, so the glyph picks up the greyscale treatment
   and the hover swap below without restating either. */
.logomark{width:24px;height:24px;flex-shrink:0;fill:currentColor}
@media(hover:hover) and (pointer:fine){.logos li:hover{filter:none;color:var(--text-primary)}}

/* ---- blog ---- */
.post{padding:0;overflow:hidden}
.cover{aspect-ratio:16/9;background:linear-gradient(135deg,var(--elevated),var(--surface));border-bottom:1px solid var(--border);display:grid;place-items:center;color:var(--text-secondary);font-family:var(--font-mono);font-size:var(--fs-eyebrow)}
.post .body{padding:20px;display:flex;flex-direction:column;gap:var(--space-2)}
/* Card title keeps the §3.5 size even though it is an h3 for outline reasons. */
.post h3{font-size:var(--fs-h4);font-weight:600;line-height:1.35;letter-spacing:0}
@media(min-width:768px){.post .body{padding:28px}}
.chip{
  align-self:flex-start;font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;
  letter-spacing:.08em;color:var(--accent);background:var(--accent-subtle);
  padding:2px var(--space-2);border-radius:var(--radius-sm)
}
/* §3.5/§3.2/§6 line clamps. Without these, titles ran 3 lines and service
   descriptions up to 4 at 320px, which is also what made the two service rows
   different heights. */
.post h3,.post .body>p:not(.postmeta){
  display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden
}
.post h3{-webkit-line-clamp:2}
.post .body>p:not(.postmeta){-webkit-line-clamp:2}
.card>p{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
.postmeta{font-size:var(--fs-small);color:var(--text-muted);margin-top:var(--space-2)}

/* §3.5: "Whole card is one link (::after overlay, single tab stop)." One tab stop per
   card rather than three competing ones, and the whole card surface becomes tappable.
   Lives on .card-link so any Card variant can use it, not just blog posts. */
.card:has(.card-link){position:relative}
.post{position:relative}
.card-link{color:inherit;text-decoration:none}
/* Scoped to .card: outside one there is no positioned ancestor, so inset:0 resolved
   against the initial containing block and painted an invisible viewport-sized link
   over the top of the document. */
.card .card-link::after{content:"";position:absolute;inset:0;border-radius:inherit;z-index:0}
/* Lift the card's own text and any sibling control above the overlay. Without this the
   overlay swallowed text selection entirely (no copying a headline, double-click
   navigates instead of selecting) and made any other button/link in the card dead. */
.card .body>p,.card .postmeta,
.card :is(button,input,select,textarea,a):not(.card-link){position:relative;z-index:1}
/* Fallback first: :has() is what moves the ring to the whole card, but an engine
   without :has() must still get an indicator rather than none at all. */
.card-link:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}
@supports selector(:has(*)){.card-link:focus-visible{outline:none}}
.card:has(.card-link:focus-visible){outline:2px solid var(--focus-ring);outline-offset:2px}
@media(hover:hover) and (pointer:fine){.card:hover .card-link{color:var(--primary)}}

/* §3.5 thin state: a real Subscribe card, never a placeholder post card. */
.post-subscribe .body{gap:var(--space-3)}
/* The card body is a flex column, so a button in it stretches to the full width and
   stops reading as a button. `start` keeps it the size of its label — and `auto` on the
   top margin pins it to the bottom, so the card's CTA lines up with the meta row of the
   real post cards beside it instead of floating under a shorter paragraph. */
.post-subscribe .btn{align-self:start;margin-top:auto}

/* §3.5: right-aligned in the section header, not below the grid. */
.sec-head{position:relative}
.blog-all{margin-top:var(--space-4)}
@media(min-width:768px){
  .blog-all{position:absolute;right:0;bottom:0;margin-top:0}
}
.logonote{margin-top:var(--space-5)}
.riskstrip{padding-block:var(--space-8)}

/* ---- contact ---- */
@media(min-width:1024px){
  /* 4fr 8fr, not the 5fr 7fr this shipped with. The form lost half its rows when the
     fields paired up, so the two columns were no longer anywhere near each other in
     height; narrowing the aside wraps its heading and its "what happens next" lines
     the way they already wrap at 1024px, and the width it gives up lets the paired
     half-columns hold a full <select> option label without truncating it. */
  .contact-grid{display:grid;grid-template-columns:4fr 8fr;gap:var(--space-9);align-items:start}
  /* Relative, NOT sticky. design-system.md §5 asks for `position:sticky;top:120px`
     here; that was changed deliberately, so don't "restore" it. */
  .contact-aside{position:relative;align-self:start}
}
.facts{list-style:none;margin:var(--space-6) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--space-3)}
.facts a{color:var(--text-primary);text-decoration:none;font-weight:500}
@media(hover:hover) and (pointer:fine){.facts a:hover{color:var(--primary)}}
.facts span{display:block;font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;letter-spacing:.1em;color:var(--text-secondary)}
.next{list-style:none;margin:var(--space-6) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--space-3)}
.next li{display:flex;gap:var(--space-3);font-size:var(--fs-small);color:var(--text-secondary)}
.next b{font-family:var(--font-mono);font-size:var(--fs-eyebrow);color:var(--accent);flex:none;padding-top:3px}

form{
  margin-top:var(--space-6);background:var(--elevated);border:1px solid var(--border);
  border-radius:var(--radius-xl);padding:var(--space-5);display:flex;flex-direction:column;
  gap:var(--space-5);   /* §5.1: 20px between fields */
  box-shadow:var(--shadow-md)
}
/* From 768px up the card tightens: paired rows (below) halve the number of rows, so
   the 40px padding and the 24px between-row rhythm that suited a nine-row stack now
   just pad an already-short form. §3.6 wants the form to read as the aside's
   counterpart rather than tower over it. */
@media(min-width:768px){form{padding:var(--space-6);gap:var(--space-4)}}
@media(min-width:1024px){form{margin-top:0}}

/* Paired field rows — name|email and company|service (the b-eye.com pattern). Each
   half column keeps its OWN reserved .field-msg row and the pair is as tall as the
   taller half, so §5.2's zero-CLS contract survives the pairing unchanged while the
   form loses one whole field-row per pair. One column below 768px: two ~140px fields
   on a phone would wrap every label. */
.form-row{display:grid;gap:var(--space-5)}
@media(min-width:768px){.form-row{grid-template-columns:1fr 1fr;gap:var(--space-4)}}

/* §5.1: 8px label->input, 20px between fields. */
.field{display:flex;flex-direction:column;gap:var(--space-2)}
.field label{font-size:var(--fs-small);font-weight:600}
/* §5.1: "mark the optional, not the required" — no asterisks anywhere. */
.field-optional{color:var(--text-muted);font-weight:400}
/* §5.2: "The 20px message row is present in layout from the start (no CLS)." This
   reserves the row whether or not there is anything to say. Do not make it
   conditional on there being a message.

   TWO lines are reserved below 768px, not one. §5.2 says 20px, but that assumes a
   one-line message, and its OWN proposed error strings do not fit one line on a
   phone — measured against every string in §5.2, the longest takes 2 lines at 320,
   390 and 480px. Reserving 20px there would have shifted the form by exactly 20px
   the first time a field failed validation, which is the shift the rule exists to
   prevent.

   From 768px up it IS one line, because the second half of that trade was taken:
   the error copy in lib/schema.ts was shortened until every string fits one line in
   the narrowest box it can appear in — a 247px paired half-column at a 1024px
   viewport. That is the "alternative is shorter error copy" this comment used to
   name as the road not taken. Changing a string in schema.ts back to a long one
   silently reintroduces the shift; the widths are in the comment there. */
.field-msg{
  margin:0;font-size:var(--fs-small);line-height:1.43;
  /* em, not px: the text scales with the user's font-size setting but a px
     reservation does not, so raising the browser default to 20px shifted the form by
     100px and to 24px by 220px. Two lines is the MOBILE reservation; the ≥768px
     override below cuts it to one, which the shortened copy in lib/schema.ts pays
     for. Measured floor for that one line is the 247px paired half-column at a
     1024px viewport, where the widest string clears by 35px. */
  min-height:2.86em;
  /* A long unbreakable token (a URL or an email echoed back in an error) otherwise
     forced a 1645px document at a 320px viewport. */
  overflow-wrap:anywhere;
  color:var(--text-secondary)
}
.field--error .field-msg{color:var(--error)}
@media(min-width:768px){.field-msg{min-height:1.43em}}
/* §5.2: error border is --error; the VALID state is --border-strong, never green —
   "a wall of green checkmarks is noise". */
.field--error .control{border-color:var(--error)}
input,select,textarea{
  font-family:var(--font-body);font-size:16px;   /* 16px minimum: below this iOS zooms on focus */
  color:var(--text-primary);background:var(--surface);
  border:1px solid var(--border-strong);border-radius:var(--radius-md);
  padding:var(--space-3) var(--space-4);min-height:52px;width:100%
}
/* Tailwind's preflight sets line-height:inherit on form controls, so a 16px input
   inherited the body's 1.65 and measured 26.4+24+2 = 52.4px — taller than BOTH
   min-heights below, which therefore never bound and quietly cost 4.4px a field. A
   short line box hands the min-height its job back. Not on textarea: it sizes from
   `rows`, and 1.2 would crush multi-line text (it keeps 1.6 in its own rule). */
input,select{line-height:1.2}
/* §5.1: "Field height 52px mobile / 48px desktop." */
@media(min-width:1024px){.control{min-height:48px}}
textarea{min-height:120px;resize:vertical;line-height:1.6}
input::-moz-placeholder, textarea::-moz-placeholder{color:var(--text-muted)}
input::placeholder,textarea::placeholder{color:var(--text-muted)}
.consent{display:flex;flex-direction:column}
.consent-row{display:flex;gap:var(--space-3);align-items:flex-start}
.consent-row input{
  width:22px;height:22px;min-height:22px;flex:none;margin-top:2px;accent-color:var(--primary);
  position:relative
}
/* The box stays 22px; the HIT AREA is expanded to 44x44 with a pseudo-element, per
   §6's floor. Growing the box itself would look wrong next to 22px native styling. */
.consent-row input::before{
  content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:44px;height:44px
}
/* §5.2 wants the error border on the control; the checkbox is not a .control. */
.field--error .consent-row input{outline:2px solid var(--error);outline-offset:2px}
.consent-row label{font-weight:400;font-size:var(--fs-small);color:var(--text-secondary)}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

:root{
  /* The §5.2 alert glyph, inlined so the error state needs no network request
     and cannot flash unstyled. Consumed as a MASK, so it carries no colour of
     its own — see below. */
  --icon-alert:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a7 7 0 100 14A7 7 0 008 1zm0 3a.9.9 0 01.9.9v4.2a.9.9 0 11-1.8 0V4.9A.9.9 0 018 4zm0 8.4a1 1 0 110-2 1 1 0 010 2z'/%3E%3C/svg%3E")
}

/* §5.2 error affordance: icon + border + text, never colour alone. The icon is a
   MASK rather than a background-image, so one shape picks up --error in both themes
   (the token is #FF6B6B dark / #C1382F light — a two-colour SVG would need two
   copies and would drift). */
.control-wrap{position:relative;display:flex;flex-direction:column}
.field--error .control-wrap::after{
  content:"";position:absolute;pointer-events:none;
  right:var(--space-4);top:50%;transform:translateY(-50%);
  width:16px;height:16px;background:var(--error);
  -webkit-mask:var(--icon-alert) center/16px 16px no-repeat;
          mask:var(--icon-alert) center/16px 16px no-repeat
}
/* A textarea has no vertical centre worth aiming at — the icon rides the first line. */
.field--error .control-wrap:has(textarea)::after{top:18px;transform:none}
/* Clear of the native <select> arrow, which owns the right edge already. */
.field--error .control-wrap:has(select)::after{right:calc(var(--space-4) + 22px)}
/* Keep the value from running under the icon. Only in the error state, so the resting
   field keeps its spec'd padding. */
.field--error .control{padding-right:calc(var(--space-4) + 24px)}
.field--error .control-wrap:has(select) .control{padding-right:calc(var(--space-4) + 46px)}

/* ---- contact form: the four states (§5.3) ---- */

/* Loading. Both button faces occupy one grid cell, so the box is always as wide as the
   wider label and the crossfade cannot resize it — §5.3's "button keeps its measured
   width", without measuring. */
.btn-swap{display:grid;place-items:center}
.btn-face{
  grid-area:1/1;display:inline-flex;align-items:center;gap:var(--space-2);
  opacity:0;visibility:hidden;
  transition:opacity var(--dur-2) var(--ease-out),visibility 0s linear var(--dur-2)
}
.btn-face[data-on="true"]{opacity:1;visibility:visible;transition-delay:0s,0s}
/* The submit button is never `disabled` while sending — see ContactForm.tsx. */
.btn[aria-busy="true"]{cursor:progress}
.spinner{
  width:16px;height:16px;flex:none;border-radius:50%;
  border:2px solid currentColor;border-top-color:transparent;
  animation:spin 700ms linear infinite
}
@keyframes spin{to{transform:rotate(360deg)}}

/* Error. §5.3: persistent, 3px --error left border, icon, "Try again", values kept. */
.formbanner{
  display:flex;gap:var(--space-3);align-items:flex-start;
  padding:var(--space-4);border-radius:var(--radius-md);
  border:1px solid var(--border);border-left:3px solid var(--error);
  background:var(--surface)
}
.formbanner-icon{color:var(--error);flex:none;display:flex;padding-top:2px}
.formbanner p{margin:0;font-size:var(--fs-small);color:var(--text-primary)}
.formbanner .tlink{min-height:auto;margin-top:var(--space-2);background:none;border:0;padding:0;cursor:pointer}

/* Success. Replaces the form's content in place — §5.3 is explicit that this must not
   navigate to a thank-you page. min-height is set inline from the form's measured
   height so the page does not jump. */
.successcard{
  margin-top:var(--space-6);background:var(--elevated);border:1px solid var(--border);
  border-radius:var(--radius-xl);padding:var(--space-5);box-shadow:var(--shadow-md);
  display:flex;flex-direction:column;align-items:flex-start;justify-content:center;
  gap:var(--space-4);
  animation:card-in var(--dur-5) var(--ease-out) both
}
/* Tracks the form's padding exactly — this card replaces the form in place, so a
   different inset would be visible as a jump at the moment of the swap. */
@media(min-width:768px){.successcard{padding:var(--space-6)}}
@media(min-width:1024px){.successcard{margin-top:0}}
.successcard h3{margin:0;font-size:var(--fs-h3)}
.successcard p{margin:0;color:var(--text-secondary)}
.successcard .tlink{background:none;border:0;padding:0;cursor:pointer}
@keyframes card-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* §5.3: "Animated --success checkmark (500ms stroke draw)". */
.successcheck{width:52px;height:52px;flex:none;overflow:visible}
.successcheck circle,.successcheck path{
  fill:none;stroke:var(--success);stroke-width:3;
  stroke-linecap:round;stroke-linejoin:round
}
.successcheck circle{
  stroke-dasharray:151;stroke-dashoffset:151;
  animation:draw var(--dur-5) var(--ease-out) forwards
}
.successcheck path{
  stroke-dasharray:36;stroke-dashoffset:36;
  animation:draw 500ms var(--ease-out) var(--dur-3) forwards
}
@keyframes draw{to{stroke-dashoffset:0}}

/* The 150ms fade-out that precedes the swap (§5.3). */
.form--leaving{opacity:0;transition:opacity 150ms var(--ease-out)}

/* Visually hidden but in the a11y tree — the submit-failure live region. Not
   `display:none` and not `.hp`'s left:-9999px: a live region moved off-canvas is fine,
   but this pattern is the one AT vendors test against. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0
}

/* §4.4 keeps "a 150ms opacity-only fade for scroll reveals, nav sheet and the success
   state" under prefers-reduced-motion — change-of-state feedback survives, motion does
   not. Must carry !important and sit after the global reset, exactly like the .reveal
   and nav rules further down this file. */
@media (prefers-reduced-motion: reduce){
  .spinner{animation:none !important;border-top-color:currentColor;opacity:.6}
  .successcard{animation:opacity-in 150ms linear both !important}
  .successcheck circle,.successcheck path{
    animation:none !important;stroke-dashoffset:0 !important
  }
  .form--leaving{transition:opacity 150ms linear !important}
}
@keyframes opacity-in{from{opacity:0}to{opacity:1}}
.formnote{
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);color:var(--text-secondary);
  text-align:center;margin-top:calc(-1 * var(--space-2))
}
.avail{
  display:inline-flex;align-items:center;gap:var(--space-2);align-self:flex-start;
  font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;letter-spacing:.08em;
  color:var(--text-secondary);border:1px solid var(--border);border-radius:var(--radius-pill);
  padding:var(--space-1) var(--space-3);margin-bottom:var(--space-4)
}
.avail::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--success)}

/* ---- footer ---- */
footer{border-top:1px solid var(--border);padding-block:var(--space-8) var(--space-6);margin-bottom:84px}
@media(min-width:1024px){footer{margin-bottom:0}}
.foot-top{display:flex;flex-direction:column;gap:var(--space-6)}
@media(min-width:768px){.foot-top{flex-direction:row;justify-content:space-between;gap:var(--space-9)}}
.foot-top p{color:var(--text-secondary);font-size:var(--fs-small);max-width:34ch;margin-top:var(--space-3)}
.footnav{display:flex;flex-direction:column;gap:0;list-style:none;margin:0;padding:0}
@media(min-width:768px){.footnav{flex-direction:row;gap:var(--space-8);flex-wrap:wrap}}
.footnav h2{font-size:var(--fs-eyebrow);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.12em;color:var(--text-secondary);margin:0}

/* §3.7 mobile: "Nav column groups as accordions (<details>)". Native <details> so the
   groups work with JS disabled and cost nothing to hydrate. */
.footgroup{border-top:1px solid var(--border)}
.footgroup summary{
  display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);
  min-height:44px;                     /* §6 tap target floor */
  padding-block:var(--space-3);
  cursor:pointer;list-style:none
}
.footgroup summary::-webkit-details-marker{display:none}
.footgroup-marker{
  position:relative;width:12px;height:12px;flex:none;
  transition:transform var(--dur-3) var(--ease-out)
}
.footgroup-marker::before,.footgroup-marker::after{
  content:"";position:absolute;background:var(--text-muted);border-radius:var(--radius-pill)
}
.footgroup-marker::before{left:0;right:0;top:5px;height:2px}
.footgroup-marker::after{top:0;bottom:0;left:5px;width:2px;transition:opacity var(--dur-3) var(--ease-out)}
.footgroup[open] .footgroup-marker::after{opacity:0}
.footnav ul{list-style:none;margin:0;padding:0 0 var(--space-4);display:flex;flex-direction:column;gap:var(--space-1)}
/* §6: ">=8px clear space between adjacent targets; 12px in the footer link stacks".
   The 44px row supplies both the target size and the spacing. */
.footnav li a{display:flex;align-items:center;min-height:44px}
.footnav a{font-size:var(--fs-small);color:var(--text-secondary);text-decoration:none}

/* From 768px the accordion is not wanted — §3.7 makes it the MOBILE treatment and
   §6 lists it under "Simplified or hidden below 768px". The groups must therefore be
   forced open, and that is genuinely awkward: CSS cannot set the `open` attribute,
   and a closed <details> hides its content through the UA, not through any rule we
   can simply override on the summary.
   Two mechanisms are needed to cover every engine, and BOTH are load-bearing:
     ::details-content { content-visibility: visible }  — Chromium's current model
     .footgroup > ul   { display: flex }                — the older display:none model
   Verified open and all links visible in Chromium, Firefox and WebKit at 768/1440.
   An earlier version of this block claimed `display:contents` did the job; it did
   not, and the desktop footer shipped with ten invisible links. */
@media(min-width:768px){
  .footgroup{border-top:none}
  .footgroup::details-content{content-visibility:visible;block-size:auto}
  .footgroup>ul{display:flex}
  .footgroup summary{min-height:0;padding-block:0;margin-bottom:var(--space-3);cursor:default}
  .footgroup-marker{display:none}
  .footnav ul{padding-bottom:0;gap:var(--space-2)}
  .footnav li a{min-height:0}
}
@media(hover:hover) and (pointer:fine){.footnav a:hover{color:var(--text-primary)}}
/* §3.7 item 3 contact block. font-style:normal because <address> defaults italic. */
.footcontact{
  display:flex;flex-direction:column;gap:var(--space-1);font-style:normal;
  margin-top:var(--space-6);font-size:var(--fs-small);color:var(--text-secondary)
}
.footcontact a{color:var(--text-secondary);text-decoration:none;display:flex;align-items:center;min-height:44px}
@media(hover:hover) and (pointer:fine){.footcontact a:hover{color:var(--text-primary)}}
@media(min-width:768px){
  .footcontact{flex-direction:row;gap:var(--space-5);align-items:center}
  .footcontact a{min-height:0}
}
.legal{
  margin-top:var(--space-7);padding-top:var(--space-5);border-top:1px solid var(--border);
  font-size:var(--fs-small);color:var(--text-muted);display:flex;flex-wrap:wrap;gap:var(--space-4);justify-content:space-between
}
/* No font-size shrink and no opacity: .8em compounded down to 8.8-9.6px inside
   already-small parents and measured 4.10:1 in light theme — the §7.6 registration
   details were the least readable text on the page. */
.tbd{font-family:var(--font-mono);color:var(--error)}

/* ---- reveal ----
   The pre-reveal state is scoped to `:not(.in)` rather than being cancelled again by
   a `.reveal.in{transform:none}` rule. An element that has revealed then carries no
   reveal styling at all, which is what lets .card's own :hover lift work: the earlier
   shape needed `:not(:hover):not(:active)` here to get out of the way, and that made
   every *non-card* .reveal — section headers, timeline steps, logo groups — fall back
   to translateY(20px) the moment it was hovered, i.e. slide 20px down.

   The transition is applied inline by the observer in components/motion/
   MockupBehaviour.tsx and cleared once it has run, for the same reason: left in a
   `.js .reveal.in` rule it outranks `.card`'s own transition, so card hovers
   inherited the reveal's 420ms duration and its stagger delay instead of --dur-3. */
.js .reveal:not(.in){opacity:0;transform:translateY(20px)}
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}
  .js .reveal:not(.in){transform:none}
  /* !important also beats the observer's inline transition, so motion stays dropped. */
  .js .reveal.in{transition:opacity 150ms linear !important}
}

/* ---- nav under prefers-reduced-motion --------------------------------------
   §4.4 keeps "a 150ms opacity-only fade for scroll reveals, nav sheet and the
   success state" — motion-sensitive users still need change-of-state feedback —
   while every translate/rotate is dropped.

   This MUST sit after the global reset above and MUST carry !important, exactly like
   the .reveal rule: the reset's `*{transition-duration:.01ms !important}` otherwise
   wins and the sheet snaps instead of fading. §4.4 says as much ("define ... inside a
   prefers-reduced-motion: reduce block *after* the reset"). */
@media (prefers-reduced-motion: reduce){
  .hdr-inner,.navlinks-indicator,.burger-bar,.stickycta{transition:none !important}
  .sheet-panel,.sheet.open .sheet-panel,
  .sheet-nav li,.sheet.open .sheet-nav li,
  .sheet-backdrop,.sheet.open .sheet-backdrop{
    transform:none !important;
    transition:opacity 150ms linear !important
  }
}

/* ---- hero: photographic background ----------------------------------------
   Deviation from .docs/design/mockup.html, requested 2026-08-19. The SVG
   diagram was removed and the hero is now a single text column laid over
   public/images/bg.png (1920x1080 datacenter/particle-wave plate).

   The plate is near-black, so it can never sit under text unscrimmed: the
   theme-scoped --hero-scrim gradient is composited above it in the same
   background shorthand. Dark theme keeps the left third close to --bg and
   opens up toward the right, where the image's rack detail lives; light theme
   uses a near-opaque light scrim so the same plate reads as faint texture
   instead of a black slab under near-black text. --hero-scrim is declared in
   tokens.css with the other theme-varying tokens. */
.hero{
  background-image:var(--hero-scrim),url("/images/bg.png");
  background-repeat:no-repeat,no-repeat;
  background-size:cover,cover;
  background-position:center,center right;
}
/* Narrow viewports crop `cover` to a tall slice. Anchoring right would put the
   plate's busiest area (the rack stack) directly behind the h1, so pull the
   crop toward the emptier left-of-centre region instead. */
@media(max-width:1023.98px){
  .hero{background-position:center,30% center}
}
/* The ported radial glow would double up on the plate's own light — keep it,
   but at reduced strength so the two don't stack into a bright smear. */
.hero::before{opacity:.45}

/* Single column now: cap the measure so the copy does not run the full 1200px
   container and so the plate's right-hand rack detail stays visible. */
.hero .wrap>div{max-width:46rem}
@media(min-width:1024px){
  /* undo the ported two-column grid — there is no second column any more */
  .hero .wrap{display:block}
  .hero{padding-block:var(--space-12) var(--space-11)}
  .hero h1{font-size:clamp(2.5rem,1.4rem + 2.2vw,3.125rem)}
  .hero .hero-cta{margin-top:var(--space-6)}
  .hero .capstrip{margin-top:var(--space-6)}
  /* capstrip spans the container even though the prose is capped */
  .hero .wrap>div{max-width:none}
  .hero .wrap>div>:not(.capstrip){max-width:46rem}
}

/* ==========================================================================
   BLOG — /blog and /blog/[slug]   (design-system.md §2.4, §3.5; issue 13)

   Everything below is scoped to the two blog routes. The card itself is NOT here:
   .post/.cover/.postmeta above are shared with the homepage teaser, because §3.5
   specifies one card in two placements.
   ========================================================================== */

/* The card cover is a fixed 16:9 box whether or not an image lands in it. next/image
   renders an <img> with its own width/height attributes, so it is forced to fill the
   box rather than impose its intrinsic 1200x630 — 16:9 is the spec'd frame and
   1200x630 (1.90:1) is the OG frame; the small difference is absorbed by object-fit
   rather than by the layout. This is the whole of "images never cause layout shift":
   the box exists before the bytes do. */
.cover img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover;display:block}

/* ---- /blog index --------------------------------------------------------- */
.blogindex-head{margin-bottom:var(--space-7)}
.blogindex-head h1{font-size:var(--fs-h2);max-width:20ch;text-wrap:balance;margin-top:var(--space-3)}
.lead{color:var(--text-secondary);font-size:var(--fs-lead);margin-top:var(--space-3)}
.blogindex-empty{color:var(--text-muted);padding-block:var(--space-8)}

/* Tag chips are display-only (§3: "tag chips (display only)"), so this is a list of
   labels and not a row of buttons — no hover, no cursor:pointer, nothing that promises
   a filter that does not exist. */
.taglist{display:flex;flex-wrap:wrap;gap:var(--space-2);list-style:none;padding:0;margin:var(--space-5) 0 0}
.taglist .chip{align-self:auto}

/* ---- /blog/[slug] article ------------------------------------------------ */
.article{padding-block:var(--space-7) var(--space-9)}

/* §2.4's sticky "back to blog". Sticky, not fixed: it scrolls in the document and so
   can never overlap the nav bar or trap itself over the footer. The offset clears the
   condensed header (60px) plus a little air, matching `section`'s scroll-margin-top. */
.article-back{
  position:sticky;top:76px;z-index:var(--z-raised);
  display:inline-flex;align-items:center;
  /* A sticky element with a transparent background smears the article text under it
     as it scrolls. The pill is what makes it readable in both themes. */
  background:color-mix(in srgb,var(--bg) 88%,transparent);
  backdrop-filter:blur(8px);
  padding:var(--space-2) var(--space-3);margin-left:calc(var(--space-3) * -1);
  border-radius:var(--radius-pill)
}
@media(min-width:1024px){.article-back{top:92px}}
/* .tlink bakes a trailing "→" into ::after, which is right for every other text link on
   the site ("All articles →", "How we work →") because they all go forward. This one
   goes back and supplies its own leading "←" in the markup, so it was rendering
   "← All articles →" — an arrow at each end, pointing opposite ways.
   The primitive is left alone: this is the only backwards tlink on the site. (The
   prev/next pair under an article is .artnav-dir, not .tlink, so it is unaffected.)
   Hover keeps the colour change from .tlink; it loses only the 3px arrow nudge, which
   had nothing left to move. */
.article-back::after{content:none}

/* THE MEASURE CAP GOES ON THE CHILDREN, NOT ON THIS ELEMENT, and that is the whole
   reason this rule looks roundabout. <header> carries both `wrap` and `article-head`,
   so it IS the container: `.wrap` gives it `margin-inline:auto`, and capping the element
   itself at --measure made that auto margin centre a 68ch box in the viewport. Every
   other block in the article — .article-back, .article-cover and .prose — is a 68ch
   block sitting at the LEFT edge of its own .wrap, so the header alone was pushed inward
   (~240px on a 1440px viewport) and the title did not line up with the cover beneath it.
   Capping the children instead leaves the header full-container and left-aligned, which
   puts all four at the same left edge at every width. */
.article-head{margin-top:var(--space-5)}
.article-head>*{max-width:var(--measure)}
/* Narrower than the measure on purpose — §1.3's `h1{max-width:20ch}` with text-wrap:
   balance. More specific than the rule above, so it survives it. */
.article-head h1{font-size:var(--fs-h1);max-width:20ch;text-wrap:balance;margin-top:var(--space-4)}
.article-head .taglist{margin-top:0}

/* §2.4 byline + §7.3 visible date. --text-secondary rather than --text-muted: the
   date is a credibility signal, and a credibility signal set in the lowest-contrast
   ink on the page is being hidden politely. */
.article-meta{
  margin-top:var(--space-5);color:var(--text-secondary);font-size:var(--fs-small);
  display:flex;flex-wrap:wrap;gap:0 var(--space-1);align-items:baseline
}
.article-author{color:var(--text-primary);font-weight:600}
/* §7.6's author photo. `align-self:center` because the row is baseline-aligned for the
   text and a 40px circle has no useful baseline. Fixed box in both axes so the byline
   does not reflow as the image decodes — the same no-CLS rule the covers follow. */
.article-avatar{
  width:40px;height:40px;flex:0 0 40px;border-radius:var(--radius-pill);
  -o-object-fit:cover;
     object-fit:cover;align-self:center;margin-right:var(--space-1);
  background:var(--surface)
}
/* Muted: it qualifies the name rather than competing with it. */
.article-role{color:var(--text-muted)}

/* draft: true. Only reachable by direct link — see app/blog/[slug]/page.tsx. */
.article-draft{
  margin-bottom:var(--space-5);padding:var(--space-3) var(--space-4);
  border:1px dashed var(--border-strong);border-radius:var(--radius-md);
  background:var(--elevated);color:var(--text-secondary);font-size:var(--fs-small);
  max-width:var(--measure)
}
.article-draft code{font-family:var(--font-mono);color:var(--text-primary)}

.article-cover{
  margin-top:var(--space-7);max-width:var(--measure);
  aspect-ratio:16/9;overflow:hidden;
  border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface)
}
.article-cover img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover;display:block}

/* ---- the rendered MDX ---------------------------------------------------- */
/* §2.4: "68ch measure". --measure is the token from issue 5, so the article column and
   every other measured column on the site cannot drift apart. */
.prose{max-width:var(--measure);margin-top:var(--space-7)}
.prose>*+*{margin-top:var(--space-5)}
.prose p{font-size:var(--fs-body);color:var(--text-secondary);line-height:1.7;max-width:none}
.prose strong{color:var(--text-primary);font-weight:600}

/* Body headings start at h2 — the frontmatter title is the h1. */
.prose h2{
  font-size:var(--fs-h3);color:var(--text-primary);margin-top:var(--space-8);
  max-width:26ch;text-wrap:balance;scroll-margin-top:92px
}
.prose h3{
  font-size:var(--fs-h4);color:var(--text-primary);margin-top:var(--space-6);
  max-width:34ch;text-wrap:balance;scroll-margin-top:92px
}
.prose h2+p,.prose h3+p{margin-top:var(--space-3)}

.prose ul,.prose ol{padding-left:1.25rem;color:var(--text-secondary);line-height:1.7}
.prose li+li{margin-top:var(--space-2)}
.prose li::marker{color:var(--text-muted)}

.prose blockquote{
  border-left:2px solid var(--accent);padding-left:var(--space-4);
  color:var(--text-primary);font-style:italic
}
.prose blockquote p{color:inherit}

.prose hr{border:0;border-top:1px solid var(--border);margin-block:var(--space-8)}

.prose a{color:var(--primary);text-decoration:underline;text-underline-offset:3px}
@media(hover:hover) and (pointer:fine){.prose a:hover{color:var(--primary-hover)}}

/* Inline code. `overflow-wrap:anywhere` so a long identifier inside a sentence wraps
   instead of pushing the 68ch column wider than 68ch. */
.prose :not(pre)>code{
  font-family:var(--font-mono);font-size:.9em;overflow-wrap:anywhere;
  background:var(--elevated);border:1px solid var(--border);border-radius:var(--radius-sm);
  padding:.1em .35em;color:var(--text-primary)
}

/* CODE BLOCKS SCROLL INSIDE THEIR OWN CONTAINER — the issue's criterion.
   `min-width:0` on the wrapper is the part that is easy to miss and the part that
   actually does the work: a grid/flex item's default `min-width:auto` lets its
   min-content width (the longest unbreakable line) push the whole column wider, so
   `overflow-x:auto` on the <pre> alone silently does nothing and the PAGE scrolls
   sideways instead. With min-width:0 the overflow is forced to resolve inside the box.
   `max-width:100%` covers the same failure outside a formatting context. */
.codeblock,.tablewrap{min-width:0;max-width:100%;overflow-x:auto}
.codeblock{
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);
  box-shadow:var(--edge-hi)
}
.codeblock pre{
  margin:0;padding:var(--space-4);overflow-x:auto;
  font-family:var(--font-mono);font-size:var(--fs-small);line-height:1.6;
  color:var(--text-primary);-moz-tab-size:2;-o-tab-size:2;tab-size:2
}
/* No wrapping inside a code block: a wrapped shell command is a shell command someone
   will copy wrongly. It scrolls instead — which is why the <pre> is focusable. */
.codeblock pre code{white-space:pre;font:inherit;background:none;border:0;padding:0}

.tablewrap{border:1px solid var(--border);border-radius:var(--radius-md)}
.prose table{border-collapse:collapse;width:100%;font-size:var(--fs-small);min-width:32rem}
.prose th,.prose td{
  padding:var(--space-3) var(--space-4);text-align:left;vertical-align:top;
  border-bottom:1px solid var(--border);color:var(--text-secondary)
}
.prose th{
  color:var(--text-primary);font-weight:600;background:var(--elevated);
  white-space:nowrap
}
.prose tbody tr:last-child td{border-bottom:0}

.mdx-img{max-width:100%;height:auto;border-radius:var(--radius-md);border:1px solid var(--border)}

/* ---- prev / next --------------------------------------------------------- */
.artnav{
  display:grid;gap:var(--space-4);margin-top:var(--space-9);
  padding-top:var(--space-6);border-top:1px solid var(--border);max-width:var(--measure)
}
@media(min-width:768px){.artnav{grid-template-columns:1fr 1fr}}
.artnav-link{
  display:flex;flex-direction:column;gap:var(--space-2);text-decoration:none;
  padding:var(--space-4);border:1px solid var(--border);border-radius:var(--radius-md);
  background:var(--surface);transition:border-color var(--dur-2) var(--ease-out)
}
@media(hover:hover) and (pointer:fine){
  .artnav-link:hover{border-color:var(--border-strong)}
  .artnav-link:hover .artnav-title{color:var(--primary)}
}
.artnav-dir{font-family:var(--font-mono);font-size:var(--fs-eyebrow);text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}
.artnav-title{color:var(--text-primary);font-weight:600;line-height:1.35}
/* Right-aligned so "Next →" reads as forward motion, and only from the breakpoint
   where the two cells actually sit side by side. */
@media(min-width:768px){.artnav-next{text-align:right}}

/* ==========================================================================
   LEGAL ROUTES — /privacy (issue 15)

   The page is a frame with no text in it yet, so the styling has one job beyond
   matching the rest of the site: make "unfinished" unmissable. The issue's own note
   is that a generic-but-real-looking privacy policy is worse than an obviously-marked
   placeholder, and that judgement has to survive someone skim-reading the page.
   ========================================================================== */

.legal-pending,.legal-note{
  max-width:var(--measure);margin-top:var(--space-6);
  padding:var(--space-5);border-radius:var(--radius-lg);
  background:var(--elevated)
}
/* Dashed, not solid: the same visual grammar as .article-draft. A dashed border
   reads as provisional in a way a solid one does not. */
.legal-pending{border:1px dashed var(--border-strong)}
/* Solid, for the opposite reason. /cookies is finished and its callout is the answer to
   the question the page was opened to ask — borrowing the "this is unfinished" dashed
   border would say the wrong thing about text that is settled. */
.legal-note{border:1px solid var(--border-strong)}
.legal-pending h2,.legal-note h2{font-size:var(--fs-h4);color:var(--text-primary);margin:0}
.legal-pending p,.legal-note p{color:var(--text-secondary);font-size:var(--fs-small);margin-top:var(--space-3)}

.legal-prose{margin-top:var(--space-8)}
.legal-prose>section+section{margin-top:var(--space-8)}
/* .prose already spaces its children; a nested <section> is one child, so its own
   internals need the same rhythm restated. */
.legal-prose h2{margin-top:0}
.legal-prose h2+p{margin-top:var(--space-3)}

/* What the section has to answer. --text-muted and italic so it reads as an editorial
   note about the page rather than as part of the policy — the distinction this whole
   page exists to preserve. */
.legal-prompt{
  font-size:var(--fs-small);color:var(--text-muted);font-style:italic;
  border-left:2px solid var(--border);padding-left:var(--space-4);margin-top:var(--space-3)
}

/*!*********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[3].use[1]!./node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[14].oneOf[3].use[2]!./node_modules/next/font/local/target.css?{"path":"app/fonts.ts","import":"","arguments":[{"src":[{"path":"./fonts/space-grotesk-500.woff2","weight":"500","style":"normal"},{"path":"./fonts/space-grotesk-700.woff2","weight":"700","style":"normal"}],"variable":"--font-space-grotesk","display":"swap","preload":true,"fallback":["SF Pro Display","system-ui","sans-serif"]}],"variableName":"fontDisplay"} ***!
  \*********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
@font-face {
font-family: 'fontDisplay';
src: url(/_next/static/media/31250b261513219c-s.p.woff2) format('woff2');
font-display: swap;
font-weight: 500;
font-style: normal;
}

@font-face {
font-family: 'fontDisplay';
src: url(/_next/static/media/46d978a299613541-s.p.woff2) format('woff2');
font-display: swap;
font-weight: 700;
font-style: normal;
}@font-face {font-family: 'fontDisplay Fallback';src: local("Arial");ascent-override: 88.74%;descent-override: 26.33%;line-gap-override: 0.00%;size-adjust: 110.89%
}.__className_bcab52 {font-family: 'fontDisplay', 'fontDisplay Fallback', SF Pro Display, system-ui, sans-serif
}.__variable_bcab52 {--font-space-grotesk: 'fontDisplay', 'fontDisplay Fallback', SF Pro Display, system-ui, sans-serif
}

/*!********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[3].use[1]!./node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[14].oneOf[3].use[2]!./node_modules/next/font/local/target.css?{"path":"app/fonts.ts","import":"","arguments":[{"src":[{"path":"./fonts/inter-400.woff2","weight":"400","style":"normal"},{"path":"./fonts/inter-500.woff2","weight":"500","style":"normal"},{"path":"./fonts/inter-600.woff2","weight":"600","style":"normal"}],"variable":"--font-inter","display":"swap","preload":true,"fallback":["system-ui","-apple-system","Segoe UI","Roboto","sans-serif"]}],"variableName":"fontBody"} ***!
  \********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
@font-face {
font-family: 'fontBody';
src: url(/_next/static/media/ef6d3f52c547a6e3-s.p.woff2) format('woff2');
font-display: swap;
font-weight: 400;
font-style: normal;
}

@font-face {
font-family: 'fontBody';
src: url(/_next/static/media/b7b43acee81d05a8-s.p.woff2) format('woff2');
font-display: swap;
font-weight: 500;
font-style: normal;
}

@font-face {
font-family: 'fontBody';
src: url(/_next/static/media/8fb1a96451cec840-s.p.woff2) format('woff2');
font-display: swap;
font-weight: 600;
font-style: normal;
}@font-face {font-family: 'fontBody Fallback';src: local("Arial");ascent-override: 89.79%;descent-override: 22.36%;line-gap-override: 0.00%;size-adjust: 107.89%
}.__className_0cc60c {font-family: 'fontBody', 'fontBody Fallback', system-ui, -apple-system, Segoe UI, Roboto, sans-serif
}.__variable_0cc60c {--font-inter: 'fontBody', 'fontBody Fallback', system-ui, -apple-system, Segoe UI, Roboto, sans-serif
}

/*!*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
  !*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[14].oneOf[3].use[1]!./node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[14].oneOf[3].use[2]!./node_modules/next/font/local/target.css?{"path":"app/fonts.ts","import":"","arguments":[{"src":[{"path":"./fonts/jetbrains-mono-400.woff2","weight":"400","style":"normal"}],"variable":"--font-jetbrains-mono","display":"swap","preload":false,"adjustFontFallback":false,"fallback":["ui-monospace","SFMono-Regular","Menlo","monospace"]}],"variableName":"fontMono"} ***!
  \*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
@font-face {
font-family: 'fontMono';
src: url(/_next/static/media/db9d659a1acad1e2.woff2) format('woff2');
font-display: swap;
font-weight: 400;
font-style: normal;
}.__className_845eb2 {font-family: 'fontMono', ui-monospace, SFMono-Regular, Menlo, monospace;font-weight: 400;font-style: normal
}.__variable_845eb2 {--font-jetbrains-mono: 'fontMono', ui-monospace, SFMono-Regular, Menlo, monospace
}

