@charset "utf-8";
/* ============================================================
   ØSTERLINE — osterline.com
   Design of record: variant-a/index.html ("Nordic light").
   System fonts only. No @import, no url(), no external requests.

   MAINTENANCE NOTE
   Everything between CRITICAL:START and CRITICAL:END is mirrored
   VERBATIM into the inline <style> block in index.html so the
   above-the-fold render does not depend on this file. If you edit
   a rule inside that region, edit it in index.html too.
   `npm`-free check:  see README.md -> "Verifying the critical CSS mirror"
   ============================================================ */

/* CRITICAL:START */
/* ---------- Tokens ---------- */
:root{
  --paper:#fbfbf9;
  --paper-2:#f4f4f0;
  --white:#ffffff;
  --ink:#101012;          /* 18.9:1 on paper */
  --ink-2:#2c2c30;        /* 12.5:1 on paper */
  --ink-3:#5c5c61;        /*  6.5:1 on paper */
  --rule:#e0e0d9;         /* decorative hairline */
  --rule-2:#c6c6bd;       /* stronger hairline */
  --edge:#6e6e73;         /* form/control boundary, 4.5:1 on paper */
  --dash:#8a8a8f;         /* placeholder boundary, 3.3:1 on paper */
  --accent:#2f5d8c;       /* 6.5:1 on paper */
  /* Boot splash pre-flip state = the opposite theme, so the invert reads. */
  --boot-start:#101012; --boot-ink:#fbfbf9; --boot-accent:#9dc1e6;
  --dark:#101012;
  --on-dark:#ffffff;
  --on-dark-2:#d8d8dd;    /* 13.38:1 on ink. Was #b9b9bd at 9.72:1 — which
                             PASSED AAA but read badly: light type on a near-black
                             ground blooms, and this token carries 11-14px copy.
                             Dark grounds need more headroom than the ratio implies. */
  --on-dark-3:#9dc1e6;    /* accent on ink, ~9:1 */
  --danger:#8c1d1a;
  --danger-edge:#a3231f;
  --danger-bg:#fdf5f5;
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
  --gut:clamp(20px,5vw,64px);
  --sec:clamp(72px,11vw,168px);
  --header-h:74px;        /* .header-in height AND .mobile-nav top offset */
  --ease:cubic-bezier(.22,.61,.36,1);

  /* Surfaces that need a translucent or transparent form of --paper. These
     cannot be derived from --paper in CSS without colour-mix, so they are
     declared per theme and MUST be kept in step with it. */
  --header-bg:rgba(251,251,249,.86);
  --paper-0:rgba(251,251,249,0);
  /* Hairlines *inside* the full-width .dark band. That band inverts with the
     theme, so these invert with it too — they are not the same as --rule. */
  --dark-rule:#33333a;
  --dark-rule-2:#4a4a52;
}

/* ---------- Dark theme ----------
   Applied by [data-theme="dark"] only. The blocking script in index.html
   resolves prefers-color-scheme itself and always writes an explicit
   data-theme, so there is exactly one code path and no media-query duplicate.

   Every ratio below was COMPUTED from the sRGB relative-luminance formula
   against the two dark grounds (bg #101012, surface #1b1b1e); the weaker of
   the two is quoted, so each number is a floor. */
:root[data-theme="dark"]{
  --paper:#101012;
  --paper-2:#1b1b1e;
  --white:#16161a;        /* was a white surface; now a raised dark plane */
  --ink:#ffffff;          /* 17.18:1 */
  --ink-2:#e6e6ea;        /* 13.81:1 */
  --ink-3:#b9b9bd;        /*  8.79:1 */
  --rule:#2a2a2e;         /* decorative hairline, 1.20:1 — never a sole boundary */
  --rule-2:#3a3a40;       /* 1.52:1, decorative */
  --edge:#8a8a92;         /* control boundary, 5.02:1 */
  --dash:#71717a;         /* placeholder boundary, 3.56:1 (SC 1.4.11) */
  --boot-start:#fbfbf9; --boot-ink:#101012; --boot-accent:#2f5d8c;
  --accent:#9dc1e6;       /*  9.17:1 — the light-on-ink accent, so the slash
                              never disappears against a dark ground */
  --danger:#ff8a80;       /*  7.53:1 */
  --danger-edge:#ff8a80;
  --danger-bg:#2a1614;

  /* The full-width ink block inverts with the page so the section still breaks
     the rhythm. On a dark page that band becomes light, and its own foregrounds
     revert to the light-theme values, which are already verified on paper. */
  --dark:#fbfbf9;
  --on-dark:#101012;
  --on-dark-2:#4a4a50;   /* 8.94:1 on the inverted paper band */
  --on-dark-3:#2f5d8c;

  --header-bg:rgba(16,16,18,.86);
  --paper-0:rgba(16,16,18,0);
  /* The .dark band is LIGHT in this theme, so its rules take light values. */
  --dark-rule:#c6c6bd;
  --dark-rule-2:#e0e0d9;
}

/* The flipper's state colour: ink on light, amber on dark (7.09:1). */
:root{--amber:#ef8f3c;--flip-accent:var(--ink)}
:root[data-theme="dark"]{--flip-accent:var(--amber)}

/* ---------- Reset ---------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body,h1,h2,h3,h4,p,figure,blockquote,ul,ol,dl,dd,fieldset{margin:0}
ul,ol{padding:0;list-style:none}
img,svg{display:block;max-width:100%}
button,input,select,textarea{font:inherit;color:inherit}
html{scroll-behavior:smooth;scroll-padding-top:96px}

body{
  background:var(--paper);
  color:var(--ink-2);
  font-family:var(--sans);
  font-size:17px;
  line-height:1.62;
  letter-spacing:-0.005em;
  font-feature-settings:"kern" 1;
  -webkit-font-smoothing:antialiased;
}

/* ---------- Focus ---------- */
:focus{outline:2px solid var(--ink);outline-offset:3px}
:focus:not(:focus-visible){outline:none}
:focus-visible{
  outline:2px solid var(--ink);
  outline-offset:3px;
  border-radius:2px;
}
.dark :focus-visible{outline-color:var(--on-dark)}

/* ---------- Skip link ---------- */
.skip{
  position:absolute;left:16px;top:-100px;z-index:200;
  background:var(--ink);color:var(--white);
  padding:14px 20px;font-size:14px;letter-spacing:.06em;text-transform:uppercase;
  text-decoration:none;transition:top .18s var(--ease);
}
.skip:focus{top:16px}

/* ---------- Layout ---------- */
.wrap{width:100%;max-width:1360px;margin:0 auto;padding-left:var(--gut);padding-right:var(--gut)}
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:clamp(16px,2.4vw,32px)}
section{position:relative}
.section{padding-block:var(--sec)}
.rule{height:1px;background:var(--rule);border:0;margin:0}

/* The <svg> sprite that holds the logo geometry. Kept in flow but
   zero-sized: display:none breaks <use> references in some engines. */
.sprite{position:absolute;width:0;height:0;overflow:hidden}

