/* Northbound — the shell.
 *
 * Tokens, type, layout, and the four act containers. FROZEN during the
 * parallel build: each department styles its own act in css/act-*.css and
 * nothing else. Everything here has to hold with no JavaScript and no WebGL,
 * because that is the state this page must still be good in.
 *
 * The CLS gate is exactly zero, so every act reserves its height before any
 * script runs and the canvas is fixed and outside every act subtree. No act
 * may insert a node that affects layout after first paint.
 */

/* --- faces ---------------------------------------------------------------
   swap, not block: the aurora behind unstyled text for 100ms is a far better
   first frame than no text at all, and LCP is gated at 1200ms on slow 4G. */
@font-face {
  font-family: 'Syne';
  src: url('../fonts/syne.woff2') format('woff2');
  font-weight: 400 800; font-display: swap; font-style: normal;
}
@font-face {
  font-family: 'Instrument Sans';
  src: url('../fonts/instrument-sans.woff2') format('woff2');
  font-weight: 400 700; font-display: swap; font-style: normal;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/jetbrains-mono.woff2') format('woff2');
  font-weight: 400 600; font-display: swap; font-style: normal;
}

:root {
  --ink:        #05090C;
  --ink-2:      #0A1218;
  --paper:      #E8EFF2;
  --paper-dim:  #9DB0B8;
  --paper-faint:#63767F;
  --line:       #16232B;
  --line-2:     #22333D;
  --aurora:     #4FD8C4;
  --aurora-dim: #1E6F63;
  --signal:     #E0A05C;

  --display: 'Syne', 'Helvetica Neue', Arial, sans-serif;
  --body:    'Instrument Sans', system-ui, -apple-system, sans-serif;
  --mono:    'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  --gutter: clamp(20px, 5vw, 72px);
  --measure: 62ch;
  --rhythm: clamp(28px, 4vh, 48px);
}

* { box-sizing: border-box; }

html {
  background: var(--ink);
  -webkit-text-size-adjust: 100%;
  /* Native smooth-scroll is for the no-JS/no-Lenis path only (js/scroll.js
     adds the `lenis` class the moment it takes over). Running both at once
     is the conflict js/scroll.js's header warns about — two systems racing
     to own the same scroll position. */
  scroll-behavior: smooth;
  /* Opening an offering locks scroll. Without a reserved gutter the scrollbar
     vanishes at that moment and the whole page jumps ~15px sideways, which is
     a layout shift, and the CLS gate is exactly zero. */
  scrollbar-gutter: stable;
}
html.lenis { scroll-behavior: auto; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* Lenis's own recommended rules (from node_modules/lenis/dist/lenis.css),
   inlined here rather than shipped as a second stylesheet request. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth iframe { pointer-events: none; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--body);
  font-size: clamp(16px, 1.05vw + 13px, 19px);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* The canvas: fixed, behind everything, outside every act subtree so it can
   never contribute a layout shift. Removed entirely when there is no context. */
#nb-stage {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  display: block;
}

a {
  color: var(--aurora); text-decoration-thickness: 1px; text-underline-offset: 3px;
  transition: color 200ms cubic-bezier(.22,.61,.36,1);
}
a:hover { color: var(--paper); }
:focus-visible { outline: 2px solid var(--aurora); outline-offset: 3px; border-radius: 2px; }
@media (prefers-reduced-motion: reduce) { a { transition: none; } }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 20;
  background: var(--aurora); color: var(--ink);
  padding: 12px 18px; font-weight: 600; text-decoration: none;
}
.skip:focus { left: 12px; top: 12px; }

