/* Single source of truth for color. Light on :root, dark under both the
   system media query and the explicit [data-theme] attribute, so an explicit
   toggle choice overrides the OS in either direction. */

:root {
  /* surfaces and text -- these flip with the theme */
  --paper: #F1EDE4;
  --ink: #16150F;
  --ink-soft: #3D3A33;
  --rule: #D6CDB9;
  --rule-strong: #B9AF99;
  --gutter-text: #A9A08D;

  /* accent family: large saturated panel fills */
  --mustard: #E8A33D;
  --tomato: #E64B3C;
  --sage: #5B7A4A;
  --pink: #F4C9D4;
  --deep: #D96A4A;
  --blue: #3A6BA8;
  --sand: #D8C9A3;

  /* fill colors for layout components */
  --fill-warm: #E0D3C1;
  --fill-cool: #D6D4C6;

  /* Mustard tuned for MARKS rather than panel fill. --mustard itself is picked
     to carry large saturated areas, and as a line or as small type on --paper it
     measures about 1.85:1 -- below the 3:1 that WCAG 1.4.11 asks of a non-text
     indicator, and far below the 4.5:1 small text needs. This flips so the
     accent stays legible either way, unlike --on-accent which deliberately does
     not.
     Every focus ring on the site uses this. Anything added here must keep >=3:1
     against --paper in BOTH themes, or keyboard focus stops being visible. */
  --accent-ink: #8A5A0C;
  /* Idle state for the Services headings: clearly recessed next to the active
     one, but still large-text legible on its own (>=3:1), which the original
     mock's flat #B8AF9B was not. */
  --idle-ink: #867E6B;

  /* NEVER flips. Foreground for content on saturated accent panels.
     Using --ink here would render near-white on mustard in dark mode. */
  --on-accent: #16150F;

  /* NEVER flips. Kept as the light-drafting-board fill for any future panel
     whose contents are fixed dark-on-light literals; the bento dashboard no
     longer paints a board of its own, so nothing consumes this today. Anything
     drawn on it must use --on-accent, never --ink / --ink-soft. */
  --board: #F0E7D2;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #14130F;
    --ink: #EFEADF;
    --ink-soft: #B5AEA0;
    --rule: #2E2B24;
    --rule-strong: #433F35;
    --gutter-text: #6B655A;

    --mustard: #E8A33D;
    --tomato: #D4523F;
    --sage: #6E8F58;
    --pink: #D9A9B4;
    --deep: #C4614A;
    --blue: #4E7CB8;
    --sand: #B9A87F;

    --fill-warm: #2A2620;
    --fill-cool: #26251E;

    --accent-ink: #F0B152;
    --idle-ink: #6F6857;
  }
}

:root[data-theme="dark"] {
  --paper: #14130F;
  --ink: #EFEADF;
  --ink-soft: #B5AEA0;
  --rule: #2E2B24;
  --rule-strong: #433F35;
  --gutter-text: #6B655A;

  --mustard: #E8A33D;
  --tomato: #D4523F;
  --sage: #6E8F58;
  --pink: #D9A9B4;
  --deep: #C4614A;
  --blue: #4E7CB8;
  --sand: #B9A87F;

  --fill-warm: #2A2620;
  --fill-cool: #26251E;

  --accent-ink: #F0B152;
  --idle-ink: #6F6857;
}

/* ---- theme toggle ---- */
.chrome-toggle { display: inline-grid; place-items: center; width: 38px; height: 38px; padding: 0; border: 1px solid var(--rule-strong); border-radius: 50%; background: transparent; color: var(--ink); font-size: 17px; line-height: 1; cursor: pointer; transition: background 180ms ease, border-color 180ms ease; }
.chrome-toggle:hover { background: var(--rule); border-color: var(--ink); }
.chrome-toggle:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }

/* ---- brand mark: collapsed reads "ai", expanded reads the full name ---- */
.chrome-brand { display: inline-flex; align-items: center; gap: 12px; text-decoration: none; color: var(--ink); }
.chrome-mark { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--mustard); color: var(--on-accent); font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 500; }
.chrome-word { display: inline-flex; align-items: baseline; font-size: 15px; font-weight: 500; letter-spacing: .06em; white-space: pre; }
.chrome-word .cw-keep { color: var(--mustard); font-weight: 700; }
.chrome-word .cw-fill { display: inline-block; overflow: hidden; max-width: 0; opacity: 0; transition: max-width 420ms cubic-bezier(.65,0,.35,1), opacity 260ms ease; }
/* 7ch is tuned to the longest connective segment produced by BRAND_KEEP over
   BRAND_NAME in chrome.jsx ('mza Tah', 7 chars). Changing the name or the keep
   indices requires re-measuring this cap or the expanded mark silently clips. */
.chrome-brand:hover .cw-fill, .chrome-brand:focus-visible .cw-fill { max-width: 7ch; opacity: 1; }
.chrome-brand:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* ---- drafting frame ---- */
:root { --frame-x: 32px; --bar-h: 64px; }
/* Content inset. The hero and the nav sit at --wrap-x from the viewport edge,
   just inside the frame rule at --frame-x, and every section below the hero
   now uses the same value -- so the whole page shares one left/right edge
   instead of the old centred 92%/1180px column, which drifted away from the
   frame as the viewport grew. Narrow screens tighten the extra gap so the
   content column does not starve. */
:root { --wrap-x: calc(var(--frame-x) + 64px); }
.frame-wrap { width: auto; max-width: none; margin-inline: var(--wrap-x); }
/* A prose measure column: centred and capped for readability, but never wider
   than the page margins allow. The min() is what keeps it honest -- a plain
   centred `max-width: 760px` sat OUTSIDE --wrap-x once the viewport got narrow
   enough, so the one block readers actually read was the one block ignoring the
   page's own edges. */
.measure-wrap {
  width: auto;
  max-width: min(760px, calc(100% - 2 * var(--wrap-x)));
  margin-inline: auto;
}

/* Section eyebrow: a short rule then a mono label. Was inline styles inside
   home.jsx's SectionLabel; promoted to a class so other sections can match it
   without copying the numbers. */
.section-label {
  display: flex; align-items: center; gap: 12px;
  font-family: 'JetBrains Mono', monospace; font-size: 11px;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink); opacity: .6;
}
.section-label-tick { width: 28px; height: 1.5px; background: var(--ink); flex: none; }

.chrome-shell { position: relative; min-height: 100vh; display: flex; flex-direction: column; }

/* One viewport tall, not `inset: 0`. The shell now contains the latent zoom's
   220vh pin, so an inset-0 frame stretched its bottom rule and corner ticks
   about two and a half viewports below the fold -- leaving the first screen
   with a top rule, two verticals and no bottom. The frame is a first-screen
   device, so it is sized to the first screen. */
/* inset: 0, so the frame spans the WHOLE shell rather than the first viewport.
   The two vertical rules are meant to flow the entire length of the pinned zoom
   and the client tray beneath it, and the bottom rule and its corner ticks close
   the frame once the tray ends -- not at the fold. Clamping this to 100vh put a
   horizontal rule straight across the zoom, which is the thing it must not do. */