/* ---------- Type ---------- */
.display{
  /* Sized against the ~705px hero column, not the viewport it sits in. At the
     old 124px max only ~5 characters fit per line, so any headline past five
     words stacked into six lines and pushed the lead and CTAs below the fold.
     `pretty` rather than `balance`: balance equalises line lengths, which in a
     narrow column means short stacked lines with 25-40% of the column empty.
     Measured: 3 lines / 350px -> 2 lines / 180px, fill 77% -> 84%. */
  font-size:clamp(38px,6.6vw,96px);
  line-height:.94;
  letter-spacing:-0.045em;
  font-weight:600;
  color:var(--ink);
  text-wrap:pretty;
}
.h2{
  /* Same fix as .display. At 64px in the 600px title column the section heads
     wrapped at ~9 characters and filled only 58-72% of it. Measured after:
     "Five industries. Five looks." and "Who you're working with." collapse
     from 2 lines to 1, contact from 3 to 2, fill rises to 89-91%. */
  font-size:clamp(29px,3.7vw,52px);
  line-height:1.02;
  letter-spacing:-0.035em;
  font-weight:600;
  color:var(--ink);
  text-wrap:pretty;
}
.h3{
  font-size:clamp(20px,2.1vw,27px);
  line-height:1.14;
  letter-spacing:-0.022em;
  font-weight:600;
  color:var(--ink);
}
.lead{
  font-size:clamp(17px,1.9vw,22px);
  line-height:1.5;
  letter-spacing:-0.014em;
  color:var(--ink-2);
  text-wrap:pretty;
}
.label{
  font-family:var(--mono);
  font-size:11px;
  line-height:1;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--ink-3);
  font-weight:500;
}
.num{
  font-family:var(--mono);
  font-size:11px;letter-spacing:.14em;color:var(--ink-3);
}
.small{font-size:15px;line-height:1.6;color:var(--ink-3)}
.tabular{font-variant-numeric:tabular-nums}
.measure-56{max-width:56ch}
.measure-52{max-width:52ch}
.measure-48{max-width:48ch}
.measure-40{max-width:40ch}

/* ---------- Buttons / links ---------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:12px;
  min-height:52px;padding:14px 26px;
  border:1px solid var(--ink);background:var(--ink);color:var(--white);
  font-size:15px;letter-spacing:.01em;font-weight:500;
  text-decoration:none;cursor:pointer;
  transition:background .22s var(--ease),color .22s var(--ease);
}
.btn:hover{background:var(--white);color:var(--ink)}
.btn .arrow{transition:transform .28s var(--ease)}
.btn:hover .arrow{transform:translateX(5px)}
.btn-ghost{background:transparent;color:var(--ink);border-color:var(--edge)}
.btn-ghost:hover{background:var(--ink);color:var(--white);border-color:var(--ink)}
.btn-invert{background:var(--white);color:var(--ink);border-color:var(--white)}
.btn-invert:hover{background:transparent;color:var(--white)}

.tlink{
  display:inline-flex;align-items:center;gap:9px;
  min-height:44px;
  color:var(--ink);text-decoration:none;font-size:15px;font-weight:500;
  border-bottom:1px solid var(--rule-2);
  transition:border-color .2s var(--ease);
}
/* The arrow moves on a compositor-only transform. Animating `gap` here
   would relayout the flex container on every frame of the hover. */
.tlink .arrow{transition:transform .28s var(--ease)}
.tlink:hover{border-color:var(--ink)}
.tlink:hover .arrow{transform:translateX(5px)}
a{color:var(--accent)}
a:hover{color:var(--ink)}

/* ---------- The mark ---------- */
/* Geometry lives once, in the <svg class="sprite"> in index.html.
   These rules style the <use> instances of it. stroke / stroke-width /
   fill are inherited SVG properties, so setting them on the <use>
   element passes them into the referenced shapes. */
/* The mark's two strokes are coloured through custom properties, because a
   custom property is the only thing that crosses into a <use> shadow tree.
   Ring = ink: it is a letterform, and it IS the Ø of the wordmark beside it,
   which is why the type only spells "sterline".
   Slash = accent: the gesture that continues into the rule and the east arrow. */
/* The mark is sized in em, not px, because in a lockup it is not an icon beside
   a word — it IS the Ø of that word. It has to track the type it sits in, so
   .lockup owns the font-size and both the mark and the wordmark scale from it.
   0.86em puts the circle a little above cap height, which is what a round
   capital should do. */
/* stroke-width 27 in a 240 viewBox at 0.86em renders ~1.74px against the
   wordmark's ~2.07px stems at 18px — deliberately a little lighter than the
   type, never heavier. Because both the mark and the type scale from
   .lockup's font-size, that ratio holds at every size (nav 18px, footer 24px). */
.mark-lockup{
  width:0.86em;height:0.86em;flex:none;stroke-width:27;color:inherit;
  --mark-ring:currentColor;
  --mark-slash:var(--accent);
}
.mark-hero{
  width:100%;height:auto;max-width:340px;aspect-ratio:1/1;
  --mark-ring:var(--ink);
  --mark-slash:var(--accent);
}
.mark-line{fill:none;stroke-width:21}
/* Ring keeps round ends so the draw-on animation terminates cleanly.
   Slash is butt-capped: the client wants its corners sharp, not rounded. */
.draw-c{stroke-linecap:round}
.draw-s{stroke-linecap:butt}

/* stroke-draw reveal — only under .js, so a no-JS render is never blank.
   Dash lengths track the geometry: ring: two 157.08deg arcs = 548.3, butt-capped,
   slash length 195.8×√2 = 276.9. Wrong values here strand the mark mid-draw. */
.js .draw-c{stroke-dasharray:548;stroke-dashoffset:548;animation:draw 1500ms var(--ease) 140ms forwards}
.js .draw-s{stroke-dasharray:277;stroke-dashoffset:277;animation:draw 760ms var(--ease) 900ms forwards}

/* Failsafe. Every stroke-draw mark hides itself via stroke-dashoffset and
   relies on its animation to reveal it. If that animation is cancelled or never
   runs while .js IS set, the mark would be stranded invisible. The head script
   adds .js-ready after 4000ms, and this pins anything still hidden.

   ⚠ COUPLING: this fires at 4000ms, so every load animation below MUST finish
   before then or it will be truncated mid-draw. Current worst case is the herO
   slash, ending at 1600 + 1020 = 2620ms — 1.4s of headroom. If those timings are
   slowed again (they have already gone 1x -> 6x -> 3x this project), raise the
   js-ready timeout in index.html to match, or this guard will cut the animation. */
.js.js-ready .ho-ring,.js.js-ready .ho-slash,
.js.js-ready .tf-ring,.js.js-ready .tf-slash{
  stroke-dasharray:none;stroke-dashoffset:0;
}

/* ---------- Accessibility section: the aperture reveal ----------
   The section reveals through a lens and zooms out, driven by scroll progress
   from main.js rather than a CSS scroll timeline.

   Why not CSS: `animation-timeline` would not drive this element at all. Five
   hypotheses were tested and eliminated — anonymous view() vs a named timeline,
   entry vs cover range, subject taller than the scrollport, a view-timeline-name
   conflict on the consuming element, and the animated property itself (opacity
   would not move either, with !important and fresh keyframes). The identical
   pattern works on .mag-lens 40 lines below, so it is something specific to this
   element that I could not isolate. JS progress is boring and it works.

   FAIL-SAFE: no clip in the resting state. The clip is applied only by JS while
   revealing, and cleared the moment it completes. No JS, failed JS or reduced
   motion all land on a fully visible section. */