/* --- masthead ------------------------------------------------------------ */
.masthead {
  position: relative; z-index: 3;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px 32px; flex-wrap: wrap;
  padding: clamp(18px, 3vh, 32px) var(--gutter);
}
/* The wordmark is drawn, not set. brand/wordmark.svg is custom lettering
   constructed on a 5x7 module — the same engineered logic as the Needle mark
   beside it — so it says what the site argues before a word is read. Both are
   given explicit intrinsic sizes in the markup so they reserve their box before
   they load; the CLS gate is exactly zero. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  line-height: 0;
}
.wordmark__mark { width: 26px; height: 26px; display: block; }
.wordmark__type {
  height: 15px;
  width: auto;
  display: block;
  /* The SVG paints with currentColor via a custom property; give it the page's
     ink so it inherits light and dark treatment from one place. */
  --color-text-primary: var(--paper);
}
.wordmark:hover .wordmark__type { --color-text-primary: var(--aurora); }
.masthead nav { display: flex; gap: clamp(14px, 2.5vw, 30px); }
.masthead nav a {
  font-family: var(--mono); font-size: 0.74rem;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--paper-dim); text-decoration: none;
  transition: color 200ms cubic-bezier(.22,.61,.36,1);
}
.masthead nav a:hover { color: var(--aurora); }
@media (prefers-reduced-motion: reduce) { .masthead nav a { transition: none; } }

/* The phone number. On a device with a mouse it reads as plain text — most
   visitors on a desktop cannot call it, and a link that does nothing on
   click but change colour is worse than no link. On a touchscreen it is
   unambiguously tappable, at a real target size. */
.tel {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
@media (pointer: fine) {
  .tel {
    color: inherit;
    text-decoration: none;
    pointer-events: none;
    user-select: text;
    cursor: text;
  }
}
@media (pointer: coarse) {
  .tel {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--aurora);
    text-decoration: underline;
  }
}

/* --- acts ----------------------------------------------------------------
   Height reserved up front. svh, not vh, so a mobile URL bar collapsing does
   not resize every section and spend the entire CLS budget in one gesture. */
main { position: relative; z-index: 2; }

.act {
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding: clamp(64px, 12vh, 140px) var(--gutter);
}
.act__ink { width: 100%; max-width: 1080px; margin: 0 auto; }

/* --- copy arrival ----------------------------------------------------------
 * Driven entirely by js/entrance.js — a scroll-linked, latched spring per
 * item, never a CSS transition. This stylesheet only supplies the mask a
 * split heading's words rise out of; every starting transform (a word at
 * translateY(115%), everything else at translateY(22px)) is set as an inline
 * style by entrance.js itself, and ONLY on a block it has confirmed is
 * off-screen at setup — the CLS gate is exactly zero, so nothing visible at
 * first paint is ever primed with a starting offset.
 *
 * Reduced motion (and `?still=1`): js/entrance.js returns before priming
 * anything, so no inline transform is ever written and nothing here applies —
 * every block is simply composed, full-size, from its first frame. */
.reveal-mask { display: inline-block; overflow: hidden; vertical-align: top; }
.reveal-word { display: inline-block; will-change: transform; }

.eyebrow {
  font-family: var(--mono); font-size: 0.7rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.18em;
  color: var(--aurora); margin: 0 0 var(--rhythm);
}

h1, h2, h3 { font-family: var(--display); letter-spacing: -0.028em; text-wrap: balance; }

h1 {
  font-weight: 800;
  font-size: clamp(2.5rem, 7.2vw, 5.4rem);
  line-height: 0.98;
  margin: 0 0 clamp(20px, 3vh, 34px);
  max-width: 16ch;
}
h2 {
  font-weight: 700;
  font-size: clamp(1.9rem, 4.6vw, 3.4rem);
  line-height: 1.06;
  margin: 0 0 clamp(18px, 2.6vh, 30px);
  max-width: 20ch;
}
h3 { font-weight: 700; font-size: 1.22rem; line-height: 1.2; margin: 0 0 6px; }

p { margin: 0 0 1.05em; max-width: var(--measure); }
.lede { font-size: clamp(1.05rem, 1.1vw + 0.85rem, 1.35rem); color: var(--paper); max-width: 54ch; }
.byline, .note { color: var(--paper-dim); font-size: 0.94rem; }
.note { border-left: 2px solid var(--line-2); padding-left: 18px; margin-top: var(--rhythm); }