.chrome-frame { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.chrome-frame span { position: absolute; background: var(--rule); }
.chrome-frame .cf-v { top: 0; bottom: 0; width: 1px; }
.chrome-frame .cf-v.cf-l { left: var(--frame-x); }
.chrome-frame .cf-v.cf-r { right: var(--frame-x); }
.chrome-frame .cf-h { left: 0; right: 0; height: 1px; }
.chrome-frame .cf-h.cf-t { top: var(--bar-h); }
.chrome-frame .cf-h.cf-b { bottom: var(--bar-h); }

.cf-tick { position: absolute; pointer-events: none; }
.cf-tick::before, .cf-tick::after { content: ""; position: absolute; background: var(--rule-strong); }
.cf-tick::before { left: -6px; top: 0; width: 13px; height: 1px; }
.cf-tick::after { left: 0; top: -6px; width: 1px; height: 13px; }
.cf-tick.cf-tl { left: var(--frame-x); top: var(--bar-h); }
.cf-tick.cf-tr { right: var(--frame-x); top: var(--bar-h); }
.cf-tick.cf-bl { left: var(--frame-x); bottom: var(--bar-h); }
.cf-tick.cf-br { right: var(--frame-x); bottom: var(--bar-h); }



@media (max-width: 1099px) {
  :root { --frame-x: 24px; --bar-h: 56px; }
}
@media (max-width: 760px) {
  :root { --wrap-x: calc(var(--frame-x) + 22px); }
}
@media (max-width: 699px) {
  :root { --frame-x: 16px; --bar-h: 52px; }
}
@media (max-width: 480px) {
  :root { --wrap-x: calc(var(--frame-x) + 10px); }
}

/* ---- flat frame nav ---- */
.chrome-nav { position: relative; z-index: 3; height: var(--bar-h); display: flex; align-items: center; justify-content: flex-start; gap: 34px; padding: 0 var(--wrap-x); }
/* Brand absorbs the free space, so its hover expansion eats its own margin
   instead of shoving the links. Links then sit against the right group. */
.chrome-nav > .chrome-brand { margin-right: auto; }
.cn-links { display: flex; align-items: center; gap: 26px; font-size: 14px; }
.cn-links a { color: var(--ink-soft); text-decoration: none; padding: 4px 2px; transition: color 180ms ease; }
.cn-links a:hover { color: var(--ink); }
.cn-links a[aria-current="page"] { color: var(--ink); box-shadow: inset 0 -2px 0 var(--mustard); }
.cn-right { display: flex; align-items: center; gap: 12px; }
.cn-contact { background: var(--mustard); color: var(--on-accent); padding: 9px 18px; border-radius: 2px; font-weight: 500; font-size: 14px; text-decoration: none; white-space: nowrap; transition: filter 180ms ease; }
.cn-contact:hover { filter: brightness(.93); }
.cn-contact:focus-visible, .cn-links a:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* ---- hamburger ---- */
.cn-burger { display: none; place-items: center; width: 38px; height: 38px; padding: 0; border: 1px solid var(--rule-strong); border-radius: 2px; background: transparent; color: var(--ink); font-size: 18px; line-height: 1; cursor: pointer; }
.cn-burger:hover { background: var(--rule); }
.cn-burger:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }

.cn-sheet { position: absolute; top: var(--bar-h); left: var(--wrap-x); right: var(--wrap-x); z-index: 20; display: none; flex-direction: column; background: var(--paper); border: 1px solid var(--rule); border-top: none; }
.cn-sheet a { padding: 15px 20px; color: var(--ink-soft); text-decoration: none; font-size: 15px; border-bottom: 1px solid var(--rule); }
.cn-sheet a:last-child { border-bottom: none; }
.cn-sheet a:hover { color: var(--ink); background: var(--rule); }
.cn-sheet a[aria-current="page"] { color: var(--ink); box-shadow: inset 3px 0 0 var(--mustard); }
.cn-sheet a:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: -2px; }

@media (max-width: 1099px) {
  .cn-links { display: none; }
  .cn-burger { display: grid; }
  .cn-sheet.is-open { display: flex; }
}