.a11y-vt{position:relative}
#accessibility > .wrap{transform-origin:50% 50%;will-change:transform}

/* ---------- Accessibility section: the peephole cover ----------
   The section is covered by an opaque panel carrying the words "Look Closely".
   The o of "Closely" is a genuine HOLE in that panel — a radial-gradient mask
   punches it out — so you see the real section through it. The hole follows the
   pointer, and clicking anywhere tears the cover away.

   The cover is BUILT IN JS, deliberately. It is the only element that can hide
   this section, so it must not be able to exist unless the script that removes
   it is also running. No JS, no cover.

   The panel is paper and the section beneath is ink, so the peephole reads as a
   dark porthole in a white wall rather than dark-on-dark. */
.a11y-cover{
  position:absolute;inset:0;z-index:5;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:clamp(18px,2.4vw,34px);
  background:var(--paper);
  border:0;padding:0;margin:0;
  font:inherit;color:var(--ink);
  cursor:none;
  transition:opacity 1.9s ease-in .35s,
             -webkit-mask-image .35s var(--ease),
             mask-image .35s var(--ease);
  -webkit-mask-image:radial-gradient(circle var(--hole,46px) at var(--hx,50%) calc(var(--hy,50%) + var(--hole-bob,0px)),transparent 99%,#000 100%);
          mask-image:radial-gradient(circle var(--hole,46px) at var(--hx,50%) calc(var(--hy,50%) + var(--hole-bob,0px)),transparent 99%,#000 100%);
  transition:opacity .55s var(--ease);
}
.a11y-cover:focus-visible{outline:3px solid var(--accent);outline-offset:-6px;cursor:auto}
.a11y-cover.is-gone{pointer-events:none}
/* The words push outward as the panel dissolves, so the reveal reads as the
   cover being blown open rather than simply switched off. */
/* The exit is a KEYFRAME animation, not a transition.
   Transitions do not fire for a change caused by removing an animation: the
   words carry an infinite float, so cancelling it and setting transform in the
   same recalc snapped them straight to scale(5) with nothing in between.
   Replacing the float with an exit animation interpolates properly. */
.a11y-cover.is-gone .a11y-cover-words{
  animation:cover-blow 2.4s cubic-bezier(.16,.72,.28,1) forwards !important;
}
/* The hint just goes. Scaling an 11px mono caption to 5x alongside the display
   type read as a glitch, not a flourish. */
.a11y-cover.is-gone /* The hint just goes on exit. Scaling an 11px mono caption to 5x alongside the
   display type read as a glitch, not a flourish. */
.a11y-cover.is-gone .a11y-cover-hint{
  animation:none !important;
  opacity:0;
  transition:opacity .12s linear;
}
@keyframes cover-blow{
  0%   {transform:scale(1);   opacity:1}
  55%  {transform:scale(2.6); opacity:.55}
  100% {transform:scale(5);   opacity:0}
}
.a11y-cover.is-gone{
  animation:cover-dissolve 2.4s ease-in forwards;
  /* Drop the mask on exit. Leaving it punched meant the peephole stayed cut out
     of the panel while it dissolved, so the hole read as part of the disappear
     animation — a shrinking void travelling with the fade. The panel should
     leave as one solid sheet. */
  -webkit-mask-image:none;
          mask-image:none;
}
@keyframes cover-dissolve{
  0%  {opacity:1}
  45% {opacity:.75}
  100%{opacity:0}
}
.a11y-cover-words{
  font-size:clamp(34px,7vw,92px);
  font-weight:600;
  letter-spacing:-.04em;
  line-height:1;
  pointer-events:none;
  user-select:none;
  /* Long and eased-out: the words drift outward past the viewport rather than
     snapping. Opacity trails the scale so they are still legible while growing. */
  /* No transform transition: the bounce is driven by --hole-bob, and a 2.2s
     ease on transform made the words lag ~7px behind the hole, which is why the
     two looked like they were moving in opposite directions. The exit is a
     keyframe animation, so nothing here needs a transition anymore. */
  transition:opacity 1.6s ease-in .3s;
}
/* The words breathe so the panel reads as alive rather than as a failed load.
   The HOLE has to breathe with them: the mask centre is a fixed pixel value, so
   while the words bobbed the peephole stayed put and slid off the o. --hole-bob
   is registered as a real length so it can be animated, and it carries the same
   6s curve as the words. It is zeroed the moment the pointer takes over, since
   from then on the hole follows the cursor rather than the letter. */
@property --hole-bob{ syntax:'<length>'; inherits:true; initial-value:0px }
/* ONE clock. The words and the hole used to run two separate 6s animations,
   which started at different moments and drifted out of phase — the hole rose
   while the words fell. Now a single animated property drives both: the cover
   animates --hole-bob, the mask offsets by it, and the words simply translate by
   the same variable. They cannot disagree. */
@media (prefers-reduced-motion: no-preference){
  .a11y-cover{animation:hole-bob 6s ease-in-out 1.2s infinite}
}
.a11y-cover-words{transform:translate3d(0,var(--hole-bob,0px),0)}
@keyframes hole-bob{
  0%,100%{--hole-bob:0px}
  50%    {--hole-bob:-38px}
}
/* The o keeps its glyph — the hole is what makes it see-through, and the hole
   is parked on it. Marked so its position can be measured, not estimated. */
.cover-o{display:inline-block}
/* Sits directly under the words in flow, not pinned to the bottom of the
   section — it is a caption for the phrase, not a footer for the panel. */
.a11y-cover-hint{
  font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--ink-3);pointer-events:none;
  transition:transform 2.2s cubic-bezier(.16,.7,.3,1),opacity .9s ease-in;
}

/* ---------- Boot splash ----------
   A full-screen overlay shown once per session: black screen, the Ø draws in
   white, the slash cuts through and the screen INVERTS to white with a black
   mark, the Ø slides left as "sterline" is written, then the splash lifts.

   Built and inserted by main.js, never in the markup. It covers the entire site,
   so it must not be able to exist unless the code that removes it is running:
   no JS, no splash. It is also skipped entirely under reduced motion, and a
   click or any key dismisses it immediately. */
.boot{
  position:fixed;inset:0;z-index:9999;
  display:flex;align-items:center;justify-content:center;
  background:var(--boot-start);
  cursor:pointer;
  transition:background .5s var(--ease);
}
/* The splash starts on the INVERSE of the resolved theme and flips onto the
   theme itself, so it lands exactly on the page it is about to uncover. It used
   to flip to a hardcoded #fbfbf9: in dark mode that meant a white screen
   cross-fading into a near-black page — a full white flash right where the hero
   appears. Verified: flipped bg was rgb(251,251,249) against a body of
   rgb(16,16,18). --boot-start/-ink/-accent are the other theme's paper/ink/accent. */
.boot.is-flipped{background:var(--paper)}
.boot.is-done{
  opacity:0;pointer-events:none;
  transition:opacity .75s var(--ease);
}
.boot-lockup{display:flex;align-items:center;gap:.04em}
.boot-mark{
  width:clamp(84px,11vw,150px);height:auto;
  --mark-ring:var(--boot-ink);
  --mark-slash:var(--boot-accent);
  transition:none;
}
.boot.is-flipped .boot-mark{--mark-ring:var(--ink);--mark-slash:var(--accent)}
.boot-ring,.boot-slash{fill:none;stroke-width:21;stroke-linecap:butt}
/* Two elements on purpose. The OUTER does the clipping and nothing else, so at
   max-width:0 it is genuinely zero wide and no sliver of the "s" leaks. The
   INNER carries the type and a trailing pad, because at -.045em tracking the
   final "e" overhangs its advance width — and overflow clips the border box, so
   padding on the clipping element would have leaked at the start while padding
   on a single combined element could not do both jobs at once. */
.boot-word{
  display:inline-block;
  white-space:nowrap;overflow:hidden;max-width:0;
}
.boot-word-in{
  display:inline-block;
  font-size:clamp(46px,9vw,150px);
  font-weight:600;letter-spacing:-.045em;line-height:1;
  color:var(--boot-ink);
  white-space:nowrap;
  padding-right:.07em;
}
.boot.is-flipped .boot-word-in{color:var(--ink)}
.boot-skip{
  position:absolute;bottom:clamp(20px,4vw,44px);left:50%;transform:translateX(-50%);
  font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;
  color:#6e6e73;background:none;border:0;padding:8px 12px;cursor:pointer;
}
.boot-skip:focus-visible{outline:2px solid var(--boot-ink);outline-offset:2px}

.boot-ring {stroke-dasharray:548;stroke-dashoffset:548;animation:draw 1400ms var(--ease)  250ms both}
.boot-slash{stroke-dasharray:277;stroke-dashoffset:277;animation:draw  900ms var(--ease) 1500ms both}
.boot-lockup{animation:boot-shift 1300ms var(--ease) 2500ms both}
.boot-word  {animation:boot-write 1300ms var(--ease) 2500ms both}
@keyframes boot-shift{
  from{transform:translateX(var(--boot-half,120px))}
  to  {transform:translateX(0)}
}
@keyframes boot-write{
  from{max-width:0}
  to  {max-width:var(--boot-word-w,9em)}
}

/* ---------- The herO ----------
   The large mark in the hero. It draws itself on load, and its slash sweep is
   what triggers the dark-to-light flip (assets/js/main.js listens for that
   animation starting). Nothing is hidden at rest: without .js no dasharray is
   applied and the mark simply renders complete. */
.hero-o{grid-row:1;grid-column:8 / -1;align-self:center;justify-self:center;display:flex}
.ho{
  width:100%;height:auto;max-width:clamp(150px,24vw,380px);
  --mark-ring:var(--ink);
  --mark-slash:var(--accent);
}
.ho-ring,.ho-slash{fill:none;stroke-width:21}
.ho-ring{stroke-linecap:butt}
.ho-slash{stroke-linecap:butt}
.js .ho-ring {stroke-dasharray:548;stroke-dashoffset:548;animation:draw 1560ms var(--ease)  300ms both}
.js .ho-slash{stroke-dasharray:277;stroke-dashoffset:277;animation:draw 1020ms var(--ease) 1600ms both}
@media (prefers-reduced-motion: reduce){
  .js .ho-ring,.js .ho-slash{
    stroke-dasharray:none !important;stroke-dashoffset:0 !important;animation:none !important}
}
@media (max-width:900px){
  /* grid-row:auto is the whole fix. Desktop puts .hero-o and .hero-copy both on
     grid-row:1 in different columns; the mobile rules only widened the columns to
     1/-1 and never released the row, so both items resolved to the SAME cell and
     the O painted straight over the lead paragraph (measured: 120x120 overlap at
     390px). order:-1 was inert too — order does not apply to explicitly placed
     grid items. Releasing the row lets them auto-place into separate rows and
     makes order live again. Copy first, mark after: the nav already carries a
     lockup, and on desktop the mark reads after the copy anyway. */
  .hero-o{grid-row:auto;grid-column:1 / -1;justify-self:start;order:2}
  .ho{max-width:120px}
}

/* ---------- Theme flipper mark: draws itself once, on load ----------
   The Ø animation lives here now rather than in the hero. It runs a single time
   when the page loads — CSS animations do not repeat unless the element is
   recreated — and the slash inherits --flip-accent, so it draws in amber on a
   dark theme and ink on a light one. Nothing is hidden at rest: without .js the
   dasharray is never applied and the mark is simply complete. */
.theme-flip .tf-ring{stroke-linecap:butt}
.theme-flip .tf-slash{stroke-linecap:butt}
.js .theme-flip .tf-ring{
  stroke-dasharray:548;stroke-dashoffset:548;
  animation:draw 1560ms var(--ease) 300ms both;
}
.js .theme-flip .tf-slash{
  stroke-dasharray:277;stroke-dashoffset:277;
  animation:draw 1020ms var(--ease) 1600ms both;
}
@media (prefers-reduced-motion: reduce){
  .js .theme-flip .tf-ring,.js .theme-flip .tf-slash{
    stroke-dasharray:none !important;stroke-dashoffset:0 !important;animation:none !important}
}

/* ---------- Nav / footer logo: hover replay ----------
   The same choreography as the hero lockup, compressed to ~700ms because at
   18px there is no room for the travelling line or the east arrow — only the
   three beats that read at this size: ring, slash, then the name written.

   Pure CSS on :hover / :focus-visible: no JS, nothing hidden at rest, and the
   resting state is the finished logo. Touch devices simply never trigger it. */
.lockup .mark-lockup .lk-ring,
.lockup .mark-lockup .lk-slash{stroke-linecap:butt}
.lockup .mark-lockup .lk-slash{stroke-linecap:butt}

.js .lockup:hover .lk-ring,
.js .lockup:focus-visible .lk-ring{
  stroke-dasharray:548;stroke-dashoffset:548;
  animation:draw 1140ms var(--ease) 0ms both;
}
.js .lockup:hover .lk-slash,
.js .lockup:focus-visible .lk-slash{
  stroke-dasharray:277;stroke-dashoffset:277;
  animation:draw 780ms var(--ease) 660ms both;
}
/* Written, not slid: clip-path reveals in place so the letters never move. */
.js .lockup:hover .wordmark,
.js .lockup:focus-visible .wordmark{animation:nav-write 1260ms var(--ease) 780ms both}

@keyframes nav-write{
  from{clip-path:inset(0 100% 0 0)}
  to  {clip-path:inset(0 0 0 0)}
}
@media (prefers-reduced-motion: reduce){
  .js .lockup:hover .lk-ring,.js .lockup:focus-visible .lk-ring,
  .js .lockup:hover .lk-slash,.js .lockup:focus-visible .lk-slash{
    stroke-dasharray:none !important;stroke-dashoffset:0 !important;animation:none !important}
  .js .lockup:hover .wordmark,.js .lockup:focus-visible .wordmark{
    clip-path:none !important;animation:none !important}
}

/* ---------- Theme flipper + the line swipe ----------
   The client's idea: the Ø's own slash is what performs the theme change.
   Clicking the flipper sends an accent line east across the viewport, and the
   new theme is revealed in its wake. The brand device is the mechanism.

   The reveal uses the View Transitions API where available; the line is a plain
   fixed element so it still reads on browsers without it, where the theme just
   swaps instantly behind the same sweep. Reduced motion skips both. */
.theme-flip{
  display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;padding:0;
  background:none;border:1px solid var(--rule-2);border-radius:50%;
  color:var(--ink);cursor:pointer;
  transition:border-color .2s var(--ease),background .2s var(--ease);
  --mark-ring:currentColor;
  --mark-slash:var(--flip-accent);
}
.theme-flip:hover{border-color:var(--edge)}
/* The mark spins on exactly the same 1150ms clock as the line sweep and the
   theme wipe (1150ms), so the button and the page turn as one action rather
   than three things that happen to overlap. */
.theme-flip svg{transform-origin:50% 50%}
.js .theme-flip.is-turning svg{animation:flip-spin 1150ms cubic-bezier(.62,.02,.24,1)}
@keyframes flip-spin{ from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
@media (prefers-reduced-motion: reduce){
  .js .theme-flip.is-turning svg{animation:none}
}
.theme-flip svg{width:19px;height:19px;stroke-width:27;fill:none}

/* The travelling line. Fixed and above everything, but pointer-transparent and
   hidden from assistive tech — it is pure decoration on a state change. */
.theme-swipe{
  /* The line IS the wipe edge, not a separate rotated bar. Previously it was a
     20deg-rotated element travelling 112vw while the wipe's polygon leaned 32%
     of viewport width and travelled 210% — different angle, different distance,
     so they visibly disagreed. Both now animate the SAME value T from 0 to 128,
     with the same lean, easing and duration, so alignment is structural rather
     than tuned. A rotate() could never match: it is a fixed angle, while the
     polygon's lean is a ratio that changes with viewport aspect. */
  position:fixed;inset:0;
  background:var(--accent);
  clip-path:polygon(-0.5% 0%, 0% 0%, -28% 100%, -28.5% 100%);
  opacity:0;pointer-events:none;z-index:9999;
  view-transition-name:swipeline;
}
/* Keep the line's own group static — we do not want the browser cross-fading a
   snapshot of it; we want the real element animating on top of the wipe. */
::view-transition-group(swipeline){z-index:50}
::view-transition-image-pair(swipeline),
::view-transition-new(swipeline){animation:none !important}
::view-transition-old(swipeline){display:none !important}
.theme-swipe.is-running{animation:swipe-east 1150ms cubic-bezier(.62,.02,.24,1)}
@keyframes swipe-east{
  0%  {opacity:0;clip-path:polygon(-0.5% 0%, 0% 0%, -28% 100%, -28.5% 100%)}
  8%  {opacity:1}
  88% {opacity:1}
  100%{opacity:0;clip-path:polygon(127.5% 0%, 128% 0%, 100% 100%, 99.5% 100%)}
}

/* The new theme is clipped in on the same 1150ms curve, so the
   colour boundary rides directly behind the line rather than cross-fading. */
::view-transition-old(root){animation:none}
::view-transition-new(root){animation:theme-wipe 1150ms cubic-bezier(.62,.02,.24,1)}
/* A DIAGONAL wipe, leaning on the Ø's own slash angle, rather than a flat
   vertical edge. The straight wipe was the generic part — this is the brand's
   one gesture applied to the whole page. */
@keyframes theme-wipe{
  from{clip-path:polygon(-120% 0%, 0% 0%, -28% 100%, -120% 100%)}
  to  {clip-path:polygon(-120% 0%, 128% 0%, 100% 100%, -120% 100%)}
}
@media (prefers-reduced-motion: reduce){
  .theme-swipe,.theme-swipe.is-running{animation:none !important;opacity:0 !important}
  ::view-transition-old(root),::view-transition-new(root){animation:none !important}
}

@keyframes draw{to{stroke-dashoffset:0}}
@keyframes fadein{to{opacity:1}}

/* ---------- Header ---------- */
.site-header{
  position:sticky;top:0;z-index:100;
  background:var(--header-bg);
  -webkit-backdrop-filter:saturate(180%) blur(14px);
  backdrop-filter:saturate(180%) blur(14px);
  border-bottom:1px solid var(--rule);
}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .site-header{background:var(--paper)}
}
.header-in{
  display:flex;align-items:center;justify-content:space-between;gap:24px;
  height:var(--header-h);
}
/* The lockup owns the font-size so the mark can scale from it in em.
   The gap is a LETTER gap, not an icon gap: the Ø is the capital of the word
   that follows it, so anything more than a hairline reads as two objects. */
/* The lockup owns the font-size so the mark can scale from it in em.
   gap is 0 and the mark carries a negative right margin: the SVG has ~2.7% of
   its own box as padding around the circle, and the "s" brings its own left
   sidebearing, so a positive gap on top of those reads as two objects rather
   than two letters. */
.lockup{
  display:inline-flex;align-items:baseline;gap:0;
  text-decoration:none;color:var(--ink);min-height:44px;
  /* Ceiling for a 74px header: the mark is 0.86em and the lockup box has to
     clear the bar with a little air. 36px puts it at ~58px tall. */
  font-size:36px;
}
.lockup .wordmark{
  font-size:1em;font-weight:600;letter-spacing:-0.028em;
}
/* Baseline-align the mark with the type: nudge it down so the circle sits on
   the same baseline the letters do rather than floating on the em box. */
.lockup .mark-lockup{align-self:baseline;transform:translateY(0.11em);margin-right:-0.045em}
.nav-list{display:flex;align-items:center;gap:clamp(10px,1.7vw,30px)}
.nav-list a{
  display:inline-flex;align-items:center;
  min-height:44px;padding:0 2px;
  color:var(--ink-2);text-decoration:none;
  font-size:14.5px;letter-spacing:-0.004em;
  position:relative;
}
/* Scales on the compositor. Animating `right` here relayouts the whole
   backdrop-filtered sticky header on every frame of the hover. */
.nav-list a::after{
  content:"";position:absolute;left:0;right:0;bottom:4px;
  height:1px;background:var(--ink);
  transform:scaleX(0);transform-origin:left;
  transition:transform .3s var(--ease);
}
.nav-list a:hover{color:var(--ink)}
.nav-list a:hover::after{transform:scaleX(1)}
.header-cta{display:inline-flex;align-items:center;gap:20px}
/* The flipper is a separate control, not part of the CTA cluster — it needs
   real air between it and the button or the two read as one widget. */
.header-cta .theme-flip{margin-right:4px}
.header-cta .btn{min-height:44px;padding:10px 20px;font-size:14px}

/* The burger only exists as a control when its script can run. */
.burger{
  display:none;
  width:48px;height:48px;align-items:center;justify-content:center;
  background:transparent;border:1px solid var(--edge);cursor:pointer;padding:0;
}
.burger span{display:block;width:19px;height:1px;background:var(--ink);position:relative}
.burger span::before,.burger span::after{
  content:"";position:absolute;left:0;width:19px;height:1px;background:var(--ink);
  transition:transform .26s var(--ease),opacity .16s linear;
}
.burger span::before{top:-6px}
.burger span::after{top:6px}
.burger[aria-expanded="true"] span{background:transparent}
.burger[aria-expanded="true"] span::before{transform:translateY(6px) rotate(45deg)}
.burger[aria-expanded="true"] span::after{transform:translateY(-6px) rotate(-45deg)}

/* mobile panel.
   NOTE: this element MUST stay a sibling of <header>, never a descendant.
   .site-header carries backdrop-filter, which per Filter Effects makes it a
   containing block for fixed-position descendants — nesting the panel inside
   it collapses `inset` against the 74px header box instead of the viewport. */
.mobile-nav{
  position:fixed;inset:var(--header-h) 0 0 0;z-index:99;
  background:var(--paper);
  border-top:1px solid var(--rule);
  padding:clamp(24px,7vw,48px) var(--gut) 48px;
  display:none;
  overflow-y:auto;
}
.js .mobile-nav[data-open="true"]{display:block}
.mobile-nav li{border-bottom:1px solid var(--rule)}
.mobile-nav li:first-child{border-top:1px solid var(--rule)}
/* Scoped to the list. Unscoped `.mobile-nav a` (0-1-1) outranks `.btn`
   (0-1-0) and would repaint the panel's CTA as ink-on-ink at 34px. */
.mobile-nav ol a{
  display:flex;align-items:baseline;gap:16px;
  padding:20px 2px;min-height:56px;
  color:var(--ink);text-decoration:none;
  font-size:clamp(24px,7vw,34px);letter-spacing:-0.032em;font-weight:600;line-height:1;
}
.mobile-nav ol a .mn-num{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--ink-3);font-weight:400}
.mobile-nav .btn{margin-top:32px;width:100%}
.mobile-nav .m-meta{margin-top:28px}