/* --- buttons ------------------------------------------------------------- */
.cta-row { display: flex; flex-wrap: wrap; gap: 12px 16px; margin-top: var(--rhythm); max-width: none; }
.btn {
  display: inline-block;
  font-family: var(--mono); font-size: 0.78rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none;
  padding: 14px 24px;
  border: 1px solid var(--line-2); border-radius: 2px;
  color: var(--paper); background: transparent;
  transform: translateY(0) scale(1);
  /* Resting/leaving curve — weight settles down quickly and plainly. */
  transition: border-color 200ms ease-out, color 200ms ease-out,
              background-color 200ms ease-out, transform 200ms ease-out;
}
.btn:hover {
  border-color: var(--aurora); color: var(--aurora);
  transform: translateY(-2px) scale(1.015);
  /* Arriving curve — a small overshoot, the house style from mascot.css. */
  transition: border-color 280ms cubic-bezier(.28,1.5,.5,1), color 280ms cubic-bezier(.28,1.5,.5,1),
              background-color 280ms cubic-bezier(.28,1.5,.5,1), transform 360ms cubic-bezier(.28,1.5,.5,1);
}
.btn:active {
  transform: translateY(0) scale(0.95);
  transition: transform 120ms ease-out;
}
.btn--go { background: var(--aurora); border-color: var(--aurora); color: var(--ink); font-weight: 600; }
.btn--go:hover { background: var(--paper); border-color: var(--paper); color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .btn { transition: none; transform: none !important; } }

.scroll-hint {
  position: absolute; bottom: clamp(20px, 4vh, 40px); left: var(--gutter);
  font-family: var(--mono); font-size: 0.66rem;
  text-transform: uppercase; letter-spacing: 0.22em;
  color: var(--paper-faint); margin: 0;
}
.act--arrival { position: relative; }

/* --- packages ------------------------------------------------------------ */
.packages {
  list-style: none; margin: var(--rhythm) 0 0; padding: 0;
  display: grid; gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
}
.pkg { background: var(--ink-2); padding: clamp(22px, 2.6vw, 34px); }
.pkg--lead { background: #0C1A1C; box-shadow: inset 2px 0 0 var(--aurora); }
.pkg h3 { color: var(--paper); }
.pkg__price {
  font-family: var(--mono); font-size: 1.5rem; font-weight: 600;
  color: var(--aurora); margin: 0 0 10px; font-variant-numeric: tabular-nums;
}
.pkg__price span { font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--paper-faint); }
.pkg__price .pkg__was { display: inline-block; font-family: var(--body, inherit); font-size: 0.85rem; font-weight: 400; text-decoration: line-through; color: var(--paper-faint); margin-left: 8px; }
.pkg__founding-tag { font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--signal); margin: 0 0 6px; }
.pkg__tag { font-style: italic; color: var(--paper-dim); font-size: 0.92rem; margin-bottom: 16px; }
.pkg ul { margin: 0; padding-left: 1.05em; color: var(--paper-dim); font-size: 0.93rem; }
.pkg li { margin-bottom: 7px; }

/* The founding-offer explainer, once, above the three cards. Paint-only
   border/background so it can never contribute layout shift — same
   constraint as everything else in this static-DOM section. */
.founding-offer {
  margin: 0 0 var(--rhythm);
  padding: clamp(18px, 2.2vw, 26px);
  border: 1px solid var(--aurora);
  background: rgba(79, 216, 196, 0.06);
}
.founding-offer .eyebrow { margin-bottom: 8px; }
.founding-offer p:last-child { margin: 0; color: var(--paper-dim); max-width: 66ch; }