/* ---- U-Net diagram ---- */
.unet {
  /* The box is one --bar-h shorter than its container, .latent-zoom-frame. That
     reserve is what keeps the latent bar off the bottom of the hero: viewBoxFor
     also reserves LABEL_PAD below the latent for its hover label, roughly an
     eighth of the box height. It does NOT line the box up with the chrome
     frame's bottom rule -- the frame is one viewport measured from the shell's
     top while this box starts below the nav, so the two sit a --bar-h apart.
     This replaces a `transform: translateY(7%)` that pushed the box PAST its
     container; do not re-add a translate, it moves the latent without moving
     anything the latent has to clear.

     The height is stated explicitly rather than left to a `bottom` inset: an
     <svg> is a REPLACED element, so with height:auto the browser ignores
     `bottom` and derives the height from the viewBox aspect ratio instead --
     which silently reinstated the old overflowing box. */
  /* Inset horizontally by one --frame-x plus the rule's own 1px, so the box ends
     exactly on the INNER edge of the chrome frame's two vertical rules and the
     latent strip can never cross them at any zoom depth. The parent's
     padding-inline cannot do this job: an absolutely positioned child resolves
     its insets and percentage width against the containing block's PADDING box,
     so padding on .latent-zoom-frame left this box spanning the full width.
     Width is stated explicitly for the same reason the height is -- an <svg> is
     a replaced element, and `width: auto` makes it derive its own size from the
     viewBox aspect and ignore the left/right insets entirely. */
  position: absolute; top: 0;
  left: calc(var(--frame-x) + 1px); right: calc(var(--frame-x) + 1px);
  width: calc(100% - 2 * (var(--frame-x) + 1px)); height: calc(100% - var(--bar-h));
  /* z-index:-1 needs an ancestor stacking context or this paints under <body>.
     .latent-zoom-frame supplies it: `position: sticky` forms one by itself, and
     the .is-static variant below states an explicit z-index for the same
     reason. It is no longer .hero-stage -- that is now a plain grid child. */
  z-index: -1;
  pointer-events: none;
}
/* Skip connections: purely decorative, so thin, dashed and low-contrast, and
   painted before the tiles so they read as passing behind them. */
.unet-skips line { stroke: var(--rule-strong); stroke-width: 1.5; stroke-dasharray: 5 7; opacity: .5; }

.unet-node { pointer-events: auto; cursor: default; }

/* The two latent links are dotted ELBOWS. A near-zero dash with a round cap
   renders as a true dot rather than a short dash, which distinguishes them from
   the skip connections' longer dashes above. fill:none is required because
   these are polylines -- an unfilled shape would otherwise paint a solid
   triangle between the elbow's two legs. */
.unet-skips .unet-latent-link {
  fill: none;
  stroke: var(--rule-strong);
  stroke-width: 1.6;
  stroke-dasharray: 0.1 6;
  stroke-linecap: round;
  opacity: .7;
}
.unet-node image { opacity: .5; transition: opacity 220ms ease; }
.unet-node:hover image, .unet-node:focus-visible image { opacity: .95; }
.unet-lbl { font-family: 'JetBrains Mono', monospace; font-size: 11px; fill: var(--gutter-text); opacity: 0; transition: opacity 220ms ease; }
.unet-node:hover .unet-lbl, .unet-node:focus-visible .unet-lbl { opacity: 1; }
.unet-ring { opacity: 0; }
.unet-node:focus-visible { outline: none; }
.unet-node:focus-visible .unet-ring { stroke: var(--accent-ink); stroke-width: 1.5; opacity: 1; }

/* The latent strip runs at FULL strength, unlike the six node tiles. Its accent
   ramp already carries its own alpha per cell (see LATENT_ACCENTS in
   unet-geometry.js: ordinary cells are painted at .35, the four stat cells
   solid), so stacking the tiles' .5 on top of that would land the texture cells
   at .175 and wash the whole thing out. Keeping the alpha in the colour instead
   of in CSS is also what lets the raster strip and the vector cells match. */
.unet-node image.unet-latent-img,
.unet-node:hover image.unet-latent-img,
.unet-node:focus-visible image.unet-latent-img { opacity: 1; }

/* Vector latent cells. These replace the baked raster strip once the zoom is
   deep enough for text; same reasoning as above, so no CSS opacity here. */