@media (max-width:940px){
  /* Collapsing the nav behind a button is only safe when the button
     works. Without JS the full list stays on screen and wraps.
     Hide the <nav> itself, not just its <ul>: hiding the list alone
     leaves an empty "Primary" navigation landmark in the a11y tree. */
  .js #primary-nav,.js .header-cta{display:none}
  .js .burger{display:inline-flex}
  html:not(.js) .header-in{height:auto;flex-wrap:wrap;padding-block:12px;row-gap:6px;gap:16px}
  html:not(.js) .nav-list{flex-wrap:wrap;gap:6px 18px}
  html:not(.js) .header-cta .btn{padding:8px 16px}
}

/* ---------- Hero ---------- */
.hero{padding-top:clamp(48px,8vw,104px);padding-bottom:clamp(56px,9vw,120px);position:relative}
.hero::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:
    linear-gradient(180deg,var(--paper-0) 60%,var(--paper) 100%),
    repeating-linear-gradient(90deg,var(--rule) 0 1px,transparent 1px 25%);
  opacity:.5;
  -webkit-mask-image:linear-gradient(180deg,#000 0%,transparent 82%);
  mask-image:linear-gradient(180deg,#000 0%,transparent 82%);
}
.hero>*{position:relative}
.hero-top{display:flex;align-items:flex-start;justify-content:space-between;gap:32px;flex-wrap:wrap}
.hero-mark{
  grid-column:1 / span 4;
  align-self:start;
}
.hero-copy{grid-row:1;grid-column:1 / span 7}
.hero-grid{margin-top:clamp(28px,5vw,56px);align-items:start}
.hero h1{margin-top:18px}
.hero .lead{margin-top:clamp(22px,3vw,32px);max-width:46ch}
.hero-actions{display:flex;flex-wrap:wrap;gap:14px;margin-top:clamp(28px,4vw,40px)}
.hero-facts{
  margin-top:clamp(44px,7vw,80px);
  border-top:1px solid var(--rule-2);
  display:grid;grid-template-columns:repeat(3,1fr);
}
.hero-facts div{padding:22px 0 0;border-right:1px solid var(--rule)}
.hero-facts div:last-child{border-right:0}
.hero-facts dt{margin-bottom:8px}
.hero-facts dd{margin:0;font-size:clamp(15px,1.5vw,17px);color:var(--ink);letter-spacing:-0.014em;padding-right:16px}

@media (max-width:900px){
  .hero-mark{grid-column:1 / -1}
  .mark-hero{max-width:190px}
  .hero-copy{grid-row:auto;grid-column:1 / -1;order:1}
  .hero-facts{grid-template-columns:1fr;gap:0}
  .hero-facts div{border-right:0;border-bottom:1px solid var(--rule);padding-bottom:18px}
  .hero-facts div:last-child{border-bottom:0}
}

/* ---------- Reveal ----------
   Scoped to .js so that with scripting off nothing is ever left at
   opacity:0. Only opacity and transform animate — neither reflows. */
.js .reveal{opacity:0;transform:translateY(18px);transition:opacity .8s var(--ease),transform .8s var(--ease)}
.js .reveal.in{opacity:1;transform:none}
/* Dead-man's switch. The inline <head> script that sets `.js` also schedules
   `.js-ready` 4s later, so the hidden state cannot outlive the script that
   created it: if main.js never loads, parses or runs, this still un-hides
   everything. main.js reveals at the same 4s mark, so nothing changes when
   it does load. */
.js.js-ready .reveal{opacity:1 !important;transform:none !important}
.js .reveal[data-d="1"]{transition-delay:.08s}
.js .reveal[data-d="2"]{transition-delay:.16s}
.js .reveal[data-d="3"]{transition-delay:.24s}
.js .reveal[data-d="4"]{transition-delay:.32s}

body.locked{overflow:hidden}

/* Shown only when scripting is unavailable. */
.nojs-only{display:none}
html:not(.js) .nojs-only{display:block}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    transition-delay:0ms !important;
    scroll-behavior:auto !important;
  }
  .js .reveal{opacity:1 !important;transform:none !important}
  .js .draw-c,.js .draw-s,
  .btn:hover .arrow{transform:none}
  .tlink:hover .arrow{transform:none}
  .nav-list a:hover::after{transform:none}
}
/* CRITICAL:END */