/* --- the work ------------------------------------------------------------ */
.work-list {
  list-style: none; margin: var(--rhythm) 0 0; padding: 0;
  display: grid; gap: clamp(14px, 2vw, 22px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}
.work-card {
  border: 1px solid var(--line-2);
  background: rgba(10, 18, 24, 0.72);
  padding: clamp(20px, 2.4vw, 30px);
  position: relative;
}
.work-card__kind {
  font-family: var(--mono); font-size: 0.64rem;
  text-transform: uppercase; letter-spacing: 0.16em;
  color: var(--signal); margin: 0 0 12px;
}
.work-card h3 a { text-decoration: none; color: var(--paper); transition: color 200ms cubic-bezier(.22,.61,.36,1); }
.work-card h3 a:hover { color: var(--aurora); }
@media (prefers-reduced-motion: reduce) { .work-card h3 a { transition: none; } }
.work-card p { color: var(--paper-dim); font-size: 0.95rem; margin-bottom: 8px; }
.work-card__tech { font-family: var(--mono); font-size: 0.74rem; color: var(--paper-faint); margin: 0; }

/* --- proof --------------------------------------------------------------- */
.proof {
  margin-top: clamp(40px, 7vh, 80px);
  border-top: 1px solid var(--line-2);
  padding-top: clamp(26px, 4vh, 44px);
}
/* No section beats a blank section: nothing here should ever be able to
   override the hidden attribute the markup ships with. */
[data-budget-root][hidden] { display: none !important; }
.proof h3 { color: var(--paper); margin-bottom: 14px; }
.proof strong { font-family: var(--mono); font-size: 1.9rem; font-weight: 600; color: var(--aurora); font-variant-numeric: tabular-nums; }
.proof a { text-decoration: none; }
.proof a:hover strong { color: var(--paper); }
.proof__caption { font-family: var(--mono); font-size: 0.72rem; color: var(--paper-faint); letter-spacing: 0.04em; }
.proof__method { font-size: 0.84rem; color: var(--paper-faint); max-width: 64ch; }

.proof__table { width: 100%; border-collapse: collapse; margin: 22px 0; font-family: var(--mono); font-size: 0.78rem; }
.proof__table caption { text-align: left; color: var(--paper-faint); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.14em; padding-bottom: 10px; }
.proof__table th, .proof__table td { text-align: left; padding: 9px 14px 9px 0; border-bottom: 1px solid var(--line); }
.proof__table thead th { color: var(--paper-faint); font-weight: 500; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.12em; }
.proof__table tbody th { color: var(--paper-dim); font-weight: 400; }
.proof__table td { color: var(--aurora); font-variant-numeric: tabular-nums; }

/* --- rules + form -------------------------------------------------------- */
.rules { list-style: none; margin: 0 0 clamp(40px, 7vh, 76px); padding: 0; max-width: 60ch; }
.rules li { position: relative; padding-left: 30px; margin-bottom: 11px; color: var(--paper-dim); }
.rules li::before { content: "→"; position: absolute; left: 0; color: var(--aurora); font-family: var(--mono); }

.contact { margin-top: var(--rhythm); max-width: 540px; }
.field { margin: 0 0 18px; max-width: none; }
.field label {
  display: block; font-family: var(--mono); font-size: 0.68rem;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--paper-faint); margin-bottom: 7px;
  transform-origin: left top;
  transition: transform var(--spring-ui-ms, 200ms) var(--spring-ui, ease-out),
              color var(--spring-ui-ms, 200ms) var(--spring-ui, ease-out);
}
.field:focus-within label { transform: translateY(-2px); color: var(--aurora); }
@media (prefers-reduced-motion: reduce) { .field label { transition: none; } }
.field input, .field select, .field textarea {
  width: 100%; padding: 12px 14px;
  background: var(--ink-2); color: var(--paper);
  border: 1px solid var(--line-2); border-radius: 2px;
  font-family: var(--body); font-size: 1rem; line-height: 1.5;
  box-shadow: 0 0 0 0 rgba(79, 216, 196, 0);
  transition: border-color 240ms cubic-bezier(.22,.61,.36,1),
              box-shadow 240ms cubic-bezier(.22,.61,.36,1);
}
.field textarea { resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--aurora); outline: none;
  box-shadow: 0 0 0 3px rgba(79, 216, 196, 0.16);
}
@media (prefers-reduced-motion: reduce) {
  .field input, .field select, .field textarea { transition: none; }
}

/* The honeypot. A person never sees this field; a bot filling every field it
   finds will fill this one too, and api/quote.ts treats a filled honeypot as
   spam. Hidden off-screen rather than display:none so it still has a real,
   focusable-looking box for anything that only checks computed visibility. */