.unet-cell { opacity: 1; }
.unet-cell-num { font-family: 'JetBrains Mono', monospace; font-size: 2.6px; fill: var(--gutter-text); transition: opacity 200ms ease; }
.unet-stats { transition: opacity 260ms ease; }
/* These sizes are in SVG USER UNITS, not pixels: a latent cell is 8 units wide
   and the strip is 20 units tall, so `6px` here is six of those units and
   `.95px` is just under one. They are not typos, and they are not free to
   round -- at font-size 9 two digits measured ~9 units, so the four figures
   merged into one number and the labels ran together.
   Space Grotesk at 700, matching .hero-h1 -- the site sets no other display
   face, and the serif that used to be here was the only Cormorant Garamond on
   the whole site.
   --on-accent, never --ink: these figures sit ON a saturated accent fill, and
   --on-accent is the non-flipping foreground that exists for exactly that. The
   weakest pairing is blue #3A6BA8 under #16150F at about 3.3:1, which clears the
   3:1 large-text threshold -- and at this zoom depth a 6-unit glyph renders
   around 270px tall, so large text is not a technicality here. */
.unet-stat-fig { font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 6px; font-weight: 700; fill: var(--on-accent); letter-spacing: -.03em; }
.unet-stat-lbl { font-family: 'JetBrains Mono', monospace; font-size: .95px; fill: var(--on-accent); opacity: .72; letter-spacing: .12em; text-transform: uppercase; }

/* Scroll-driven latent zoom. The outer element's height is set inline (220vh)
   so the pin length lives with the component that computes progress from it;
   only the sticky behaviour is here. */
.latent-zoom { position: relative; }
/* padding-inline: --frame-x keeps the SVG's box inside the frame's two vertical
   rules, so the strip cannot cross them at any zoom depth. Without it the
   deepest frame ran the cells out to the viewport edges and straight through
   both lines. */
.latent-zoom-frame { position: sticky; top: 0; height: 100vh; padding-inline: var(--frame-x); display: grid; place-items: center; overflow: hidden; }
.latent-zoom-copy { position: relative; z-index: 2; grid-area: 1 / 1; transition: opacity 120ms linear; }
/* Cross-fades with .latent-zoom-copy in the same grid cell -- both are grid
   children of a `place-items: center` parent, so they occupy the same box and
   their opacities sum to 1 (see headingOpacity in stats-zoom.js). Stacked rather
   than sequential, so the slot never sits empty mid-scroll. */
.latent-zoom-head { position: relative; z-index: 2; grid-area: 1 / 1; max-width: 16ch; margin: 0; text-align: center; font-size: clamp(42px, 6.6vw, 84px); font-weight: 700; line-height: .98; letter-spacing: -.035em; color: var(--ink); transition: opacity 120ms linear; }
.latent-zoom.is-static { height: auto; }
/* `relative` + an explicit z-index, not `static`: the static branch still draws
   the diagram (reduced motion at desktop width had it deleted outright), and
   .unet is `position: absolute; z-index: -1`, so it needs both a containing
   block and a stacking context here or it paints under <body> and vanishes. */
.latent-zoom.is-static .latent-zoom-frame { position: relative; z-index: 0; height: auto; overflow: visible; }
.stats-band { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 28px; padding: 8px var(--wrap-x) 64px; }
.stats-band-item { text-align: center; }
/* Space Grotesk, matching .hero-h1 and the SVG figures. This band sits on the
   page ground rather than on an accent fill, so it keeps --ink -- --on-accent
   belongs only over an accent. */
.stats-band-fig { font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 54px; font-weight: 700; line-height: 1; letter-spacing: -.03em; color: var(--ink); }
.stats-band-lbl { margin-top: 8px; font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-soft); }