/* ============================================================
   Below-the-fold. Loaded from this file only.
   ============================================================ */

/* ---------- Section header ---------- */
.sec-head{align-items:end;padding-bottom:clamp(30px,4vw,54px)}
.sec-head .sh-label{grid-column:1 / span 3}
.sec-head .sh-title{grid-column:4 / span 6}
.sec-head .sh-note{grid-column:11 / span 2;text-align:right}
@media (max-width:900px){
  .sec-head .sh-label,.sec-head .sh-title,.sec-head .sh-note{grid-column:1 / -1;text-align:left}
  .sec-head .sh-label{margin-bottom:14px}
  .sec-head .sh-note{margin-top:16px}
}

/* ---------- Offer tiers ---------- */
.tiers{border-top:1px solid var(--rule-2)}
.tier{
  border-bottom:1px solid var(--rule);
  padding-block:clamp(30px,4vw,48px);
  align-items:start;
  position:relative;
  isolation:isolate;
}
/* The hover wash bleeds past the grid on both sides so it does not stop dead on
   the last digit of the price, which sits hard against column 12's right edge.
   It is a pseudo-element rather than padding + negative margin on .tier itself,
   because that would drag the card's border-bottom wider than the section rules
   above and below it. Only the fill extends; the rule stays aligned. */