.field--hp {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- colophon + noscript ------------------------------------------------- */
.colophon {
  position: relative; z-index: 2;
  border-top: 1px solid var(--line);
  padding: clamp(30px, 5vh, 54px) var(--gutter);
  display: flex; flex-wrap: wrap; gap: 8px 40px;
  font-family: var(--mono); font-size: 0.7rem; letter-spacing: 0.06em;
  color: var(--paper-faint);
}
.colophon p { margin: 0; max-width: none; }

.noscript-note {
  position: relative; z-index: 3;
  margin: 0; padding: 18px var(--gutter);
  background: var(--ink-2); border-top: 1px solid var(--line-2);
  color: var(--paper-dim); font-size: 0.88rem; max-width: none;
}

/* --- no WebGL ------------------------------------------------------------
   The Stage sets data-gl="off" and removes the canvas when it cannot get a
   context, or when a context is lost and never comes back. The page then has
   to stand on its own, so each act gets a quiet static ground rather than the
   flat void the canvas was covering. This is a designed state, not an error
   state, and perf.mjs's no-WebGL sweep asserts it produces no console errors
   and no overflow. */
[data-gl="off"] .act--arrival {
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(79, 216, 196, 0.10), transparent 62%),
    linear-gradient(180deg, #071016 0%, var(--ink) 70%);
}
[data-gl="off"] .act--drift {
  background: radial-gradient(80% 60% at 20% 50%, rgba(30, 111, 99, 0.16), transparent 70%);
}
[data-gl="off"] .act--offer {
  background: linear-gradient(180deg, transparent, rgba(10, 18, 24, 0.8));
}

@media (max-width: 640px) {
  .act { min-height: auto; padding-block: clamp(56px, 10vh, 90px); }
  .act--arrival { min-height: 100svh; }
  .scroll-hint { display: none; }
}

/* --- phones --------------------------------------------------------------
   Written after a real pass on a 390-wide device against the live site, not
   from a breakpoint guess. Nothing here changes the desktop page. */

/* The measurement caption and its date were two inline spans with no
   separator between them, so on a narrow column they ran together as
   "...regular 4G Measured September 12, 2026" and read as one broken
   sentence. The date gets its own line. */
.proof__caption time { display: block; margin-top: 4px; }

@media (pointer: coarse) {
  /* Touch targets. The nav and the two work-card titles were 20-24px tall —
     fine for a cursor, a coin toss for a thumb. 44px is the smallest target
     both Apple and Google publish, and these are the links the page actually
     wants tapped. The padding is symmetrical so nothing moves: the rule
     applies before first paint, and the CLS gate stays at zero. */
  .masthead nav { gap: 4px; margin-left: -12px; }
  .masthead nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 12px;
  }
  .wordmark { display: inline-flex; align-items: center; min-height: 44px; }
  .work-card h3 a {
    display: inline-block;
    min-height: 44px;
    padding-block: 9px;
  }
  .colophon a, .proof__method a { display: inline-block; padding-block: 6px; }
}

@media (max-width: 480px) {
  /* Reclaiming the first screen on a phone.
   
     Measured, not guessed: at a 390x664 viewport the hero stack came to 750px
     against 664 of screen, so both calls to action sat under the fold — on the
     one page whose entire job is to get someone to start a project. The
     headline is the design and does not get sacrificed for it, so the space
     comes from everything around it.
   
     The masthead was the biggest single win. The wordmark and the three nav
     links were wrapping onto two rows and costing 129px; holding them on one
     line costs about half that. The rest is padding and the gaps between the
     three paragraphs under the headline. */
  /* Row 1: wordmark + phone. Row 2: nav, forced onto its own line by a
     100% flex-basis rather than a media-query-only reflow, so nothing here
     depends on script and nothing moves after first paint — the CLS gate
     stays at zero. */
  .masthead {
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    /* Both rows are 44px tap targets on a touchscreen, so the only room to
       give back is around them: no row gap and a thin top/bottom. That keeps
       the primary CTA above the fold on a 375x667 phone, where it sat before
       the phone number joined the header. */
    gap: 0 12px;
    padding-block: 4px;
  }
  .masthead .wordmark { order: 1; }
  .masthead .tel { order: 2; }
  .masthead nav { order: 3; flex-basis: 100%; gap: 0; margin-right: -12px; }
  .masthead nav a { font-size: 0.68rem; letter-spacing: 0.1em; }

  .act--arrival { padding-block: clamp(16px, 3vh, 48px) clamp(40px, 8vh, 72px); }
  h1 { font-size: clamp(2.1rem, 10.4vw, 3rem); margin-bottom: 16px; }
  .act--arrival .lede { font-size: 1.02rem; line-height: 1.5; margin-bottom: 12px; }
  .act--arrival .byline { font-size: 0.84rem; line-height: 1.45; margin-bottom: 0; }
  .cta-row { gap: 10px; margin-top: 14px; }
  .btn { padding: 15px 20px; }
}