/* Visually gone, still in the accessibility tree. The figures inside the SVG
   are unreachable to assistive tech -- they sit under two nested role="img"
   ancestors, and ARIA specifies Children Presentational: True for that role --
   so the zoomed hero carries a hidden HTML copy of the same band. No `display:
   none` and no zero height: both would remove it from the tree as well. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

.tray-section { padding: 8px 0 78px; }
.tray-h2 { margin: 14px 0 0; }
.tray-dek { max-width: 560px; margin: 10px 0 0; color: var(--ink-soft); }

/* The viewport crops the row; the row itself is three copies of the set, so a
   translate of exactly one third lands on an identical frame.
   The crop is one-axis on purpose. With `overflow: hidden` the detail card --
   which is positioned ABOVE its mark, so entirely outside this box -- was
   clipped away and could never be seen at any width. `overflow-x: clip` keeps
   the row cropped sideways while letting the card escape upward; `clip` rather
   than `hidden` so overflow-y is free to stay `visible`. */
.tray-viewport { position: relative; margin-top: 30px; overflow-x: clip; overflow-y: visible; }
.tray-viewport::before, .tray-viewport::after { content: ""; position: absolute; top: 0; bottom: 0; width: 72px; z-index: 2; pointer-events: none; }
.tray-viewport::before { left: 0; background: linear-gradient(to right, var(--paper), transparent); }
.tray-viewport::after { right: 0; background: linear-gradient(to left, var(--paper), transparent); }
/* No `gap` here, deliberately. 21 marks give 20 gaps, so a gapped row measures
   3S + 20g while a seamless one-third translate needs 3(S + 7g) -- the 19px
   shortfall showed up as a snap on every loop. Moving the spacing onto the item
   puts it inside the repeating unit, where the loop math needs it. */
.tray-row { display: flex; align-items: center; width: max-content; animation: tray-scroll 26s linear infinite; }

/* A real pause, not a slower duration. A decelerating row still moves the
   target while it is being read, which is the exact thing that makes logo
   marquees feel broken. */
.tray-viewport.is-paused .tray-row { animation-play-state: paused; }

@keyframes tray-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-33.3333%); }
}

/* margin-inline-end, not the row's gap -- see .tray-row above. */
.tray-mark { position: relative; flex: none; margin-inline-end: 56px; background: none; border: 0; padding: 6px 2px; cursor: pointer; font-family: inherit; font-size: 17px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--ink); opacity: .55; transition: opacity 200ms ease; }
.tray-viewport.is-paused .tray-mark { opacity: .22; }
.tray-viewport.is-paused .tray-mark.is-active { opacity: 1; }
.tray-mark:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* Inverted panel, matching .hero-hl-pop: background --ink, normal text --paper,
   so the two flip together and stay legible in either theme. That is why the
   muted lines below derive from --paper rather than reaching for --ink-soft,
   which is tuned for text ON --paper and would sit at ~1:1 against --ink here.
   (--ink and --paper do flip; only --on-accent and --board are non-flipping.)
   Left-anchored, not centre-anchored: centring put the first mark's card at
   x -14 at 1440px and x -49 at 642px, i.e. off the left edge of the page, and
   the card is 300px against a mark barely a third that wide. Anchoring to the
   mark's left edge removes the overflow risk outright and keeps the tail near
   the mark; max-width holds it inside the viewport on the narrowest screens. */
.tray-pop { position: absolute; left: 0; bottom: calc(100% + 12px); transform: translateY(-6px); width: 300px; max-width: min(300px, calc(100vw - 24px)); padding: 13px 15px; border-radius: 14px; background: var(--ink); color: var(--paper); box-shadow: 0 22px 55px -20px rgba(26,26,26,.55); text-align: left; text-transform: none; letter-spacing: 0; font-weight: 400; opacity: 0; pointer-events: none; transition: opacity .22s ease, transform .22s ease; z-index: 30; }
.tray-mark.is-active .tray-pop { opacity: 1; transform: translateY(0); }
.tray-pop::after { content: ""; position: absolute; left: 24px; bottom: -6px; width: 12px; height: 12px; background: var(--ink); transform: rotate(45deg); border-radius: 2px; }
.tray-pop-label { display: block; margin-bottom: 9px; font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: color-mix(in srgb, var(--paper) 60%, transparent); }
.tray-pop-body { display: block; font-size: 13px; line-height: 1.5; }
.tray-pop-meta { display: block; margin-top: 9px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: color-mix(in srgb, var(--paper) 60%, transparent); }