.tier::before{
  content:"";
  position:absolute;
  inset:0 calc(-1 * clamp(14px,1.8vw,28px));
  z-index:-1;
  background:transparent;
  transition:background .3s var(--ease);
}
.tier:hover::before{background:var(--paper-2)}
.tier .t-num{grid-column:1 / span 1}
.tier .t-name{grid-column:2 / span 4}
.tier .t-body{grid-column:6 / span 4}
.tier .t-price{grid-column:10 / span 3;text-align:right}
.tier .t-name .h3{margin-bottom:12px}
.t-list{margin-top:16px;display:grid;gap:9px}
.t-list li{
  position:relative;padding-left:20px;font-size:15px;color:var(--ink-3);line-height:1.5;
}
.t-list li::before{
  content:"";position:absolute;left:0;top:.62em;width:9px;height:1px;background:var(--edge);
}
.price{
  font-size:clamp(24px,3vw,38px);letter-spacing:-0.035em;font-weight:600;color:var(--ink);
  line-height:1;display:block;
}
.price-note{display:block;margin-top:10px}
.tiers-foot{padding-top:26px;display:flex;flex-wrap:wrap;gap:16px 40px;align-items:baseline;justify-content:space-between}
@media (max-width:900px){
  .tier .t-num{grid-column:1 / -1;margin-bottom:10px}
  .tier .t-name,.tier .t-body{grid-column:1 / -1}
  .tier .t-body{margin-top:14px}
  .tier .t-price{grid-column:1 / -1;text-align:left;margin-top:22px}
}