/* --- the offerings procession -------------------------------------------
   Height and pinning only. Everything about how the panels look lives in
   css/act-offerings.css; these two rules are here because they are structural
   and because the Stage's scroll arithmetic depends on them.

   The height is fixed, unconditional, and identical in every path: WebGL on,
   WebGL off, JavaScript off, reduced motion, every breakpoint. It is never
   computed by script. That is the entire CLS mechanism for this section.

   It is 1000svh because the procession must carry panel 0 from t = -2.5 to
   panel 11 at t = +2.5, which is 16 steps of 0.62 viewports. svh rather than
   vh so a collapsing mobile URL bar cannot resize it. */
#offerings { min-height: 1000svh; padding: 0; display: block; }

.offer-pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  padding: clamp(64px, 12vh, 140px) var(--gutter);
  /* Not decoration. A transformed or overflowing descendant extends the
     scrollable overflow area, which changes documentElement.scrollHeight, which
     the Stage divides by to get page progress — so one overflowing element in
     here would silently rescale EVERY act's scroll window on the whole page. */
  contain: layout paint;
  overflow: clip;
  /* Vertical scroll always belongs to the page. On touch you get ripple,
     horizontal drag and flick; vertical drag is the page's, not ours. */
  touch-action: pan-y;
}

/* With no WebGL the pin stops pinning and the same twelve items become the
   section: a long, calm, readable list down the same reserved height. Switching
   between the two paths shifts nothing, which matters because data-gl is set at
   DOMContentLoaded and may land after first paint. */
[data-gl="off"] .offer-pin {
  position: static;
  height: auto;
  min-height: 100svh;
  align-items: flex-start;
  padding-block: clamp(80px, 14vh, 180px);
}


/* The concept builds show what they actually look like. posters/atlas.webp and
   posters/vector.webp are real captures of the two deployed builds, not mockups
   and not stock — which is the only kind of image this page is allowed to use
   about its own work. posters/halo.webp exists and is deliberately not here:
   Halo has nothing built behind it, and a poster of a thing that does not exist
   is exactly the claim this repo has a standing rule against. */
.work-card { overflow: hidden; }
.work-card__poster {
  display: block;
  width: calc(100% + 2px);
  margin: calc(-1 * clamp(20px, 2.4vw, 30px)) calc(-1 * clamp(20px, 2.4vw, 30px)) clamp(16px, 2vw, 22px);
  max-width: none;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  object-position: top center;
  border-bottom: 1px solid var(--line);
  opacity: 0.86;
  transition: opacity 260ms cubic-bezier(.22,.61,.36,1);
}
.work-card:hover .work-card__poster,
.work-card:focus-within .work-card__poster { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .work-card__poster { transition: none; } }

/* --- sticky "Get a quote" -------------------------------------------------
 * Three inputs decide its one output attribute, data-state (js/interact.js):
 *   pre-scroll   set at boot, before the first scroll — never present at all
 *                for a visitor with JavaScript off.
 *   hidden       set while #contact is on screen or the offer overlay is open.
 * `--sticky-pulse`, a small extra scale on top of the base transform, is
 * written by js/interact.js on every scroll-direction flip, sampled through
 * `--spring-ui` — see that file's header. */
.sticky-cta {
  position: fixed;
  z-index: 50;
  right: var(--gutter);
  bottom: clamp(18px, 3vh, 28px);
  display: flex;
  align-items: center;
  gap: 14px;
  opacity: 1;
  --sticky-pulse: 1;
  transform: translateY(0) scale(var(--sticky-pulse, 1));
  visibility: visible;
  transition: opacity 220ms ease-out, transform var(--spring-ui-ms, 200ms) var(--spring-ui, ease-out), visibility 0s linear 0s;
}
.sticky-cta[data-state="pre-scroll"],
.sticky-cta[data-state="hidden"] {
  opacity: 0;
  transform: translateY(14px) scale(var(--sticky-pulse, 1));
  visibility: hidden;
  pointer-events: none;
  transition: opacity 200ms ease-out, transform 200ms ease-out, visibility 0s linear 200ms;
}
@media (prefers-reduced-motion: reduce) {
  .sticky-cta { transition: none; }
  .sticky-cta[data-state="pre-scroll"],
  .sticky-cta[data-state="hidden"] { transition: none; }
}