/* ---- hero ---- */
/* No `flex: 1`: the hero is a grid child of .latent-zoom-frame now, not a flex
   child of .chrome-shell, so a flex-grow factor here does nothing. */
.hero-stage { position: relative; z-index: 2; display: grid; place-items: center; padding: 64px var(--wrap-x); }
.hero-copy { position: relative; z-index: 2; max-width: 560px; text-align: center; }
.hero-h1 { margin: 0; font-size: clamp(46px, 7.4vw, 92px); font-weight: 700; line-height: .98; letter-spacing: -.038em; }
.hero-accent { position: relative; display: inline-block; isolation: isolate; }
.hero-accent::after { content: ""; position: absolute; z-index: -1; left: 34%; right: 2%; bottom: .09em; height: .16em; background: var(--mustard); opacity: .82; }
.hero-lede { margin: 28px auto 0; max-width: 430px; font-size: clamp(14px, 1.4vw, 16px); line-height: 1.65; color: var(--ink-soft); }
.hero-ctas { display: flex; gap: 10px; justify-content: center; margin-top: 32px; flex-wrap: wrap; }
.hero-btn { font-family: 'JetBrains Mono', monospace; font-size: 13px; padding: 12px 22px; border-radius: 2px; text-decoration: none; border: 1px solid transparent; transition: background 180ms ease, border-color 180ms ease; }
.hero-btn-solid { background: var(--ink); color: var(--paper); }
.hero-btn-solid:hover { filter: brightness(1.2); }
.hero-btn-ghost { color: var(--ink); border-color: var(--rule-strong); }
.hero-btn-ghost:hover { border-color: var(--ink); }
.hero-btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* ---- page transition overlay (Barba) ----------------------------------- */
/* Paints the paper colour over the outgoing page through a diffuse expanding
   circle -- the same feTurbulence displacement wipe the project cards use --
   then reopens a hole in it over the incoming page. */
.pt-overlay {
  position: fixed; inset: 0; z-index: 9999;
  /* --ink, not --paper: the overlay was previously the same colour as the page
     behind it, so the wipe was invisible and only the label appeared to happen.
     Inverting it means the front is actually visible sweeping across, and it
     stays high-contrast in both themes because --ink and --paper flip together. */
  background: var(--ink);
  pointer-events: none;
  opacity: 0;
  -webkit-mask: url(#pt-cover); mask: url(#pt-cover);
}
.pt-overlay.is-active { opacity: 1; pointer-events: auto; }
.pt-overlay.is-revealing { -webkit-mask: url(#pt-reveal); mask: url(#pt-reveal); }
/* The destination's name, held between the two wipes. It sits inside the masked
   overlay deliberately: the closing wipe reveals it, and the opening wipe eats
   it again from the same origin, so it belongs to the transition rather than
   floating above it. */
.pt-label {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  margin: 0; padding: 0 6vw;
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: clamp(38px, 7vw, 92px);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1;
  text-align: center;
  color: var(--paper);
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 220ms ease, transform 220ms cubic-bezier(.2,.8,.2,1);
}
.pt-overlay.is-named .pt-label { opacity: 1; transform: translateY(0); }
.pt-defs { position: absolute; width: 0; height: 0; }
/* The wipe is decoration; with reduced motion the router skips it entirely and
   this never becomes visible. */
@media (prefers-reduced-motion: reduce) { .pt-overlay { display: none; } }