/* ---------- Dark block (accessibility) ---------- */
.dark{background:var(--dark);color:var(--on-dark-2)}
.dark .h2,.dark .h3,.dark .display{color:var(--on-dark)}
.dark .label{color:var(--on-dark-2)}
.dark .lead{color:var(--on-dark)}
.dark .rule{background:var(--dark-rule)}
.dark .small{color:var(--on-dark-2)}
.dark a{color:var(--on-dark-3)}
.dark a:hover{color:var(--on-dark)}
.a11y-grid{align-items:start;gap:clamp(24px,3vw,40px) clamp(16px,2.4vw,32px)}
.a11y-intro{grid-column:1 / span 6}
.a11y-intro .label{margin-bottom:22px}
.a11y-intro .lead{margin-top:26px}
.a11y-intro .a11y-note{margin-top:22px}
.a11y-stats{grid-column:8 / span 5;display:grid;gap:0;border-top:1px solid var(--dark-rule-2)}
.stat{padding:26px 0;border-bottom:1px solid var(--dark-rule)}
.stat .snum{
  font-size:clamp(30px,4.4vw,52px);line-height:1;letter-spacing:-0.04em;font-weight:600;color:var(--on-dark);
  display:block;margin-bottom:12px;
}
.stat p{font-size:14.5px;line-height:1.55;color:var(--on-dark-2);max-width:38ch}
.source{margin-top:20px;font-size:13px;line-height:1.6;color:var(--on-dark-2)}
.a11y-checks{
  grid-column:1 / -1;margin-top:clamp(30px,5vw,64px);
  border-top:1px solid var(--dark-rule-2);padding-top:clamp(24px,3vw,36px);
  display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(18px,2.4vw,32px);
}
.check{border-left:1px solid var(--dark-rule);padding-left:18px}
.check h3{font-size:15px;font-weight:600;color:var(--on-dark);letter-spacing:-0.01em;margin-bottom:8px;line-height:1.3}
.check p{font-size:14px;line-height:1.55;color:var(--on-dark-2)}
.a11y-cta{grid-column:1 / -1;margin-top:clamp(30px,4vw,52px);display:flex;flex-wrap:wrap;gap:16px 32px;align-items:center}
@media (max-width:900px){
  .a11y-intro,.a11y-stats{grid-column:1 / -1}
  .a11y-checks{grid-template-columns:1fr;gap:22px}
  .check{border-left:0;border-top:1px solid var(--dark-rule);padding-left:0;padding-top:16px}
}

/* ---------- Portfolio ---------- */
.work{display:grid;grid-template-columns:repeat(6,1fr);gap:clamp(16px,2.2vw,28px)}
.card{
  grid-column:span 2;
  border:1px solid var(--rule-2);
  background:var(--white);
  display:flex;flex-direction:column;
  transition:border-color .28s var(--ease);
}
.card:nth-child(1),.card:nth-child(2){grid-column:span 3}
.card:hover{border-color:var(--edge)}
.card-visual{
  position:relative;aspect-ratio:16/11;overflow:hidden;
  border-bottom:1px solid var(--rule);
  background:var(--paper-2);
}
.card:nth-child(1) .card-visual,.card:nth-child(2) .card-visual{aspect-ratio:16/9}
.card-visual svg{position:absolute;inset:0;width:100%;height:100%}
.card-body{padding:20px 20px 22px;display:flex;flex-direction:column;gap:8px;flex:1}
.card-body .h3{font-size:19px;letter-spacing:-0.018em}
.card-body p{font-size:14.5px;line-height:1.5;color:var(--ink-3)}
.chip{
  align-self:flex-start;margin-top:auto;padding-top:16px;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.15em;text-transform:uppercase;
  color:var(--ink-3);
  display:inline-flex;align-items:center;gap:7px;
}
.chip::before{content:"";width:6px;height:6px;border:1px solid var(--edge);flex:none}
@media (max-width:1000px){
  .work{grid-template-columns:repeat(4,1fr)}
  .card,.card:nth-child(1),.card:nth-child(2){grid-column:span 2}
}
@media (max-width:640px){
  .work{grid-template-columns:1fr}
  .card,.card:nth-child(1),.card:nth-child(2){grid-column:span 1}
}

/* ---------- Process ---------- */
.steps{border-top:1px solid var(--rule-2)}
.step{
  border-bottom:1px solid var(--rule);
  padding-block:clamp(28px,3.6vw,44px);
  align-items:start;
}
.step .s-num{grid-column:1 / span 1}
.step .s-title{grid-column:2 / span 4}
.step .s-body{grid-column:7 / span 5}
.step .s-body p{max-width:56ch;color:var(--ink-3);font-size:16px}
@media (max-width:900px){
  .step .s-num{grid-column:1 / -1;margin-bottom:10px}
  .step .s-title,.step .s-body{grid-column:1 / -1}
  .step .s-body{margin-top:12px}
}

/* ---------- About ---------- */
.about-grid{align-items:start}
.about-portrait{grid-column:1 / span 4}
.about-copy{grid-column:6 / span 6}
.about-copy .h3{margin-bottom:22px}
.about-copy p + p{margin-top:20px}
.about-copy p{color:var(--ink-2);font-size:17px}
.about-copy .about-link{margin-top:30px}
.portrait{
  border:1px solid var(--rule-2);
  background:
    linear-gradient(180deg,var(--white),var(--paper-2));
  aspect-ratio:4/5;position:relative;overflow:hidden;
}
.portrait svg{position:absolute;inset:0;width:100%;height:100%}
.portrait-cap{
  margin-top:14px;display:flex;justify-content:space-between;gap:16px;
}
@media (max-width:900px){
  .about-portrait{grid-column:1 / -1;max-width:320px}
  .about-copy{grid-column:1 / -1;margin-top:36px}
}