.sticky-cta__bar { display: none; }
.sticky-cta__desktop { display: flex; align-items: center; gap: 14px; }
.sticky-cta__desktop .tel { font-family: var(--mono); }

@media (max-width: 640px) {
  .sticky-cta {
    left: 0; right: 0; bottom: 0;
    display: block;
    padding: 10px var(--gutter) calc(10px + env(safe-area-inset-bottom));
    background: var(--ink-2);
    border-top: 1px solid var(--line-2);
  }
  .sticky-cta__desktop { display: none; }
  .sticky-cta__bar { display: flex; gap: 10px; }
  .sticky-cta__bar .btn,
  .sticky-cta__bar .tel {
    flex: 1;
    text-align: center;
    justify-content: center;
  }
  /* Static, not toggled — reserved at this breakpoint whether the bar is
     showing or in one of its data-state="hidden"/"pre-scroll" states,
     because position: fixed content never moves body's own box either way.
     This is the whole reason the sticky CTA can never cover content on a
     phone: the space is always there, toggled or not. */
  body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
}

/* --- form status / confirmation -------------------------------------------
 * Ported alongside the fetch submit path in js/interact.js. */
.form-status { margin: 10px 0 0; font-size: 0.88rem; color: var(--paper-dim); }
.form-status[data-status="error"] { color: var(--signal); }
.form-confirm { color: var(--aurora); font-size: 1.05rem; max-width: 46ch; }

/* --- package card hover / press --------------------------------------------
 * `--mx`/`--my` (0..1, pointer position across the card face) and `--near`
 * (0..1, proximity before the pointer has entered) are written by
 * js/interact.js, smoothed through the `lift` preset — never per-frame for a
 * card that is not on screen or not being interacted with. Touch gets the
 * identical treatment through `data-pressed`, set on pointerdown and cleared
 * on pointerup/cancel, so nothing here is hover-only. */
.pkg {
  position: relative;
  --mx: 0.5; --my: 0.5; --near: 0;
  transform: translateY(0);
  transition: transform var(--spring-lift-ms, 220ms) var(--spring-lift, ease-out),
              box-shadow var(--spring-lift-ms, 220ms) var(--spring-lift, ease-out);
  box-shadow: inset 0 0 0 1px rgba(79, 216, 196, calc(var(--near, 0) * 0.35));
}
.pkg::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(240px circle at calc(var(--mx, 0.5) * 100%) calc(var(--my, 0.5) * 100%),
              rgba(79, 216, 196, 0.16), transparent 60%);
  transition: opacity 200ms ease-out;
}
.pkg:hover, .pkg[data-pressed] {
  transform: translateY(-4px);
}
.pkg:hover::before, .pkg[data-pressed]::before { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .pkg, .pkg::before { transition: none; transform: none !important; }
}

/* --- nav underline, drawn from whichever side the pointer entered ---------
 * `--from` (0 or 1) is set on pointerenter by js/interact.js; the underline
 * scales from `transform-origin: var(--from, 0) 0`, timed on `--spring-ui`. */
.masthead nav a {
  position: relative;
  --from: 0;
}
.masthead nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -3px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: calc(var(--from, 0) * 100%) 0;
  transition: transform var(--spring-ui-ms, 200ms) var(--spring-ui, ease-out);
}
.masthead nav a:hover::after,
.masthead nav a:focus-visible::after,
.masthead nav a[data-pressed]::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) { .masthead nav a::after { transition: none; } }

/* --- magnetic pull ---------------------------------------------------------
 * `--mag-x`/`--mag-y` (px) are written per frame, only while a pointer is
 * within pull range of a `.btn`, by js/interact.js's magnet spring — parked
 * (and this rule inert) the instant the spring settles back to (0, 0).
 * `translate` is the standalone CSS property (not `transform`'s function),
 * so it composes with .btn/.btn:hover/.btn:active's own `transform` instead
 * of fighting it for the same declaration. */
.btn {
  --mag-x: 0px; --mag-y: 0px;
  translate: var(--mag-x, 0px) var(--mag-y, 0px);
}
@media (prefers-reduced-motion: reduce) { .btn { translate: none !important; } }