/* ---------- Testimonial placeholder ---------- */
.proof{padding-block:clamp(48px,7vw,104px)}
.proof .label{margin-bottom:22px}
.ph{
  border:1px dashed var(--dash);
  background:
    repeating-linear-gradient(135deg,rgba(138,138,143,.045) 0 8px,transparent 8px 16px),
    var(--paper);
  padding:clamp(32px,6vw,72px) clamp(24px,5vw,64px);
  text-align:center;
}
.ph-tag{
  display:inline-flex;align-items:center;gap:8px;
  border:1px solid var(--dash);
  padding:6px 12px;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-3);margin-bottom:22px;
}
.ph-quote{
  font-size:clamp(21px,3.2vw,38px);line-height:1.16;letter-spacing:-0.03em;
  color:var(--ink-3);font-weight:500;max-width:20ch;margin:0 auto;text-wrap:balance;
}
.ph p.small{margin-top:20px;max-width:52ch;margin-left:auto;margin-right:auto}

/* ---------- Contact ---------- */
.contact{padding-top:clamp(48px,7vw,96px)}
.contact-grid{align-items:start}
.contact-aside{grid-column:1 / span 4}
.contact-form{grid-column:6 / span 7}
.aside-item{border-top:1px solid var(--rule);padding:20px 0}
.aside-item:first-of-type{border-top:1px solid var(--rule-2)}
.aside-item dt{margin-bottom:8px}
.aside-item dd{margin:0;font-size:17px;color:var(--ink);letter-spacing:-0.012em}
.aside-item dd a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--rule-2);display:inline-flex;align-items:center;min-height:44px}
.aside-item dd a:hover{border-color:var(--ink)}

.field{margin-bottom:24px}
.field label{
  display:block;font-size:14px;font-weight:500;color:var(--ink);
  letter-spacing:.005em;margin-bottom:8px;
}
.req{color:var(--accent);font-weight:600}
.field .hint{display:block;font-size:13.5px;color:var(--ink-3);margin-top:-2px;margin-bottom:8px}
.field input,.field select,.field textarea{
  width:100%;min-height:52px;padding:14px 15px;
  background:var(--white);
  border:1px solid var(--edge);
  border-radius:0;
  font-size:16px;color:var(--ink);
  transition:border-color .2s var(--ease),box-shadow .2s var(--ease);
  -webkit-appearance:none;appearance:none;
}
.field textarea{min-height:150px;resize:vertical;line-height:1.5}
.field select{
  background-image:linear-gradient(45deg,transparent 50%,var(--ink) 50%),linear-gradient(135deg,var(--ink) 50%,transparent 50%);
  background-position:calc(100% - 20px) calc(50% + 1px),calc(100% - 14px) calc(50% + 1px);
  background-size:6px 6px,6px 6px;
  background-repeat:no-repeat;
  padding-right:44px;
}
.field input:hover,.field select:hover,.field textarea:hover{border-color:var(--ink)}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  border-color:var(--ink);outline:2px solid var(--ink);outline-offset:2px;
}
.field [aria-invalid="true"]{
  border-color:var(--danger-edge);border-width:2px;
  background-color:var(--danger-bg);
}
.err{
  display:none;align-items:flex-start;gap:8px;
  margin-top:8px;font-size:14px;color:var(--danger);line-height:1.45;font-weight:500;
}
.err[data-show="true"]{display:flex}
.err svg{flex:none;margin-top:3px}
.form-foot{display:flex;flex-wrap:wrap;align-items:center;gap:18px 24px;margin-top:8px}
.status{
  flex:1 1 260px;min-height:24px;font-size:14.5px;line-height:1.5;color:var(--ink-2);
}
.status[data-state="demo"]{
  border-left:2px solid var(--accent);padding-left:14px;color:var(--ink-2);
}
.status[data-state="error"]{
  border-left:2px solid var(--danger);padding-left:14px;color:var(--danger);font-weight:500;
}
.form-note{margin-top:22px}
/* No-JS: the demo form has nothing to submit to, so the button is
   withdrawn and the direct email path is surfaced instead. */
html:not(.js) .form-submit{display:none}
.nojs-form-note{
  margin-top:4px;
  border-left:2px solid var(--accent);padding-left:14px;
  font-size:14.5px;line-height:1.5;color:var(--ink-2);
}
@media (max-width:900px){
  .contact-aside,.contact-form{grid-column:1 / -1}
  .contact-form{margin-top:44px}
}

/* ---------- Footer ---------- */
.site-footer{border-top:1px solid var(--rule-2);padding-block:clamp(44px,6vw,72px) clamp(28px,4vw,40px)}
.foot-grid{align-items:start}
.foot-brand{grid-column:1 / span 4}
.foot-brand .mark-lockup{width:34px;height:34px}
/* Set on .lockup, not .wordmark, so the mark scales with the type too. */
.foot-brand .lockup{font-size:24px}
.foot-brand p{margin-top:16px;max-width:34ch}
.foot-col:nth-of-type(1){grid-column:6 / span 3}
.foot-col:nth-of-type(2){grid-column:9 / span 3}
.foot-col h2{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-3);font-weight:500;margin-bottom:16px}
.foot-col li a{
  display:inline-flex;align-items:center;min-height:44px;
  color:var(--ink-2);text-decoration:none;font-size:15px;
  border-bottom:1px solid transparent;
}
.foot-col li a:hover{color:var(--ink);border-color:var(--ink)}
.foot-bottom{
  margin-top:clamp(40px,6vw,72px);padding-top:22px;border-top:1px solid var(--rule);
  display:flex;flex-wrap:wrap;gap:10px 28px;justify-content:space-between;align-items:baseline;
}
@media (max-width:900px){
  .foot-brand{grid-column:1 / -1}
  .foot-col:nth-of-type(1),.foot-col:nth-of-type(2){grid-column:span 6;margin-top:36px}
}
@media (max-width:520px){
  .foot-col:nth-of-type(1),.foot-col:nth-of-type(2){grid-column:1 / -1}
}

/* ---------- 404 ---------- */
/* 404.html has no upper footer row, so the bottom bar must not carry the
   separator and offset it normally uses to sit under one. */
.site-footer > .foot-bottom:first-child{margin-top:0;padding-top:0;border-top:0}
.oops{
  min-height:64vh;display:flex;flex-direction:column;justify-content:center;
  padding-block:clamp(64px,12vw,140px);
}
.oops .code{margin-bottom:20px}
.oops h1{max-width:16ch}
.oops p{margin-top:clamp(20px,3vw,28px);max-width:52ch}
.oops .oops-actions{display:flex;flex-wrap:wrap;gap:14px;margin-top:clamp(28px,4vw,40px)}

/* Touch tap targets. A bare mailto/tel link is 18px tall — fine for a cursor,
   under every touch-target guideline on a phone, and email is the primary
   action on this site. Scoped to coarse pointers so desktop line rhythm is
   untouched. */
@media (pointer: coarse){
  a[href^="mailto:"],
  a[href^="tel:"]{
    display:inline-flex;align-items:center;min-height:44px;
  }
}

@media print{
  .site-header,.mobile-nav{position:static}
  .js .reveal{opacity:1;transform:none}
  .hero::before{display:none}
}
