
:root{
  --bg-base:#F5EAD3;
  --bg-raised:#FCF8EF;
  --bg-deep:#1E3634;
  --bg-deep-raised:#2A4744;
  --bg-deep-shade:#122320;
  --text-primary:#26332F;
  --text-secondary:#6B7268;
  --text-caption:#575E54;
  --ink-blue:#2A6B80;
  --brand-blue:#47899A;
  --teal-baby:#47899A;
  --hero-hush:#5697A8;
  --accent-baby:#C9683B;
  --text-on-deep:#F6F0E2;
  --text-on-deep-secondary:#B3C6C0;
  --accent:#B04516;
  --accent-deep:#7C2D0E;
  --accent-press:#9A3A10;
  --accent-bright:#CA5A1E;
  --accent-wash:#F2DFC5;
  --accent-mute:#DDB49A;
  --sage:#94A99E;
  --sage-deep:#78907F;
  --sun:#C4892F;
  --line:rgba(38,51,47,.16);
  --line-on-deep:rgba(246,240,226,.18);
  --line-accent:rgba(176,69,22,.28);
  --shadow-soft:rgba(30,54,52,.12);
  --shadow-mid:rgba(30,54,52,.28);
  --shadow-strong:rgba(30,54,52,.42);
  --shadow-clay:rgba(124,45,14,.38);
  --text-on-accent:#F6F0E2;
  --ink-blue-hover:#215A6E;
  --edge-rgb:245,234,211;
  --font-display:'Cormorant Garamond',Georgia,serif;
  --font-body:'DM Sans',-apple-system,'Helvetica Neue',sans-serif;
  --font-utility:'DM Sans',-apple-system,sans-serif;
  --font-hand:'Madhu Hand',ui-rounded,cursive;
  --r-card:18px;--r-media:14px;--r-pill:999px;
  --motion-fast:180ms;--motion-base:320ms;--motion-slow:640ms;
  --ease-standard:cubic-bezier(.33,0,.2,1);--ease-enter:cubic-bezier(.2,.7,.2,1);
  --space:8px;
  --glass-grad-a:rgba(255,255,255,.50);--glass-bg:rgba(255,252,244,.42);--glass-grad-b:rgba(255,255,255,.26);
  --glass-border:rgba(255,255,255,.60);
  --glass-cres-top:rgba(255,255,255,.98);--glass-cres-bottom:rgba(255,255,255,.62);
  --glass-cres-side:rgba(255,255,255,.42);--glass-glow:rgba(255,255,255,.16);--glass-prism-o:.9;
  color-scheme:light;
}
/* ---- night mode: the same site moved onto its deep-green surface. Every value here
   lives in data/theme.json under "night" — resampled LIGHTER rust/teal so all text
   pairings hold WCAG AA on the dark grounds (checked in-session 2026-08-19). The
   attribute is stamped on <html> before first paint by the boot script in <head>
   (stored choice first, otherwise the viewer's own sunrise/sunset). */
[data-theme="night"]{
  --bg-base:#182421;
  --bg-raised:#20302C;
  --bg-deep:#101B19;
  --bg-deep-raised:#1C2C29;
  --bg-deep-shade:#0A1211;
  --text-primary:#EAE4D3;
  --text-secondary:#A9B3A7;
  --text-caption:#C2CBBE;
  --ink-blue:#85C4D6;
  --ink-blue-hover:#A5D6E4;
  --brand-blue:#74AEBF;
  --teal-baby:#74AEBF;
  --hero-hush:#6BA4B5;
  --accent-baby:#D98A5F;
  --text-on-deep:#F2EEE0;
  --text-on-deep-secondary:#B3C6C0;
  --accent:#E08A52;
  --accent-deep:#EFA97C;
  --accent-press:#C9703B;
  --accent-bright:#E8935F;
  --accent-wash:#223029;
  --accent-mute:#6E5138;
  --sage:#94A99E;
  --sage-deep:#A9BEAC;
  --sun:#D9A251;
  --text-on-accent:#1D130B;
  --line:rgba(234,228,211,.18);
  --line-on-deep:rgba(246,240,226,.18);
  --line-accent:rgba(224,138,82,.32);
  --shadow-soft:rgba(0,0,0,.35);
  --shadow-mid:rgba(0,0,0,.5);
  --shadow-strong:rgba(0,0,0,.65);
  --shadow-clay:rgba(0,0,0,.55);
  --edge-rgb:24,36,33;
  --glass-grad-a:rgba(255,255,255,.10);--glass-bg:rgba(24,36,33,.46);--glass-grad-b:rgba(255,255,255,.05);
  --glass-border:rgba(255,255,255,.18);
  --glass-cres-top:rgba(255,255,255,.35);--glass-cres-bottom:rgba(255,255,255,.20);
  --glass-cres-side:rgba(255,255,255,.14);--glass-glow:rgba(255,255,255,.06);--glass-prism-o:.45;
  color-scheme:dark;
}
/* the theme switch: where the browser has the View Transitions API the crossing is ONE
   composited cross-fade of two full-page snapshots — no per-element work at all. The
   .theme-anim class below is the fallback for engines without it: every surface
   transitions its own paint, minus box-shadow (repainting every shadowed card's spread
   each frame was most of the lag she named 2026-08-25). Both paths are skipped under
   reduced motion by the toggle JS. */
::view-transition-old(root),::view-transition-new(root){
  animation-duration:520ms;animation-timing-function:var(--ease-standard)}
.theme-anim,.theme-anim *,.theme-anim *::before,.theme-anim *::after{
  transition:background-color 600ms var(--ease-standard),color 600ms var(--ease-standard),
    border-color 600ms var(--ease-standard),
    opacity 600ms var(--ease-standard) !important}
/* Her hand, as a typeface — cut from the sheets in story-src by tools/make-hand-font.js.
   Self-hosted and tiny (14kb), so it is fetched from the same origin as the page and there is
   no third party in the critical path for the one line of copy the hero exists to deliver.
   swap, not block: the fallback is a rounded system face at a similar x-height, and a
   sentence that arrives late in the wrong face beats a sentence that is invisible for 3s. */
@font-face{font-family:'Madhu Hand';font-style:normal;font-weight:400;font-display:swap;
  src:url(assets/fonts/madhu-hand.woff?v=mtdfum5g) format('woff')}
/* madhu-hand.ttf ships beside it but is deliberately NOT listed here. WOFF is the same
   outlines compressed and every browser since IE9 reads it, so a second src only gets both
   files fetched. The .ttf is there to be downloaded and installed — it is her typeface, and
   she should be able to use it in Figma. */
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--bg-base);color:var(--text-primary);font-family:var(--font-body);-webkit-font-smoothing:antialiased;overflow-x:hidden}
img,video{max-width:100%;display:block}
a{color:inherit}
/* site-wide typography rule: never a single word alone on the last line. build.js ties
   the last two words of every string together (see typo()); text-wrap:pretty asks the
   browser to avoid widows everywhere else it can. */
.nw{white-space:nowrap}
p,h1,h2,h3,li,figcaption,blockquote{text-wrap:pretty}
.svg-defs{position:absolute;width:0;height:0;overflow:hidden}

/* ------------------------------------------------ cursor
   A small solid dot. On a case-study card it does three quick flips and then rolls out
   into a pill offering "I'm curious".
   Three layers, because each needs its own transform: .dc is positioned by JS, .dc-spin
   owns the flips, .dc-body owns the shape. Stacking them on one element would mean the
   follow, the turn and the roll-out all fighting over 'transform'.
   dc-on is set by JS only, and only for a fine pointer, so if the script never runs or
   the visitor is on a touch device the real cursor is never taken away. */
body.dc-on,body.dc-on a,body.dc-on button,body.dc-on [role=tab]{cursor:none}
.dc{position:fixed;left:0;top:0;z-index:9999;pointer-events:none;opacity:0;
  will-change:transform;transition:opacity .25s linear}
.dc.live{opacity:1}
.dc-spin{position:absolute;left:0;top:0;transform:translate(-50%,-50%);transform-style:preserve-3d}
/* a solid circle has no features, so a spin would be invisible — rotateY works because
   the disc foreshortens to a line and back on each half turn */
.dc.turning .dc-spin{animation:dcTurn .5s cubic-bezier(.5,0,.5,1)}
@keyframes dcTurn{
  from{transform:translate(-50%,-50%) rotateY(0deg)}
  to{transform:translate(-50%,-50%) rotateY(1080deg)}}
.dc-body{height:13px;width:13px;border-radius:999px;background:var(--accent);
  display:flex;align-items:center;justify-content:center;overflow:hidden;white-space:nowrap;
  box-shadow:0 2px 8px rgba(38,51,47,.22);
  transition:width .34s var(--ease-enter),height .34s var(--ease-enter),background .2s linear}
/* night: the dot trades terracotta for the ink blue (2026-08-19 her ask). Night's
   ink-blue is a light value, so the dark bg-base label on the pill still reads. */
[data-theme="night"] .dc-body{background:var(--ink-blue)}
.dc-body span{opacity:0;transition:opacity .16s linear;padding:0 16px;
  font-family:var(--font-utility);font-weight:500;font-size:.62rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--bg-base)}
/* the pill animates width, so it can't be auto — the cursor code measures the label
   at hover time and pins the target width in --dc-pill-w (140px is "I'm curious") */
.dc.pill .dc-body{width:var(--dc-pill-w,140px);height:34px}
.dc.pill .dc-body span{opacity:1;transition-delay:.1s}
.dc.go .dc-spin{animation:dcGo .34s var(--ease-standard) forwards}
@keyframes dcGo{
  from{transform:translate(-50%,-50%) scale(1);opacity:1}
  to{transform:translate(-50%,-50%) scale(1.35);opacity:0}}
@media (hover:none),(pointer:coarse){.dc{display:none}body.dc-on{cursor:auto}}

/* Brushed colour across the cream field + paper grain.
   This was three big radial-gradients standing in for warm light. She asked for "no
   gradients — think more paint strokes texture", so it is now her own brush swatch: the
   texture is an alpha-only mask (tools/make-paint-wash.js strips the swatch's own shading
   and mirror-quilts it so it tiles), and the COLOUR comes from a palette token painted
   through it. Retune the wash by editing --accent-wash, not the PNG. */
.atmosphere{position:fixed;inset:0;pointer-events:none;z-index:0;
  background:var(--accent-wash);opacity:.55;
  -webkit-mask:url(assets/hero/paint-wash.png?v=mtdfum5g) repeat center/504px 2032px;
  mask:url(assets/hero/paint-wash.png?v=mtdfum5g) repeat center/504px 2032px}
body::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:60;opacity:.045;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}

/* chalk underline in fern */
.chalk-line{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='9' viewBox='0 0 140 9'%3E%3Cfilter id='c' x='-10%25' y='-150%25' width='120%25' height='400%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.14 0.9' numOctaves='2' seed='7'/%3E%3CfeDisplacementMap in='SourceGraphic' scale='4'/%3E%3C/filter%3E%3Cpath d='M3 5 Q 35 3.5 70 4.5 T 137 4.5' fill='none' stroke='%231B6E49' stroke-width='2.6' stroke-linecap='round' stroke-dasharray='9 2 14 3 6 2 11 2' filter='url(%23c)' opacity='.85'/%3E%3C/svg%3E");
  background-repeat:repeat-x;background-position:left bottom;background-size:140px 9px;padding-bottom:.14em}

/* ------------------------------------------------ liquid glass
   Four things make glass read as glass, and the old version only had one of them:
   1. it bends what is behind it       → backdrop-filter:url(#glassWarp), a low-frequency
                                         feDisplacementMap, so edges of things behind
                                         the panel visibly shift
   2. light catches the top and bottom → bright inset crescents, not a flat 1px line
   3. the rim splits light like a prism→ a faint conic rainbow masked to the edge only
   4. it is never perfectly still      → a slow, tiny float
   No sweeping shimmer on hover — it read as a swipe effect, not as a material. */
.glass{
  position:relative;
  background:linear-gradient(150deg, var(--glass-grad-a) 0%, var(--glass-bg) 52%, var(--glass-grad-b) 100%);
  -webkit-backdrop-filter:blur(20px) saturate(1.5);
  backdrop-filter:blur(20px) saturate(1.5);
  border:1px solid var(--glass-border);
  box-shadow:
    inset 0 1.5px 1.5px -1px var(--glass-cres-top),
    inset 0 -2px 2px -1px var(--glass-cres-bottom),
    inset 1.5px 0 2px -1px var(--glass-cres-side),
    inset -1.5px 0 2px -1px var(--glass-cres-side),
    inset 0 0 18px var(--glass-glow),
    0 10px 30px var(--shadow-soft);
}
/* the prism rim. Sits on the edge only, so the middle of the panel stays clean. */
.glass::after,.theme-toggle .tt-rim{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:0;
  background:conic-gradient(from 38deg,
    rgba(255,255,255,0) 0deg, rgba(255,120,128,.34) 22deg, rgba(255,214,96,.30) 42deg,
    rgba(126,240,186,.26) 62deg, rgba(112,178,255,.34) 82deg, rgba(192,152,255,.24) 102deg,
    rgba(255,255,255,0) 128deg);
  -webkit-mask:linear-gradient(#000,#000) content-box exclude,linear-gradient(#000,#000);
  mask:linear-gradient(#000,#000) content-box exclude,linear-gradient(#000,#000);
  padding:1.5px;mix-blend-mode:screen;opacity:var(--glass-prism-o)}
.glass>*{position:relative;z-index:1}
/* .glass-dark and .float were both dropped with the button rework and are not referenced by
   any markup or script. .glass-dark existed only for the case-study tags, which are now
   chips from the button system; .float was the slow bob on the hero CTAs, which the control
   sheet rules out — a flat control that drifts contradicts "solid fills only". The glass
   material itself is untouched and still carries the nav and the panels. */

/* ------------------------------------------------ buttons
   From Madhu's control sheet. The governing note on it is "solid fills only — the label
   always sits on flat colour, so it stays readable. The watercolour lives in the artwork,
   not in the controls." So everything a control could borrow from the hero — glass, paint
   texture, the drawn ink outline, drop shadows — is deliberately absent. A button is a
   flat fill or a flat outline, and nothing else. The art and the UI stay separate.

   Three ranks, in order of how loud they are:
     .solid    terracotta fill, cream label     — one per view, the thing to actually do
     .outline  terracotta line, terracotta label — the alternative
     .teal     baby-teal line, teal label        — quiet; case studies, tags, icons

   Labels are uppercase and widely letterspaced, so the glyph count is small and the
   letterforms carry the weight — which is why the size steps below move padding and
   tracking together rather than just font-size. */
.btn{font-family:var(--font-utility);font-weight:500;font-size:.75rem;letter-spacing:.14em;
  text-transform:uppercase;text-decoration:none;white-space:nowrap;
  padding:0 26px;border-radius:var(--r-pill);min-height:48px;
  display:inline-flex;align-items:center;justify-content:center;gap:10px;
  border:1.5px solid transparent;background:none;color:var(--text-primary);
  cursor:pointer;position:relative;
  transition:background-color var(--motion-fast) var(--ease-standard),
             border-color var(--motion-fast) var(--ease-standard),
             color var(--motion-fast) var(--ease-standard)}

/* ---- buttons swap into her hand on hover, same move as the nav links (2026-08-19:
   "do the handwriting thing for all my buttons"). Overlay is absolute so the pill never
   changes width; the sans label goes transparent under it. The hand ink colour rides a
   per-variant custom property because the label's own color is the thing being hidden.
   Button labels are uppercase, so the hand overlay is too — data-hand carries the label
   already capitalised, bang included ('!' has been in Madhu Hand since 2026-08-19,
   composed from her l and her full stop). Same -.52em baseline constant as the nav: it
   folds both fonts' metrics and scales with the type size. */
.btn[data-hand]::after{content:attr(data-hand);content:attr(data-hand) / "";
  position:absolute;left:0;right:0;top:50%;transform:translateY(-.52em);text-align:center;
  font-family:var(--font-hand);font-size:1.05em;letter-spacing:0;text-transform:none;font-weight:400;
  color:var(--hand-ink,var(--ink-blue));
  opacity:0;transition:opacity var(--motion-fast) var(--ease-standard);pointer-events:none;white-space:nowrap}
@media (hover:hover){
  /* .btn.btn out-guns the variant hover rules below: .btn.outline:hover sets a cream
     label at EQUAL specificity later in the sheet, which kept the sans visible under
     the handwriting on the resume buttons (2026-08-19 "the old text doesn't disappear") */
  .btn.btn[data-hand]:hover,.btn.btn[data-hand]:focus-visible{color:transparent}
  .btn[data-hand]:hover::after,.btn[data-hand]:focus-visible::after{opacity:1}
}
.btn.solid{--hand-ink:var(--text-on-accent)}
.btn.outline{--hand-ink:var(--accent-deep)}
/* an outline button FILLS with accent-press on hover, and the resting hand ink is
   accent-deep — dark orange on dark rust, invisible. The ink flips to cream with the fill. */
.btn.outline:hover,.btn.outline:focus-visible{--hand-ink:var(--text-on-accent)}

/* PRIMARY. The hover and pressed steps are the accent walked down in value, not the accent
   plus a shadow — on a flat control the only honest way to show depression is that the
   colour gets heavier. --accent-deep is already the bottom of that walk, so pressed lands
   there and hover sits between the two. */
.btn.solid{background:var(--accent);color:var(--text-on-accent)}
.btn.solid:hover{background:var(--accent-press)}
.btn.solid:active{background:var(--accent-deep)}

/* SECONDARY. Fills with --accent-press rather than --accent on hover: a cream label on the
   plain accent measures 4.47:1, which is a hair under the 4.5 small text wants, and the
   hover state is supposed to be the darker one anyway. */
.btn.outline{border-color:var(--accent);color:var(--accent)}
.btn.outline:hover{background:var(--accent-press);border-color:var(--accent-press);color:var(--text-on-accent)}
.btn.outline:active{background:var(--accent-deep);border-color:var(--accent-deep);color:var(--text-on-accent)}

/* TERTIARY — the baby teal. The LINE is --teal-baby and the LABEL is --ink-blue, which is
   what the sheet shows and is also the only version that passes contrast: baby teal is a
   tint, and a tint that reads correctly as a 1.5px line on cream is far too pale to set
   small letterspaced type in. On hover the line fills and the label flips to cream.
   The hover fill is --ink-blue and not --teal-baby for the same reason: a cream label on
   baby teal is only 3.6:1, whereas on the full ink-blue it is 5.4:1. */
.btn.teal{border-color:var(--teal-baby);color:var(--ink-blue)}
.btn.teal:hover{background:var(--ink-blue);border-color:var(--ink-blue);color:var(--text-on-accent)}
.btn.teal:active{background:var(--bg-deep);border-color:var(--bg-deep);color:var(--text-on-deep)}

/* SIZES */
.btn.sm{min-height:38px;padding:0 20px;font-size:.68rem;letter-spacing:.12em}
.btn.lg{min-height:62px;padding:0 40px;font-size:.9rem;letter-spacing:.16em}

/* ICON — a circle, so it is sized rather than padded. aspect-ratio keeps it round whatever
   the min-height above resolves to. */
.btn.icon{padding:0;width:56px;min-width:56px;height:56px;min-height:56px;aspect-ratio:1;
  border-radius:50%;font-size:1rem;letter-spacing:0}
.btn.icon.sm{width:42px;min-width:42px;height:42px;min-height:42px;font-size:.85rem}

/* CHIP — a label, not a control: no hover state and no pointer, because nothing happens
   when you click it. Two jobs, so the dot is opt-in rather than built in:
     .chip        taxonomy — the three tags under a case study title
     .chip.live   status   — "OPEN TO WORK", where the dot is the point
   A dot on every case-study tag would read as three bullet points in a row. */
.chip{font-family:var(--font-utility);font-weight:500;font-size:.72rem;letter-spacing:.13em;
  text-transform:uppercase;display:inline-flex;align-items:center;gap:10px;
  padding:0 20px;min-height:40px;border-radius:var(--r-pill);
  border:1.5px solid var(--teal-baby);color:var(--ink-blue)}
.chip.live::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--teal-baby);flex:none}
.chip.accent{border-color:var(--accent-baby);color:var(--accent)}
.chip.accent.live::before{background:var(--accent)}

/* LINK — the quietest rank. The rule is drawn with a border rather than text-decoration so
   it sits clear of the letterforms and can animate its colour independently. */
.btn-link{font-family:var(--font-utility);font-weight:500;font-size:.78rem;letter-spacing:.14em;
  text-transform:uppercase;text-decoration:none;color:var(--accent);cursor:pointer;
  display:inline-flex;align-items:center;gap:12px;padding-bottom:7px;
  border-bottom:1.5px solid var(--accent);background:none;
  transition:color var(--motion-fast) var(--ease-standard),
             border-color var(--motion-fast) var(--ease-standard),
             gap var(--motion-fast) var(--ease-standard)}
.btn-link:hover{color:var(--accent-deep);border-color:var(--accent-deep);gap:18px}
.btn-link.teal{color:var(--ink-blue);border-color:var(--teal-baby)}
.btn-link.teal:hover{color:var(--ink-blue);border-color:var(--ink-blue)}

/* DISABLED. Colour only — a disabled control must still be legible enough to read what it
   would have done, so this is the accent desaturated toward the paper rather than made
   transparent (which would let the blooms show through a control). */
.btn[disabled],.btn[aria-disabled="true"]{background:var(--accent-mute);border-color:transparent;
  color:var(--bg-raised);cursor:not-allowed;pointer-events:none}

/* FOCUS. One ring for every control on the site, teal so it never disappears against a
   terracotta fill, and offset so it reads as a ring around the button rather than as part
   of its outline. */
.btn:focus-visible,.btn-link:focus-visible{outline:2px solid var(--ink-blue);outline-offset:3px}

/* On the dark case-study bands the terracotta line is too close in value to the ground, so
   both quiet ranks switch to the cream inks that are already used for type down there. */
.on-deep .btn.outline,.btn.outline.on-deep{border-color:var(--text-on-deep-secondary);color:var(--text-on-deep)}
.on-deep .btn.outline:hover,.btn.outline.on-deep:hover{background:var(--text-on-deep);color:var(--bg-deep)}
.on-deep .chip,.chip.on-deep{border-color:var(--line-on-deep);color:var(--text-on-deep-secondary)}
.on-deep .chip::before,.chip.on-deep::before{background:var(--text-on-deep-secondary)}

/* ------------------------------------------------ nav */
.nav-wrap{position:fixed;top:14px;left:0;right:0;z-index:50;display:flex;justify-content:center;padding:0 16px}
nav.bar{display:flex;align-items:center;gap:6px;padding:6px 6px 6px 20px;border-radius:var(--r-pill);max-width:820px;width:100%;justify-content:space-between}
/* The SVG displacement warp lives on the nav and on the company chips, and nowhere else.
   Every element carrying backdrop-filter:url() costs a full backdrop snapshot + turbulence +
   displacement pass, and there were twelve of them once (nav, every button, every chip, the
   play button), which is why this is a list and not a property of .glass. The nav has always
   been on it. The four company chips joined it 2026-08-12, at her ask — they carried .glass
   already, but .glass without the warp is a frosted pill, and a frosted pill on flat cream
   has nothing to frost. What was missing was the one thing that reads as glass rather than as
   translucency: the edges of what is behind it moving as it passes.
   Six warped elements, all static filters. Do not promote this to .glass. The sixth is
   the day/night toggle (2026-08-19: "make it the same style as the nav bar at the top") —
   a second fixed pill, so it reads as the nav's counterweight in the opposite corner. */
nav.bar.glass,.w-chip.glass,.theme-toggle.glass{backdrop-filter:url(#glassWarp) blur(20px) saturate(1.5)}
.wordmark{font-family:var(--font-display);font-size:1.35rem;letter-spacing:.02em;text-decoration:none;color:var(--text-primary);margin-right:8px;white-space:nowrap}
.nav-links{display:flex;gap:2px;align-items:center}
.nav-links a,.theme-toggle{font-family:var(--font-utility);font-size:.74rem;letter-spacing:.05em;text-decoration:none;color:var(--text-secondary);
  padding:10px 13px;border-radius:var(--r-pill);transition:color var(--motion-fast) var(--ease-standard),background var(--motion-fast) var(--ease-standard)}
/* hover keeps the color shift only — the wash pill left with the handwriting swap
   (2026-08-19: "remove the hover highlight thing. just do the text") */
.nav-links a:hover,.nav-links a:focus-visible,.theme-toggle:hover,.theme-toggle:focus-visible{color:var(--accent-deep)}
/* ---- the day/night label (2026-08-19 her ask). The label names the mode you'll GET:
   in day it reads "night mode", click it and the site crosses over and the label flips.
   Both faces are CSS content keyed off [data-theme] so the right words are painted
   before any JS runs; the sans face is the ::before, and the ::after is the same
   handwriting swap every other control gets. */
.theme-toggle{position:fixed;left:18px;bottom:18px;z-index:80;cursor:pointer;white-space:nowrap;
  /* (2026-08-19 her ask) the toggle leaves the nav bar for the bottom-left corner — fixed,
     so day/night is reachable from anywhere without riding the bar, and the phone bar gets
     its width back. Bottom-LEFT because the right is spoken for: the pot stands above the
     footer on the right and say-hey caps the bar. It wears the nav's glass, not the sheet's
     flat fill (2026-08-19: "make it the same style as the nav bar at the top") — the ONE
     control allowed to, because like the nav it is a fixed pill floating over whatever
     scrolls beneath it, and a flat fill pinned over moving content read as a sticker. The
     material comes from .glass on the markup plus the warp list above; the prism rim, which
     .glass normally draws with ::after, lives on a real child (.tt-rim) here because ::after
     is spoken for below — it is the handwriting face. */
  padding:12px 18px}
/* (2026-08-25, second pass: "the line looks weird… I don't like how thick it is… it looks
   messy"). The rim still names the mode the button will GIVE you — rust ringing "Night
   Mode", night-blue ringing "Day Mode" — but the 2px full-strength band read as a frame
   fighting a grey label. Now it is a HAIRLINE at half strength and the LABEL takes the
   same ink as its ring, so line and word read as one control: matched, quiet, and still
   the only coloured pill in that corner. Both key off [data-theme] so everything flips
   together. Day ink is --accent (4.7:1 on cream); night ink is night's --ink-blue. */
.theme-toggle{color:var(--accent)}
.theme-toggle .tt-rim{background:rgba(176,69,22,.5);mix-blend-mode:normal;opacity:1;padding:1px}
[data-theme="night"] .theme-toggle{color:var(--ink-blue)}
[data-theme="night"] .theme-toggle .tt-rim{background:rgba(133,196,214,.55)}
.theme-toggle::before{content:"Night Mode"}
[data-theme="night"] .theme-toggle::before{content:"Day Mode"}
.theme-toggle::after{content:"Night Mode";
  /* this ::after also matches .glass::after (the prism rim), so the rim's paint has to be
     taken back off the handwriting BEFORE the positioning below — inset:auto first, or it
     would reset the left/right/top it is meant to protect. The rim is drawn by .tt-rim. */
  background:none;-webkit-mask:none;mask:none;padding:0;mix-blend-mode:normal;inset:auto;z-index:1;
  position:absolute;left:0;right:0;top:50%;transform:translateY(-.52em);
  text-align:center;font-family:var(--font-hand);font-size:1.05em;letter-spacing:0;color:var(--ink-blue);
  opacity:0;transition:opacity var(--motion-fast) var(--ease-standard);pointer-events:none;white-space:nowrap}
[data-theme="night"] .theme-toggle::after{content:"Day Mode"}
@media (hover:hover){
  .theme-toggle:hover,.theme-toggle:focus-visible{color:transparent}
  .theme-toggle:hover::after,.theme-toggle:focus-visible::after{opacity:1}
}
@media (max-width:700px){.nav-links a.optional{display:none}
  /* corner pill on phones too — it never fit the 375px bar; down here it clears the
     home-indicator strip instead */
  .theme-toggle{left:14px;bottom:max(14px,env(safe-area-inset-bottom, 14px))}}

/* ---- phone edge fades (2026-08-26, her iOS Messages screenshot): the nav and the
   day/night pill staying see-through is right — what was wrong was the content under
   them staying SHARP enough to read. iOS softens each end of the screen with a
   PROGRESSIVE blur: not one blur thinning out, but the blur radius itself dying toward
   the middle of the screen. The first pass here was the thinning kind — a single
   blur(12px) whose mask faded its opacity — and she caught it ("doesn't feel as
   gradual"): opacity-fading a fixed blur reads as a veil lifting, not softness
   receding. Now each strip is THREE stacked backdrop layers (the element and both
   pseudos): 18px hugging the screen edge, 8px reaching to ~60% of the strip, 3px
   running its full height, every mask and the tint an easedFade() — smoothstep, so no
   fade has a findable start or end. Where the layers overlap the radii compound, and
   the eye reads one continuous ramp from ~20px to nothing. The strips sit at z-index
   40 — under the nav (50) and the toggle (80), so the pills float on softened ground
   and stay the only crisp things in their corners. --edge-rgb is bg-base as a bare
   triplet (emitted per theme above), so night fades into its own deep green, never
   cream. pointer-events:none throughout — the pot under the bottom strip keeps its
   clicks. Phones only: on desktop the pills sit in open margins and the page never
   scrolls under them at reading size. Six static blurs total, none animated, so this
   does not repeat the feTurbulence-in-backdrop lag of 2026-08. */
.edge-fade{position:fixed;left:0;right:0;height:128px;z-index:40;pointer-events:none;display:none}
.edge-fade::before,.edge-fade::after{content:"";position:absolute;inset:0;pointer-events:none}
.edge-fade.top{top:0}
.edge-fade.bottom{bottom:0}
.edge-fade.top{background:linear-gradient(to bottom,rgba(var(--edge-rgb),0.5) 0%,rgba(var(--edge-rgb),0.486) 8.5%,rgba(var(--edge-rgb),0.448) 17%,rgba(var(--edge-rgb),0.392) 25.5%,rgba(var(--edge-rgb),0.324) 34%,rgba(var(--edge-rgb),0.25) 42.5%,rgba(var(--edge-rgb),0.176) 51%,rgba(var(--edge-rgb),0.108) 59.5%,rgba(var(--edge-rgb),0.052) 68%,rgba(var(--edge-rgb),0.014) 76.5%,rgba(var(--edge-rgb),0) 85%);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  -webkit-mask-image:linear-gradient(to bottom,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 10%,rgba(0,0,0,0.896) 20%,rgba(0,0,0,0.784) 30%,rgba(0,0,0,0.648) 40%,rgba(0,0,0,0.5) 50%,rgba(0,0,0,0.352) 60%,rgba(0,0,0,0.216) 70%,rgba(0,0,0,0.104) 80%,rgba(0,0,0,0.028) 90%,rgba(0,0,0,0) 100%);mask-image:linear-gradient(to bottom,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 10%,rgba(0,0,0,0.896) 20%,rgba(0,0,0,0.784) 30%,rgba(0,0,0,0.648) 40%,rgba(0,0,0,0.5) 50%,rgba(0,0,0,0.352) 60%,rgba(0,0,0,0.216) 70%,rgba(0,0,0,0.104) 80%,rgba(0,0,0,0.028) 90%,rgba(0,0,0,0) 100%)}
.edge-fade.top::before{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  -webkit-mask-image:linear-gradient(to bottom,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 6%,rgba(0,0,0,0.896) 12%,rgba(0,0,0,0.784) 18%,rgba(0,0,0,0.648) 24%,rgba(0,0,0,0.5) 30%,rgba(0,0,0,0.352) 36%,rgba(0,0,0,0.216) 42%,rgba(0,0,0,0.104) 48%,rgba(0,0,0,0.028) 54%,rgba(0,0,0,0) 60%);mask-image:linear-gradient(to bottom,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 6%,rgba(0,0,0,0.896) 12%,rgba(0,0,0,0.784) 18%,rgba(0,0,0,0.648) 24%,rgba(0,0,0,0.5) 30%,rgba(0,0,0,0.352) 36%,rgba(0,0,0,0.216) 42%,rgba(0,0,0,0.104) 48%,rgba(0,0,0,0.028) 54%,rgba(0,0,0,0) 60%)}
.edge-fade.top::after{-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  -webkit-mask-image:linear-gradient(to bottom,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 3.4%,rgba(0,0,0,0.896) 6.8%,rgba(0,0,0,0.784) 10.2%,rgba(0,0,0,0.648) 13.6%,rgba(0,0,0,0.5) 17%,rgba(0,0,0,0.352) 20.4%,rgba(0,0,0,0.216) 23.8%,rgba(0,0,0,0.104) 27.2%,rgba(0,0,0,0.028) 30.6%,rgba(0,0,0,0) 34%);mask-image:linear-gradient(to bottom,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 3.4%,rgba(0,0,0,0.896) 6.8%,rgba(0,0,0,0.784) 10.2%,rgba(0,0,0,0.648) 13.6%,rgba(0,0,0,0.5) 17%,rgba(0,0,0,0.352) 20.4%,rgba(0,0,0,0.216) 23.8%,rgba(0,0,0,0.104) 27.2%,rgba(0,0,0,0.028) 30.6%,rgba(0,0,0,0) 34%)}
.edge-fade.bottom{background:linear-gradient(to top,rgba(var(--edge-rgb),0.5) 0%,rgba(var(--edge-rgb),0.486) 8.5%,rgba(var(--edge-rgb),0.448) 17%,rgba(var(--edge-rgb),0.392) 25.5%,rgba(var(--edge-rgb),0.324) 34%,rgba(var(--edge-rgb),0.25) 42.5%,rgba(var(--edge-rgb),0.176) 51%,rgba(var(--edge-rgb),0.108) 59.5%,rgba(var(--edge-rgb),0.052) 68%,rgba(var(--edge-rgb),0.014) 76.5%,rgba(var(--edge-rgb),0) 85%);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  -webkit-mask-image:linear-gradient(to top,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 10%,rgba(0,0,0,0.896) 20%,rgba(0,0,0,0.784) 30%,rgba(0,0,0,0.648) 40%,rgba(0,0,0,0.5) 50%,rgba(0,0,0,0.352) 60%,rgba(0,0,0,0.216) 70%,rgba(0,0,0,0.104) 80%,rgba(0,0,0,0.028) 90%,rgba(0,0,0,0) 100%);mask-image:linear-gradient(to top,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 10%,rgba(0,0,0,0.896) 20%,rgba(0,0,0,0.784) 30%,rgba(0,0,0,0.648) 40%,rgba(0,0,0,0.5) 50%,rgba(0,0,0,0.352) 60%,rgba(0,0,0,0.216) 70%,rgba(0,0,0,0.104) 80%,rgba(0,0,0,0.028) 90%,rgba(0,0,0,0) 100%)}
.edge-fade.bottom::before{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  -webkit-mask-image:linear-gradient(to top,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 6%,rgba(0,0,0,0.896) 12%,rgba(0,0,0,0.784) 18%,rgba(0,0,0,0.648) 24%,rgba(0,0,0,0.5) 30%,rgba(0,0,0,0.352) 36%,rgba(0,0,0,0.216) 42%,rgba(0,0,0,0.104) 48%,rgba(0,0,0,0.028) 54%,rgba(0,0,0,0) 60%);mask-image:linear-gradient(to top,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 6%,rgba(0,0,0,0.896) 12%,rgba(0,0,0,0.784) 18%,rgba(0,0,0,0.648) 24%,rgba(0,0,0,0.5) 30%,rgba(0,0,0,0.352) 36%,rgba(0,0,0,0.216) 42%,rgba(0,0,0,0.104) 48%,rgba(0,0,0,0.028) 54%,rgba(0,0,0,0) 60%)}
.edge-fade.bottom::after{-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);
  -webkit-mask-image:linear-gradient(to top,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 3.4%,rgba(0,0,0,0.896) 6.8%,rgba(0,0,0,0.784) 10.2%,rgba(0,0,0,0.648) 13.6%,rgba(0,0,0,0.5) 17%,rgba(0,0,0,0.352) 20.4%,rgba(0,0,0,0.216) 23.8%,rgba(0,0,0,0.104) 27.2%,rgba(0,0,0,0.028) 30.6%,rgba(0,0,0,0) 34%);mask-image:linear-gradient(to top,rgba(0,0,0,1) 0%,rgba(0,0,0,0.972) 3.4%,rgba(0,0,0,0.896) 6.8%,rgba(0,0,0,0.784) 10.2%,rgba(0,0,0,0.648) 13.6%,rgba(0,0,0,0.5) 17%,rgba(0,0,0,0.352) 20.4%,rgba(0,0,0,0.216) 23.8%,rgba(0,0,0,0.104) 27.2%,rgba(0,0,0,0.028) 30.6%,rgba(0,0,0,0) 34%)}
@media (max-width:700px){.edge-fade{display:block}}

/* ---- the wordmark's greeting (2026-08-19). M and K load side by side and spread apart,
   the dashes revealing themselves between them; hovering squishes them back together and
   the dashes disappear. Transforms and opacity only, and the two letters move INWARD into
   the dash's space, so the a's box never changes size — the nav does not re-layout and
   the glass filter is not disturbed. --wm-dx is half the rendered width of "--" in the
   wordmark's own font (measured at 1.35rem Cormorant + .02em tracking); the letters
   meeting exactly is that measurement, so re-measure if the font or tracking changes. */
.wm-m,.wm-k,.wm-d{display:inline-block}
.wordmark{--wm-dx:.34em}
.wm-m,.wm-k{transition:transform .38s var(--ease-enter)}
.wm-d{transform-origin:50% 50%;transition:transform .38s var(--ease-enter),opacity .28s var(--ease-standard)}
.wordmark:hover .wm-m{transform:translateX(var(--wm-dx))}
.wordmark:hover .wm-k{transform:translateX(calc(-1*var(--wm-dx)))}
.wordmark:hover .wm-d{opacity:0;transform:scaleX(.1)}
@keyframes wm-in-m{from{transform:translateX(var(--wm-dx))}to{transform:none}}
@keyframes wm-in-k{from{transform:translateX(calc(-1*var(--wm-dx)))}to{transform:none}}
@keyframes wm-in-d{from{opacity:0;transform:scaleX(.1)}to{opacity:1;transform:none}}
/* .wm-load is stamped on the index's wordmark only: the greeting plays where the site
   is entered; clicking into a case study keeps M--K still, as if the bar persisted */
.wm-load .wm-m{animation:wm-in-m .8s var(--ease-enter) .5s backwards}
.wm-load .wm-k{animation:wm-in-k .8s var(--ease-enter) .5s backwards}
.wm-load .wm-d{animation:wm-in-d .8s var(--ease-enter) .5s backwards}

/* ---- nav labels turn into her hand on hover (same day). The handwritten copy is a
   ::after overlay, absolutely positioned so the pill never changes width — the sans
   label fades to transparent under it rather than leaving the layout. It exists at
   opacity 0 from first paint, which is also what keeps madhu-hand.woff (14KB) warm on
   every page — no flash of fallback on the first hover. The double content declaration
   is the alt-text syntax: modern browsers take the second form and keep the duplicate
   word out of the accessibility tree; older ones keep the first and stay functional.
   Gated to (hover:hover) so a phone tap never strands a link in its hover face. */
.nav-links a{position:relative}
.nav-links a[data-hand]::after{content:attr(data-hand);content:attr(data-hand) / "";
  /* -.52em drops the hand's BASELINE onto the sans label's baseline (2026-08-19: it sat
     visibly high). Madhu Hand carries a deep descender box (asc 9 / desc 8), so centring
     the line box floats the ink upward; the constant folds the two fonts' ascents and
     half-leadings together and scales with the pill's type size. */
  position:absolute;left:0;right:0;top:50%;transform:translateY(-.52em);text-align:center;
  font-family:var(--font-hand);font-size:1.05em;letter-spacing:0;color:var(--ink-blue);
  opacity:0;transition:opacity var(--motion-fast) var(--ease-standard);pointer-events:none;white-space:nowrap}
@media (hover:hover){
  .nav-links a[data-hand]:hover,.nav-links a[data-hand]:focus-visible{color:transparent}
  .nav-links a[data-hand]:hover::after,.nav-links a[data-hand]:focus-visible::after{opacity:1}
}

/* ------------------------------------------------ shared sections */
main{position:relative;z-index:1}
section{padding:calc(var(--space)*14) calc(var(--space)*6);max-width:1240px;margin:0 auto;position:relative}
.eyebrow{font-family:var(--font-utility);font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:calc(var(--space)*3)}
.section-title{font-family:var(--font-display);font-weight:400;font-size:clamp(1.8rem,3.4vw,2.7rem);line-height:1.2;max-width:26ch;margin-bottom:calc(var(--space)*7);color:var(--ink-blue)}
.fade{opacity:0;transform:translateY(24px);transition:opacity var(--motion-slow) var(--ease-enter),transform var(--motion-slow) var(--ease-enter)}
.fade.visible{opacity:1;transform:none}
.below-fold{}

/* ------------------------------------------------ hero */
/* Full viewport height so the flora's clip edge lands exactly on the fold. At any shorter
   height the bottom bloom gets sliced by a visible horizontal line partway up the screen,
   which reads as the edge of a box rather than as art running off the page. */
.hero{padding-top:calc(var(--space)*24);padding-bottom:calc(var(--space)*10);min-height:100vh;
  display:flex;flex-direction:column;justify-content:center;isolation:isolate}
/* The copy sits in a clear column between the two blooms — no longer over them.
   The overlap used to be defended as "on thin petal tips only", but at 1440 it put the rust
   eyebrow flat on a rust petal, which is the one pairing on this site with no contrast at
   all, and ran petals through the last handwritten line. So the rule is now simply: type
   never touches paint. The blooms hold the outer margins (see --flora-keep below) and the
   copy owns everything between them.

   The indent is DERIVED from the keep-out rather than being its own hand-tuned clamp. It
   used to be clamp(0px,13vw,190px), which happened to clear the blooms at 1440 and closed
   to a 6px gap around 1200, because the two numbers grew at different rates and nothing
   tied them together. Now the copy simply starts one gutter past wherever the left bloom is
   allowed to reach, and the arithmetic converts that viewport position into a margin on the
   section box — a section is capped at 1240px and centred, so its own left edge moves with
   the viewport too and has to be subtracted back out. max(0px,…) is the narrow case, where
   the section already starts outside the keep-out and no indent is wanted at all.

   --copy-max closes the same trap on the other side. Indenting the copy past the left bloom
   without also capping its width just pushes its right edge into the RIGHT bloom instead:
   at a fixed 640px the two would have collided at about 950px of viewport, which is exactly
   the class of width-dependent overlap this rule exists to abolish. So the cap is whatever
   is left of the viewport once both keep-outs, both gutters and the indent are taken out —
   the copy is never allowed to be wider than its own channel. */
.hero{--section-left:calc(max(0px,(100vw - 1240px)/2) + var(--space)*6);
  --flora-gutter:46px;
  --copy-indent:max(0px,calc(var(--flora-keep) + var(--flora-gutter) - var(--section-left)));
  --copy-max:calc(100vw - var(--flora-keep) - var(--flora-gutter) - var(--section-left) - var(--copy-indent))}
/* 640px was the width of the old copy block, chosen when it was sitting ON the blooms and
   had to stay narrow to keep off them. In a clear channel that same 640 leaves a dead strip
   between the last handwritten line and the right bloom, and unused space at this scale
   reads as a mistake rather than as air. The block now takes most of its channel and lets
   her own line lengths — which shorten 96% → 70% down the paragraph — open the space
   diagonally, away from the bloom rising into the bottom right corner. */
.hero>:not(.hero-flora){position:relative;margin-left:var(--copy-indent);
  max-width:min(100%,760px,var(--copy-max))}
.hero .eyebrow{margin-bottom:calc(var(--space)*4)}
/* display type is the reference blue, the way the wordmark is on her Orange & Mellow
   sheet — body copy stays the warm near-black so long paragraphs still read easily */
.hero h1{font-family:var(--font-display);font-weight:300;font-size:clamp(3.2rem,8vw,7rem);
  line-height:1;letter-spacing:-.005em;color:var(--ink-blue)}
/* Madhu's own handwriting — and now actually SET, not photographed.

   It used to be four PNG masks cut out of a sheet she wrote (tools/slice-hero-lines.js), one
   per written line, dropped back at the exact fractions her hand put them at. That was the
   only way to show her writing when her writing was a picture, and it had the cost you would
   expect: the copy could not change without her writing it out again, the line breaks were
   baked into the image so they could not respond to the viewport, and none of it was text.

   tools/make-hand-font.js turns the same sheets into a real typeface, so this is now a
   paragraph. It re-flows, it selects, it is read aloud correctly, it is indexed, and the copy
   lives in data/site.json where the rest of the words live.

   SET SMALLER AND WIDER than the old block, which she asked for and the type agrees with: at
   620px the old sentence ran only five or six words to the line. At 860px and this size it
   runs ten or eleven, which is a paragraph measure rather than a pull quote — and it stays
   under 75 characters, so the eye still finds the next line.

   THE SIZE WENT UP 15% when the ascenders were fixed, and it had to. Her x-height used to be
   60% of the em because the font was built with ascenders barely above it; giving b d h k l
   their real rise puts the x-height at 53%, which is an ordinary number for a typeface but
   means the same font-size renders visibly smaller. Same optical size, taller letters.

   line-height has to be generous and is: her descenders reach 64% of the em below the baseline
   and her f goes further still, so at anything tighter the tail of a g lands in the line
   underneath. 1.75 clears everything except the f, which is allowed to reach — it does on her
   page too. */
.hero-line{font-family:var(--font-hand);color:var(--ink-blue);
  width:min(100%,860px);margin-top:calc(var(--space)*5);
  font-size:clamp(1.18rem,1.78vw,1.52rem);line-height:1.75;
  letter-spacing:.005em;text-wrap:pretty}
/* Each sentence is its own line of thought, so it starts on its own line — but as a block
   the three still read as one paragraph, so the space between them is smaller than the space
   between the paragraph and anything else. */
.hero-line span.hs{display:block}
.hero-line span.hs + span.hs{margin-top:.5em}
/* sentence focus (2026-08-19, her ask): hovering one handwritten sentence quiets the
   others, so the eye can hold a single thought. Opacity only — nothing moves, the
   stickman's terrain is untouched (his rebuild listener allowlists load-sequence
   elements precisely so this hover never re-reads the world). Hover devices only:
   a phone tap should never dim two-thirds of the hero. */
.hero-line span.hs{transition:color 240ms var(--ease-standard)}
@media (hover:hover){
  /* 2026-08-19: colour, not opacity — the sentences step back into a light teal and
     the one under the cursor deepens slightly past resting ink. The step-back ink is
     its OWN token (2026-08-20 "make the light blue a tad lighter"): it used to borrow
     --teal-baby, but that hex belongs to the button system's 3:1-on-cream invariant,
     and a hover-dim state is allowed to sit lighter than a control outline is. */
  .hero-line:hover span.hs{color:var(--hero-hush)}
  .hero-line:hover span.hs:hover{color:var(--ink-blue-hover)}
}
.vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
.hero-cta-row{margin-top:calc(var(--space)*7);display:flex;gap:14px;flex-wrap:wrap}

/* florals. The composition is her reference's: one bloom hanging from the top and running
   off the LEFT edge, one rising from the bottom and running off the RIGHT, with the copy in
   the open diagonal channel between them.

   The container is 100vw, not the section box. A section is capped at 1240px, so a bloom
   pinned inside it stops ~100px short of the screen on a wide display and reads as a framed
   picture. The whole effect is the hard crop AT THE SCREEN EDGE, so the flora spans the
   viewport and clips there. (body already sets overflow-x:hidden, so 100vw is safe.) */
.hero-flora{position:absolute;top:0;bottom:0;left:50%;width:100vw;transform:translateX(-50%);
  z-index:-1;pointer-events:none;overflow:hidden}
/* height:auto is load-bearing — the width/height attributes on the tags reserve the right
   box before the PNGs land, but without it the CSS width fights the height attribute and
   each bloom stretches into a 1680px-tall smear. */
.hero-flora img{position:absolute;display:block;height:auto}
/* KEEP-OUT. The blooms used to be placed by nudging left/right offsets until it looked
   about right, and the result was that at 1440 they covered most of the frame and the copy
   was set on top of them. This is the same composition — one bloom hanging from the top
   left, one rising from the bottom right — but stated as a rule instead of a nudge:

     a bloom may reach --flora-keep in from its own edge of the viewport, and no further.

   Everything else follows from that. Each bloom's inner edge is pinned at --flora-keep, so
   its width can change with the viewport (or be clamped at the ends) without ever
   re-entering the copy: widening a bloom pushes it further off-screen rather than inward.
   That is what makes "type never touches paint" a property of the layout and not a
   coincidence that has to be re-checked at every width. The copy column starts outside it
   — see the margin-left on .hero's children — and the gap between the two is the channel.

   Roughly half of each bloom hangs off its edge, which is what keeps them reading as art
   running off the page rather than as two pictures placed in two corners. */
/* The WIDTHS are capped at what the artwork can actually pay for. bloom-left.png is 785px
   wide, so on a 2x screen it can fill 393 CSS px before the browser starts inventing
   detail — and inventing detail is precisely the softness she is objecting to. 27vw lands
   on 389px at 1440. There is no more resolution to be had: the source sheet is 486px wide
   and these are already upscaled 3x from it, so the display size is the last lever.
   Note this does not change how much bloom is on screen — that is --flora-keep, and it is
   independent. A narrower flower at the same keep-out simply hangs less far off the edge,
   so slightly more of the whole bloom is visible. */
:root{--flora-keep:16.5vw;--flora-a-w:clamp(280px,27vw,393px);--flora-b-w:clamp(280px,28vw,396px)}
/* min(0px,…) is the ultra-wide guard (2026-08-19, her "stem is cut off" report): past
   ~2380px of viewport the keep-out grows beyond the artwork's width, keep − width goes
   POSITIVE, and the bloom detaches from the screen edge — showing the PNG's own cut
   boundary (and, before it was erased from the asset, the stem ending mid-air). A bloom
   may reach LESS far than its keep-out; it must never float free of its edge. */
.flora-a{top:-11%;width:var(--flora-a-w);left:min(0px,calc(var(--flora-keep) - var(--flora-a-w)))}
.flora-b{bottom:-12%;width:var(--flora-b-w);right:min(0px,calc(var(--flora-keep) - var(--flora-b-w)))}
/* They ran at 82% because the copy was on top of them and two inks of near-identical
   luminance cannot share a pixel. Nothing overlaps now, so the paint is at full strength —
   which is also most of what she meant by "sharper": a bloom faded into the cream loses its
   darkest edge first, and the dark rust line around each petal IS the definition. */
.hero-flora img{opacity:1}
/* Below ~1180 the copy column and the two keep-outs stop fitting side by side on one line,
   so the blooms give up width rather than the type giving up its channel. */
@media (max-width:1180px){:root{--flora-keep:13vw;--flora-a-w:clamp(260px,26vw,360px);--flora-b-w:clamp(260px,27vw,380px)}}
/* on a phone the copy runs full width, so the blooms come almost all the way off the
   edge — a sliver of colour at each margin rather than a picture behind the text */
@media (max-width:760px){.hero-flora img{opacity:.5}
  /* Same keep-out rule, just a much smaller one — and it is the one place on the site where
     type does end up over paint, because a phone has no room for both a full-width column
     and a channel either side of it. Hence the 50%: at a sliver's width and half strength
     the blooms read as colour at the margin rather than as a picture behind the words. */
  :root{--flora-keep:7vw;--flora-a-w:230px;--flora-b-w:240px}
  .flora-a{top:-6%}.flora-b{bottom:-4%}
  /* the indent that keeps the copy clear of the blooms on a desktop is pure lost margin on a
     phone, where the blooms are slivers at the edges and the copy needs the full width */
  .hero>:not(.hero-flora){margin-left:0;max-width:none}}
/* the stick figure's canvas — the hero's HEIGHT but the VIEWPORT's width, the same trick
   as .hero-flora above. A section is capped at 1240px, so a canvas pinned to the hero box
   ended his world (100vw − 1240)/2 short of the screen on a wide display: told to run
   right, he stopped at an invisible wall in the middle of the page. His cage is now the
   same box the blooms clip at — the actual screen edge. (body sets overflow-x:hidden, so
   100vw is safe here too.) pointer-events:none means every real control under it keeps
   working; stick.js listens on the document and gates input to this band. The say-hey
   button stays a real anchor — the figure only holds a drawn post under it, so the
   control sheet is untouched.
   Written as .hero>.stick-cv, not bare .stick-cv: the ".hero>:not(.hero-flora)" copy-column
   rule is specificity 0,2,0 and would otherwise win, putting the canvas back IN FLOW —
   which grows the hero, which resizes the canvas, which grows the hero, forever. Same
   specificity + later in the sheet, plus explicit resets of the column's margin and cap.
   width/height are not decor: a canvas is a replaced element, so positioning alone leaves
   it at its intrinsic bitmap size (device pixels — double size on Retina) instead of
   stretching it to its box. */
.hero>.stick-cv{position:absolute;top:0;left:50%;width:100vw;height:100%;transform:translateX(-50%);z-index:3;pointer-events:none;margin-left:0;max-width:none;
  opacity:0;transition:opacity 600ms var(--ease-standard)}
.hero.stick-on>.stick-cv{opacity:1}
.hero.stick-dragging{user-select:none;-webkit-user-select:none}
/* ---- the cinematic load. The hero used to arrive in two registers at once: the
   handwriting wrote itself in word by word while the blooms, the headline and the two
   buttons just APPEARED at first paint — which is the harshness she named (2026-08-19,
   "fade in cinematically"). Now it assembles in layers off the same .revealed trigger
   the word reveal already uses, each layer fading up a few px as it lands:

     paint (1.6s fade from 150ms) → headline (rises, 450ms→1.4s) → the eyebrow's
     handwriting (900ms base, in step with the blooms' own resolve) → buttons
     (1.35s→2.15s) → the hero paragraph (data-base 1800: the architecture line only
     starts writing once the blooms have fully arrived — 2026-08-25 her ask) →
     the figure last (stick.js
     boots at 2.4s and fades his canvas in — he reads his world from the LIVE rects,
     so booting while the headline is mid-rise would bake his terrain 16px low; he
     also rebuilds on hero transitionend, which catches the words settling).

   Opacity and transform only, both composited — nothing here re-runs layout or
   touches the glass filters (the feTurbulence lesson). The flora fades as a
   CONTAINER so the per-image opacity rules (1 desktop, .5 phone) stay untouched. */
.hero-flora{opacity:0;transition:opacity 1600ms var(--ease-standard) 150ms}
.hero.revealed .hero-flora{opacity:1}
.hero h1{opacity:0;transform:translateY(16px);transition:opacity 950ms var(--ease-enter) 450ms,transform 950ms var(--ease-enter) 450ms}
.hero.revealed h1{opacity:1;transform:none}
.hero-cta-row{opacity:0;transform:translateY(14px);transition:opacity 800ms var(--ease-enter) 1350ms,transform 800ms var(--ease-enter) 1350ms}
.hero.revealed .hero-cta-row{opacity:1;transform:none}
/* words land clean — the blur-resolve tried on the whole hero came straight back out
   (2026-08-19, same day: "it hurts my eyes"). It survives on the EYEBROW ONLY below,
   because she asked to keep it for the word portfolio. */
.w{display:inline-block;white-space:pre;opacity:0;transform:translateY(.45em);transition:opacity var(--motion-slow) var(--ease-enter),transform var(--motion-slow) var(--ease-enter)}
/* iOS WebKit culls glyph ink it believes cannot exist: it takes a text run's ink bounds
   from the font's hhea descent (656 units), and her g's loop reaches 1098 — so on a phone
   the bottom of one g (whichever crosses a tile boundary; "guidance." on the iPhone 17
   wrap, 2026-08-19 "one of the g's on mobile is still getting cut off") was simply never
   painted. Chrome computes real outline bounds and never shows it. The fix is to make the
   overflow not be overflow: pad each word-box past the deepest tail (1.098em down, ±.35em
   sideways for the left-swinging loops and right flicks) and take the exact same amount
   back with negative margins. An inline-block's line contribution is its MARGIN box, and
   its baseline is its text baseline, so layout, wrapping and rhythm do not move by a
   pixel — only the box WebKit rasterises grows to hold all of her ink. */
.hero-line .w{padding:0 .35em .3em .35em;margin:0 -.35em -.3em -.35em}
.eyebrow .w{filter:blur(9px);transition:opacity 900ms var(--ease-enter),transform 900ms var(--ease-enter),filter 1400ms var(--ease-standard)}
.revealed .w{opacity:1;transform:none}
.revealed .eyebrow .w{filter:blur(0)}
/* the blooms arrive WITH the word Portfolio (2026-08-19 "can portfolio blur in with the
   flowers in the background") — the same blur-resolve the eyebrow kept, on the eyebrow's
   own 900ms cue with the same pair of clocks, so word and paint read as one layer
   resolving. Only the HIDDEN state is written, so each bloom lands on the stylesheet's
   own resting opacity — 1 on desktop, .5 on a phone. Gated to no-preference rather than
   patched in the reduced-motion block: under RM the blooms must simply BE there, at
   whatever opacity their width tier says, from the first frame. */
@media (prefers-reduced-motion:no-preference){
  .hero-flora img{transition:opacity 900ms var(--ease-enter) 900ms,filter 1400ms var(--ease-standard) 900ms}
  .hero:not(.revealed) .hero-flora img{opacity:0;filter:blur(14px)}
}
.lively .ch{display:inline-block;white-space:pre;transition:transform var(--motion-base) var(--ease-standard),color var(--motion-base) var(--ease-standard)}
.lively:hover .ch{color:var(--accent)}
.lively.waving .ch{transform:translateY(-7px)}
@media (max-width:760px){.hero{padding-top:calc(var(--space)*20);min-height:0}
  /* phone measure wraps her sentences onto more lines, so descender collisions that
     desktop never sees show up here — the g of "strategy" landing in "study"
     (2026-08-19 "the g in the hero text gets cut off"). More leading, not less type. */
  .hero-line{max-width:none;line-height:1.95}
  .hero-line span.hs + span.hs{margin-top:.7em}}

/* ------------------------------------------------ brands strip */
.brands{padding-top:0;padding-bottom:calc(var(--space)*6)}
.brands-head{border-top:1px solid var(--line);padding-top:calc(var(--space)*4);
  display:flex;align-items:baseline;justify-content:space-between;gap:calc(var(--space)*4);flex-wrap:wrap}
.brands-head p{font-family:var(--font-utility);font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--text-secondary)}
.brands-row{margin-top:calc(var(--space)*6);display:grid;
  grid-template-columns:repeat(auto-fit,minmax(80px,1fr));align-items:center;
  gap:calc(var(--space)*5) calc(var(--space)*3.5)}
/* logos are a mixed bag of cream, black and full-colour marks — masking them to one
   ink is the only way a row of nine reads as a set rather than a clip-art pile.
   The masks come from assets/logos/clean/, not the raw files: each original is a cream
   mark with an opaque dark outline + drop shadow baked in, and an alpha mask can't tell
   mark from shadow, so every glyph doubled and smeared. tools/clean-logos.js rebuilds
   the alpha from luminance to recover just the letterforms. */
.brand{height:38px;width:100%;background:var(--brand-blue);
  -webkit-mask:var(--logo) center/contain no-repeat;mask:var(--logo) center/contain no-repeat;
  transition:background var(--motion-base) var(--ease-standard)}
.brand:hover{background:var(--ink-blue)}

/* THE WALL INTRODUCES ITSELF. Nine marks that are simply present when you arrive at them
   read as wallpaper; the same nine arriving one after another read as a list of people who
   hired her, which is what they are.

   Each logo sits in a slot with a hard bottom edge and starts BELOW it, so what you see is
   not a logo fading in — it is a logo coming up from behind a line that is not drawn. The
   slot is exactly the height of the mark, so overflow:hidden IS the line: there is nothing to
   draw and nothing to keep in register.

   The cascade is 65ms a step, left to right, set from --i on the slot. That is slow enough to
   read as one-after-another and quick enough that the ninth is up within six tenths of a
   second — a longer step turns a credential list into a performance. The rise is a fraction
   over its own height so the mark clears the edge before it starts easing, and the fade
   finishes early, so it is the movement you notice rather than the opacity. */
.brand-slot{overflow:hidden;display:block;line-height:0}
/* display:block, and it is load-bearing rather than tidy: .brand used to be a grid item,
   which blockifies it, and putting it inside a slot made it an inline span again — where
   height:38px does not apply, the box collapses to nothing, and translateY(118%) of nothing
   is 0. The logos simply faded, with no movement at all. */
.brand-slot .brand{display:block;transform:translateY(118%);opacity:0;
  transition:background var(--motion-base) var(--ease-standard),
             transform 620ms var(--ease-enter) calc(var(--i) * 65ms),
             opacity 380ms var(--ease-standard) calc(var(--i) * 65ms)}
.brands-row.visible .brand{transform:none;opacity:1}
/* The row itself carries the observer but not the movement — the logos do that, and a row
   that also slid up would set every mark going before its own delay had run. */
.brands-row.fade{opacity:1;transform:none;transition:none}
.settled .brand,.rm-mode .brand{transform:none;opacity:1}
@media (prefers-reduced-motion:reduce){.brand{transform:none !important;opacity:1 !important;transition:background var(--motion-base) var(--ease-standard) !important}}
@media (max-width:600px){.brands-row{gap:calc(var(--space)*4)}.brand{height:28px}}

/* ------------------------------------------------ work cards (outcome statements) */
.wproject{display:grid;grid-template-columns:1fr minmax(0,46%);gap:calc(var(--space)*8);align-items:center;margin-bottom:calc(var(--space)*13);
  text-decoration:none;color:inherit;border-radius:var(--r-card);padding:calc(var(--space)*3);
  transition:background var(--motion-base) var(--ease-standard)}
/* hover wash is the terracotta wash token, not white — white is not in this palette and
   read as a cold sheet laid over the warm cream */
.wproject:hover{background:var(--accent-wash)}
.wproject:last-of-type{margin-bottom:0}
.w-outcome{font-family:var(--font-display);font-weight:400;font-size:clamp(1.45rem,2.7vw,2.15rem);line-height:1.28;max-width:24ch}
.w-chiprow{display:flex;align-items:center;gap:12px;margin-top:calc(var(--space)*2.5);flex-wrap:wrap}
.w-chip{font-family:var(--font-utility);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;
  padding:9px 16px;border-radius:var(--r-pill);color:var(--text-primary)}
/* The nav reads as glass because content scrolls under it; these chips sit in the flat
   cream gutter, where the shared .glass fill (white up to .50) has nothing to frost and
   just paints a small pill solid white. So the chips keep the material but drop the milk:
   a much clearer interior lets the cream and paint wash genuinely show through, and the
   edge does the talking — the same crescents, a slightly stronger prism rim, and a real
   lift shadow so it floats the way the nav does. Label contrast survives because the ink
   is #26332F on cream either way. */
.w-chip.glass{
  background:linear-gradient(150deg, rgba(255,255,255,.22) 0%, rgba(255,252,244,.12) 52%, rgba(255,255,255,.07) 100%);
  border-color:rgba(255,255,255,.72);
  box-shadow:
    inset 0 1.5px 1.5px -1px rgba(255,255,255,.98),
    inset 0 -2px 2px -1px rgba(255,255,255,.70),
    inset 1.5px 0 2px -1px rgba(255,255,255,.48),
    inset -1.5px 0 2px -1px rgba(255,255,255,.48),
    inset 0 0 10px rgba(255,255,255,.10),
    0 6px 16px var(--shadow-soft),
    0 1px 3px rgba(30,54,52,.10)}
.w-chip.glass::after{opacity:1}
.w-tags{font-family:var(--font-utility);font-size:.7rem;letter-spacing:.1em;text-transform:uppercase;color:var(--text-secondary)}
@media (max-width:760px){.wproject{grid-template-columns:1fr;gap:calc(var(--space)*4);margin-bottom:calc(var(--space)*9)}
  .wproject .project-media-wrap{order:-1}}
.project-media-wrap{display:flex;justify-content:center}

/* ---------- iPhone frame (see DEVICE_MOCKUP_SPEC.md) ----------
   Every dimension is a fraction of the screen width SW, so one frame scales to any
   size. Screen is 9:19.5 (0.4615); black glass bezel 0.0305·SW; titanium rail
   0.0213·SW; screen corner radius 0.1341·SW. Border T = 0.0518·SW, so the device is
   1.1036·SW wide and 2.2705·SW tall — that is the aspect-ratio below. Percentages
   here are of the DEVICE width, i.e. the spec fraction divided by 1.1036.
   Container query units make 1cqw = 1% of the device width, which is what lets the
   rail be shaded by distance inward (see .phone) rather than with a flat gradient. */
.phone-media{container-type:inline-size;width:100%;max-width:222px}
.phone{position:relative;aspect-ratio:1.1036/2.2705;
  --rail:1.930cqw;              /* 0.0213·SW */
  --border:4.694cqw;            /* rail + black bezel = T */
  border-radius:16.85% / 8.19%; /* (screen radius + T) as % of device w / h */
  background:#8E9195;
  /* distance-inward metallic ramp: each inset ring follows the rounded corners, so the
     specular highlight lands ON the visible rail and wraps the corners correctly.
     A left-to-right gradient would hide the highlight under the black bezel. */
  box-shadow:
    inset 0 0 0 calc(var(--rail)*.06) rgb(108,110,114),
    inset 0 0 0 calc(var(--rail)*.16) rgb(172,175,180),
    inset 0 0 0 calc(var(--rail)*.30) rgb(240,242,245),
    inset 0 0 0 calc(var(--rail)*.50) rgb(214,216,220),
    inset 0 0 0 calc(var(--rail)*.70) rgb(190,193,198),
    inset 0 0 0 calc(var(--rail)*.86) rgb(160,163,168),
    inset 0 0 0 var(--rail) rgb(112,114,119);
  filter:drop-shadow(0 1.1cqw 1.9cqw rgba(12,14,20,.28)) drop-shadow(0 2.8cqw 4.7cqw rgba(12,14,20,.20))}
/* key light from the upper left, so the frame is not symmetrical and synthetic.
   Only visible over the rail — the glass layer covers the middle. */
.phone::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:1;
  background:linear-gradient(152deg,rgba(255,255,255,.34),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 64%,rgba(0,0,0,.18))}
/* black glass, inset by the rail; radius = screen radius + bezel */
.phone::after{content:"";position:absolute;inset:var(--rail);z-index:2;pointer-events:none;
  border-radius:14.92% / 7.25%;background:#08080A;
  box-shadow:inset 0 0 0 1px rgba(60,61,64,.9)}
.phone-screen{position:absolute;inset:var(--border);z-index:3;overflow:hidden;background:#000;
  border-radius:12.15% / 5.91%}
.phone-screen img,.phone-screen video{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:top center}
/* Sources within ~5% of 9:19.5 are stretched rather than cropped, so nothing is lost
   off the bottom of the screen. build.js decides this per source — see screenFit(). */
.phone-screen img[data-fit=fill]{object-fit:fill}
/* The still screens are only ~300px wide at source, so on a Retina display they are
   being upscaled. A gentle unsharp mask restores edge definition on the upscale — it
   cannot invent detail, only re-crisp what is there. Videos are already sharp, so the
   filter is scoped to images. */
.phone-screen img{filter:url(#screenSharpen)}
/* Dynamic Island: 0.3186 × 0.0915 of SW, sitting 0.0274·SW below the screen top */
.phone-island{position:absolute;z-index:4;left:50%;transform:translateX(-50%);
  top:7.18cqw;width:28.87cqw;height:8.29cqw;border-radius:999px;background:#06060A}
.phone-island::after{content:"";position:absolute;right:19%;top:50%;transform:translateY(-50%);
  width:2.1cqw;height:2.1cqw;border-radius:50%;background:radial-gradient(circle at 36% 32%,#243042,#0A0C12 70%)}
/* side buttons. The inner end fades to the rail colour so they read as tucked under
   it rather than pasted on top (a child can't paint behind its parent's background). */
.phone-btn{position:absolute;z-index:0}
.phone-btn.l{left:-.97cqw;width:2.4cqw;border-radius:.6cqw 0 0 .6cqw;
  background:linear-gradient(90deg,#5C5F63,#9A9DA1 46%,rgba(150,153,157,0))}
.phone-btn.r{right:-.97cqw;width:2.4cqw;border-radius:0 .6cqw .6cqw 0;
  background:linear-gradient(270deg,#5C5F63,#9A9DA1 46%,rgba(150,153,157,0))}
.phone-btn.action{top:16%;height:4.2%}
.phone-btn.volup{top:23%;height:6.4%}
.phone-btn.voldn{top:31%;height:6.4%}
.phone-btn.power{top:24%;height:9%}
.phone-btn.camera{top:36%;height:4.6%}
@media (max-width:760px){.phone-media{max-width:178px}}

/* the landscape card and the Mac window must end up the SAME box. The window is
   1.945 of body plus 80 logical px of chrome at a 1280pt reference, i.e. total height
   = w*(1/1.945 + 80/1280) — a constant ratio of 1.7343, whatever the width. */
.project-media{position:relative;border-radius:var(--r-media);overflow:hidden;border:1px solid var(--line);background:var(--bg-raised);aspect-ratio:1.7343;width:100%}
.project-media video,.project-media img.poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
[data-video] video{opacity:0;transition:opacity var(--motion-base) var(--ease-standard)}
[data-video].playing video{opacity:1}

/* ---------- Chrome, drawn once (home-card window + the MacBook's screen) ----------
   Everything is sized in --u = one logical pt of the window the chrome pretends to
   be, so the same two rows serve the 1280pt standalone window and the maximized
   window on the MacBook's 1512pt display. Metrics and colors are Chrome's own,
   checked against her screenshot: 40pt tab strip #DEE1E6 with a white 34pt active
   tab, 12pt traffic lights 8pt apart, 40pt toolbar with the #F1F3F4 omnibox pill,
   ink #202124, icons #5F6368. Hard-coded px or invented tints are the two things
   that give a fake browser away. */
.chrome-tabs{display:flex;align-items:flex-end;height:calc(40*var(--u));background:#DEE1E6;
  padding-left:calc(14*var(--u));flex:0 0 auto;overflow:hidden}
.chrome-tabs .lights{display:flex;gap:calc(8*var(--u));align-self:center;margin-right:calc(14*var(--u));flex:0 0 auto}
.chrome-tabs .lights i{width:calc(12*var(--u));height:calc(12*var(--u));border-radius:50%;display:block}
.chrome-tabs .lights i:nth-child(1){background:#EC6A5E}
.chrome-tabs .lights i:nth-child(2){background:#F4BF4F}
.chrome-tabs .lights i:nth-child(3){background:#61C554}
.chrome-tabs .tab{display:flex;align-items:center;gap:calc(7*var(--u));background:#fff;
  border-radius:calc(9*var(--u)) calc(9*var(--u)) 0 0;height:calc(34*var(--u));
  width:calc(240*var(--u));padding:0 calc(10*var(--u));overflow:hidden;white-space:nowrap;
  font-family:var(--font-utility);font-size:max(6px,calc(12*var(--u)));letter-spacing:.01em;color:#202124}
.chrome-tabs .fav{width:calc(14*var(--u));height:calc(14*var(--u));border-radius:calc(3*var(--u));background:var(--accent);flex:0 0 auto}
.chrome-tabs .tab-t{flex:1;overflow:hidden;text-overflow:ellipsis}
.chrome-tabs .tab-x{color:#5F6368;flex:0 0 auto}
.chrome-tabs .tab-plus{align-self:center;margin-left:calc(10*var(--u));color:#5F6368;
  font-family:var(--font-utility);font-size:max(7px,calc(16*var(--u)));line-height:1}
.chrome-nav{display:flex;align-items:center;gap:calc(10*var(--u));height:calc(40*var(--u));
  background:#fff;padding:0 calc(14*var(--u));border-bottom:1px solid #DADCE0;flex:0 0 auto}
.chrome-nav .nav-ic{font-family:var(--font-utility);font-size:max(6px,calc(13*var(--u)));color:#5F6368;flex:0 0 auto}
.chrome-nav .nav-ic.is-off{opacity:.35}
.chrome-nav .omni{flex:1;display:flex;align-items:center;gap:calc(7*var(--u));height:calc(28*var(--u));
  border-radius:999px;background:#F1F3F4;padding:0 calc(12*var(--u));overflow:hidden;white-space:nowrap;
  font-family:var(--font-utility);font-size:max(6px,calc(12*var(--u)));color:#202124}
.chrome-nav .lock{width:calc(9*var(--u));height:calc(9*var(--u));border-radius:calc(2*var(--u));
  border:1px solid #5F6368;opacity:.7;flex:0 0 auto}
.chrome-nav .prof{width:calc(18*var(--u));height:calc(18*var(--u));border-radius:50%;
  background:linear-gradient(135deg,#7A9CC6,#4A6B96);flex:0 0 auto}

/* the standalone window: 1280pt wide, 10pt radius. Tab strip + toolbar stay 80u
   total — the 1.7343 SAME-box contract with the landscape cards depends on it. */
.browser{container-type:inline-size;width:100%;--u:.0781cqw;
  border-radius:calc(10*var(--u));overflow:hidden;background:#DEE1E6;
  box-shadow:0 calc(24*var(--u)) calc(54*var(--u)) var(--shadow-mid),
             0 calc(2*var(--u)) calc(6*var(--u)) var(--shadow-soft),
             inset 0 0 0 1px rgba(24,38,32,.10)}
/* matches the 2880x1480 screen captures, so nothing gets cropped */
.browser-body{position:relative;aspect-ratio:1.945;background:#fff;overflow:hidden}
.browser-body img.poster,.browser-body video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top center}

/* ---------- MacBook bezel (horizontal screens on case-study pages) ----------
   Drawn the way the phone is drawn: measured from her reference photo, not from
   memory. "mac bezel.png" is 917x576; with the base slab as the unit (896px wide)
   the pixels say: lid 759x515, inset 68px a side; bezel 11px on sides and bottom,
   25px on top with the camera 13px down; base slab 31px tall, thumb scoop 130x10px,
   bottom corners sweeping in 50px. Colors are sampled, not styled: #020202 glass
   behind a #3A3B3E edge line; base face #B1B3BA falling through #5E5F63 with a
   #97989D floor bounce; the scoop reads BRIGHTER than the face (#F2F4F9), its ends
   falling to #9EA0A5. Everything is in cqw so one frame serves every width. */
.mac-media{container-type:inline-size;width:100%}
.mac{position:relative;aspect-ratio:896/546;
  filter:drop-shadow(0 1.1cqw 2cqw rgba(12,14,20,.26)) drop-shadow(0 .3cqw .7cqw rgba(12,14,20,.16))}
.mac-lid{position:absolute;top:0;left:7.59cqw;right:7.59cqw;height:57.48cqw;
  background:#020202;border-radius:1.8cqw 1.8cqw 0 0;
  box-shadow:inset 0 0 0 1px #3A3B3E,inset 0 .16cqw .3cqw rgba(255,255,255,.16)}
/* key light from the upper left, same rule as the phone — a symmetric device
   reads synthetic. The screen div paints over this, so it only touches the bezel. */
.mac-lid::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(152deg,rgba(255,255,255,.07),rgba(255,255,255,0) 24%,rgba(0,0,0,0) 70%,rgba(0,0,0,.10))}
.mac-cam{position:absolute;top:1.05cqw;left:50%;transform:translateX(-50%);
  width:.6cqw;height:.6cqw;border-radius:50%;
  background:radial-gradient(circle at 36% 32%,#243048,#0A0C12 70%)}
/* The screen is a working Mac, not a void: menu bar, then Chrome maximized, then
   the page filling every remaining pixel — her rule from the reference screenshot,
   where no black glass shows around the site. The display plays a 1512pt 14",
   so inside the screen --u = screen-width/1512; the shared .chrome-* rows scale
   themselves. Menu bar is 24pt like the real one. */
.mac-screen{position:absolute;top:2.79cqw;left:8.82cqw;right:8.82cqw;bottom:4.69cqw;
  border-radius:.55cqw;background:#000;overflow:hidden;
  display:flex;flex-direction:column;--u:.05447cqw}
.mac-menubar{height:calc(24*var(--u));background:#F4F4F6;flex:0 0 auto;
  display:flex;align-items:center;gap:calc(11*var(--u));padding:0 calc(12*var(--u));
  font-family:var(--font-utility);font-size:max(4px,calc(10*var(--u)));color:#1D1D1F;
  white-space:nowrap;overflow:hidden}
.mac-menubar b{font-weight:700}
.mac-menubar .mb-apple{width:calc(11*var(--u));height:calc(11*var(--u));fill:#1D1D1F;flex:0 0 auto}
.mac-menubar .mb-right{margin-left:auto;display:flex;align-items:center;gap:calc(9*var(--u))}
.mac-menubar .mb-ic{width:calc(11*var(--u));height:calc(11*var(--u));fill:currentColor;flex:0 0 auto}
.mac-menubar .mb-batt{width:calc(17*var(--u))}
/* the page under the chrome lands at ~1.72; captures run 1.77-2.03, so cover from
   the top — a site cropping at the fold is what a real screen does anyway */
.mac-page{position:relative;flex:1;min-height:0;background:#fff;overflow:hidden}
.mac-page img.poster,.mac-page video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top center}
.mac-base{position:absolute;left:0;right:0;bottom:0;height:3.47cqw;
  border-radius:.3cqw .3cqw 5.6cqw 5.6cqw / .3cqw .3cqw 2.6cqw 2.6cqw;
  background:linear-gradient(#B1B3BA 0 40%,#A9ABB2 50%,#83848A 62%,#5E5F63 76%,#56575C 82%,#97989D 90%,#6E6F74 100%)}
/* the ends fall off darker — the right a touch more, the same key light again */
.mac-base::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(90deg,rgba(0,0,0,.14),rgba(0,0,0,0) 7%,rgba(0,0,0,0) 93%,rgba(0,0,0,.20))}
.mac-base::before{content:"";position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:14.5cqw;height:1.15cqw;border-radius:0 0 999px 999px;
  background:linear-gradient(90deg,#9EA0A5,#EDEFF4 14%,#F2F4F9 86%,#9EA0A5)}

/* ------------------------------------------------ disciplines */
.panels{display:flex;gap:calc(var(--space)*1.5);height:min(72vh,620px)}
.panel{position:relative;flex:1 1 0;min-width:0;border-radius:var(--r-media);overflow:hidden;cursor:pointer;border:1px solid var(--line);
  transition:flex-grow var(--motion-slow) var(--ease-enter);background:var(--bg-raised)}
.panel.active{flex-grow:8}
.panel img,.panel video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;filter:saturate(.94)}
.panel::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(18,35,32,0) 38%,rgba(18,35,32,.85) 100%);
  transition:opacity var(--motion-base) var(--ease-standard)}
.panel video{opacity:0;transition:opacity var(--motion-base) var(--ease-standard);z-index:1}
/* .v-live is stamped by JS the moment play() resolves: the clip shows whenever it is
   actually running — active or collapsed — and the poster img holds the frame anywhere
   playback can't start (Low Power Mode, reduced motion, data saver) */
.panel.has-video.v-live video{opacity:1}
.panel-collapsed-label{position:absolute;bottom:calc(var(--space)*3);left:50%;transform:translateX(-50%) rotate(180deg);writing-mode:vertical-rl;
  font-family:var(--font-utility);font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--text-on-deep);white-space:nowrap;z-index:2;opacity:.9;
  transition:opacity var(--motion-base) var(--ease-standard);text-shadow:0 1px 6px var(--shadow-strong)}
.panel.active .panel-collapsed-label{opacity:0;pointer-events:none}
.panel-content{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:calc(var(--space)*4);opacity:0;transform:translateY(12px);
  transition:opacity var(--motion-base) var(--ease-enter) 120ms,transform var(--motion-base) var(--ease-enter) 120ms;pointer-events:none}
.panel.active .panel-content{opacity:1;transform:none}
.p-org{font-family:var(--font-utility);font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;color:var(--accent-bright);margin-bottom:calc(var(--space)*1)}
.p-title{font-family:var(--font-display);font-weight:400;color:var(--text-on-deep);font-size:clamp(1.3rem,2.2vw,1.9rem);line-height:1.2;margin-bottom:calc(var(--space)*1);max-width:24ch}
.p-desc{color:var(--text-on-deep-secondary);font-size:.98rem;line-height:1.5;max-width:52ch}
@media (max-width:760px){.panels{flex-direction:column;height:auto}
  .panel{height:84px;flex:none;transition:height var(--motion-slow) var(--ease-enter)}
  .panel.active{height:360px;flex:none}
  /* the open panel's scrim starts much higher on a phone (2026-08-19: "the gradient
     doesn't go high enough so the text is a little hard to read") — 360px of panel
     holds three lines of content, and the desktop 38% start left them on raw photo */
  .panel.active::after{background:linear-gradient(180deg,rgba(18,35,32,0) 6%,rgba(18,35,32,.62) 45%,rgba(18,35,32,.92) 100%)}
  .panel-collapsed-label{writing-mode:horizontal-tb;transform:translateX(-50%);bottom:auto;top:50%;margin-top:-.5em}}
/* press-and-hold on touch peeks at the photo: text and scrim lift while held
   (2026-08-19 her ask). Hover devices don't need it — nothing covers the image
   until the panel is active, and a mouse can simply move away. */
@media (hover:none){
  .panel,.panel img{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
  .panel.peek::after,.panel.peek .panel-content,.panel.peek .panel-collapsed-label{opacity:0}
}

/* ------------------------------------------------ story */
.story{margin:0}
/* capped at its natural width so it is never upscaled — a soft GIF would undo the point */
.story img{width:100%;max-width:1000px;height:auto}
/* the stacked cut is a different shape, and <source> carries no dimensions — without this
   the phone reserves the wide aspect and the page jumps when the GIF lands */
@media (max-width:760px){.story img{aspect-ratio:660/1060;max-width:none}}
/* night: the GIF's background is the DAY cream, baked into its pixels so it can sit
   seamlessly on the day page. Rather than repaint her artwork, night frames it as the
   object it is — a notebook page, lit on a dark desk. */
[data-theme="night"] .story img{border-radius:var(--r-media);box-shadow:0 16px 44px var(--shadow-mid)}

/* ------------------------------------------------ about + footer */
.about-band{display:grid;grid-template-columns:180px 1fr;gap:calc(var(--space)*7);align-items:center}
.about-band .headshot{width:180px;height:180px;border-radius:50%;object-fit:cover;box-shadow:0 18px 40px var(--shadow-mid)}
/* display type is the reference blue, same rule as .hero h1 and .section-title — this is
   the architecture/brand sentence, and it is a headline, not body copy */
.about-title{font-family:var(--font-display);font-weight:400;font-size:clamp(1.5rem,2.9vw,2.3rem);line-height:1.25;margin-bottom:calc(var(--space)*2);max-width:30ch;color:var(--ink-blue)}
/* The about copy is a short essay, not a blurb, so it is set as paragraphs. The gap between
   them is one line of its own leading — enough to separate two thoughts, not so much that
   the three read as three separate blocks — and only the last one carries the full space
   down to the resume button. */
.about-body{color:var(--text-secondary);line-height:1.6;max-width:62ch;margin-bottom:1.6em}
.about-body:last-of-type{margin-bottom:calc(var(--space)*3)}
/* ---- the longer story (2026-08-25 her ask): the headline stands alone and the writing
   folds away. The reveal is the 0fr→1fr grid-row trick — the browser animates the row,
   the inner div clips, nothing is ever measured in JS. The paragraphs also drift in a
   few px and fade, so opening reads as the text arriving, not a box resizing. */
.about-more{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--motion-slow) var(--ease-standard)}
/* visibility rides the fade with a delay so screen readers and find-in-page skip the
   folded copy, but the close animation still shows the text on its way out */
.about-more>div{overflow:hidden;opacity:0;transform:translateY(-6px);visibility:hidden;
  transition:opacity var(--motion-slow) var(--ease-standard),transform var(--motion-slow) var(--ease-standard),
    visibility 0s var(--ease-standard) var(--motion-slow)}
.about-more.open{grid-template-rows:1fr}
.about-more.open>div{opacity:1;transform:none;visibility:visible;transition-delay:0s}
.rm-mode .about-more,.rm-mode .about-more>div{transition:none}
/* .btn-link was drawn for <a>; on a real <button> the UA's own border and padding come
   with it and read as a boxed control — strip everything but the drawn underline */
.about-toggle{margin-bottom:calc(var(--space)*4);position:relative;
  border:0;border-bottom:1.5px solid var(--teal-baby);padding:0 0 7px;background:none}
.about-toggle:hover{border-color:var(--ink-blue)}
/* the toggle takes the same hand-swap as every other control (her rule): the sans label
   goes quiet under her handwriting. The btn-link underline stays put, so on hover the
   handwritten words sit ON the drawn line — same baseline constant as the nav. */
.about-toggle[data-hand]::after{content:attr(data-hand);content:attr(data-hand) / "";
  position:absolute;left:0;right:0;top:50%;transform:translateY(-.52em);text-align:center;
  font-family:var(--font-hand);font-size:1.15em;letter-spacing:0;text-transform:none;font-weight:400;
  color:var(--ink-blue);opacity:0;transition:opacity var(--motion-fast) var(--ease-standard);
  pointer-events:none;white-space:nowrap}
@media (hover:hover){
  .about-toggle[data-hand]:hover,.about-toggle[data-hand]:focus-visible{color:transparent}
  .about-toggle[data-hand]:hover::after,.about-toggle[data-hand]:focus-visible::after{opacity:1}
}
/* ---- education (2026-08-25 her ask): the Block M and the Google G, official marks at
   full colour — credentials, so they sit small beside their words above a hairline,
   the same quiet register as the client wall. The marks are height-locked so the two
   sit optically equal whatever their native boxes are. */
.about-creds{display:flex;flex-wrap:wrap;gap:calc(var(--space)*3) calc(var(--space)*7);
  border-top:1px solid var(--line);padding-top:calc(var(--space)*4);
  margin:calc(var(--space)*1) 0 calc(var(--space)*4);max-width:62ch}
.about-cred{display:flex;align-items:center;gap:14px}
.about-cred img{height:30px;width:auto;flex:none}
.about-cred .cred-name{font-weight:500;color:var(--text-primary);font-size:.92rem;line-height:1.35}
.about-cred .cred-detail{color:var(--text-secondary);font-size:.83rem;line-height:1.4}
@media (max-width:700px){.about-band{grid-template-columns:1fr}.about-band .headshot{width:130px;height:130px}}

/* ---- the pot ---------------------------------------------------------------------------
   Madhu's ink drawing, standing on the rule that runs across the top of the footer, so it
   reads as an object resting on a surface rather than as an image placed near one.

   bottom:100% is what does that. An absolutely positioned child is laid out against its
   containing block's PADDING box, and the footer's 1px rule is drawn just outside that — so
   bottom:100% puts the base of the pot exactly on the underside of the line, and the line
   passes behind its foot. Tying it to the footer rather than to the section above also means
   it stays on the line if the about copy ever changes length.

   A MASK rather than an <img>: the drawing is ink on paper photographed on a phone, and the
   page is cream, so a plain image would land a grey rectangle above the footer.
   tools/lift-pot.js rebuilds the alpha from the photo's local contrast and the colour comes
   from the palette here — same treatment as her handwriting in the hero and the client logos.

   transform-origin is the bottom centre because that is exactly where the pot meets the
   line it is standing on; rotating about anything else reads as it pivoting in mid-air
   rather than tipping on its base. The rocking is integrated in JS — see makeJS. */
/* The shelf is the footer rule, and align-items:flex-end is what puts the pot's foot on it
   and keeps it there whatever the drawing's height does. --pot-base is the single size knob.
   It went up by half (116 -> 172px at the top of the ramp) when the dog was taken off the
   line: the old number was chosen so the pot would not be dwarfed by an animal 3.5x its
   height standing next to it, and with nothing beside it that size reads as a leftover
   rather than as an object. */
.footer-shelf{position:absolute;bottom:100%;right:clamp(8px,5vw,76px);
  --pot-base:clamp(112px,12vw,172px);
  display:flex;align-items:flex-end;pointer-events:none}
.footer-shelf>*{pointer-events:auto}

.pot{width:var(--pot-base);aspect-ratio:var(--pot-w)/var(--pot-h);
  /* no tap-highlight: iOS paints its dark rectangle over the pot's whole box on touch
     (2026-08-19 "i dont like that it has this dark background") — the rock is the feedback */
  -webkit-tap-highlight-color:transparent;
  background:var(--ink-blue);
  -webkit-mask:var(--pot) center bottom/contain no-repeat;
  mask:var(--pot) center bottom/contain no-repeat;
  transform-origin:50% 100%;will-change:transform;cursor:pointer}

/* THE DOG THAT USED TO STAND HERE IS GONE (her ask, 2026-08-12) — with it went .dog,
   .dog-l, .dog-head, the four stacked masks and the three-spring bark in makeJS. The pot is
   now the only thing on the shelf, which is why it has grown: at the old size it was scaled
   to sit beside an animal three and a half times its height, and alone on the line that
   reads as an ornament that got left behind. tools/lift-dog.js and assets/about/dog*.png
   are still on disk; nothing references them. */

@media (max-width:640px){.footer-shelf{right:clamp(6px,4vw,32px);--pot-base:clamp(92px,17vw,124px)}}
.resume-band{text-align:center}
.big-link{font-family:var(--font-display);font-size:clamp(2rem,5vw,3.6rem);color:var(--text-primary);text-decoration:none;transition:color var(--motion-fast) var(--ease-standard)}
.big-link:hover,.big-link:focus-visible{color:var(--accent)}
footer{border-top:1px solid var(--line);padding:calc(var(--space)*5) calc(var(--space)*6);display:flex;justify-content:space-between;align-items:center;gap:16px;max-width:1240px;margin:0 auto;position:relative;z-index:1}
footer a{color:var(--text-primary);text-decoration:none;font-size:.92rem;transition:color var(--motion-fast) var(--ease-standard)}
footer a:hover{color:var(--accent)}
.foot-links{display:flex;align-items:center;gap:calc(var(--space)*3);flex-wrap:wrap;justify-content:center}
.foot-mark{font-family:var(--font-display);font-size:1.3rem}
.foot-note{font-family:var(--font-utility);font-size:.7rem;letter-spacing:.08em;color:var(--text-secondary)}
@media (max-width:640px){footer{flex-direction:column;gap:10px;text-align:center;
  /* the fixed Night/Day pill lives in the bottom-left corner — without this the last
     footer line scrolls in underneath it */
  padding-bottom:calc(var(--space)*11)}
  section{padding-left:calc(var(--space)*3);padding-right:calc(var(--space)*3);padding-top:calc(var(--space)*10);padding-bottom:calc(var(--space)*10)}}

/* ------------------------------------------------ painted panels
   Every decorative panel gradient on the case-study pages is gone: she asked for paint
   texture rather than gradients. Each surface is now a flat palette token with her own
   brush swatch laid over it, so a big block of colour still has grain and brush direction
   without being shaded from one corner to another.
   The ::before sits at z-index:-1 inside the panel's own isolated stacking context, which
   paints it above the panel's background but below its content — so text stays crisp.
   ONE gradient survives on purpose: the scrim on .panel::after. That one is not decoration,
   it is what keeps cream type legible over an arbitrary photograph. */
.cs-hero-band,.cs-next-card,.cs-media-inner.deep,
.cs-stats-band,.cs-media-inner.wash,.cmp-col.after{position:relative;isolation:isolate}
.cs-hero-band::before,.cs-next-card::before,.cs-media-inner.deep::before,
.cs-stats-band::before,.cs-media-inner.wash::before,.cmp-col.after::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;border-radius:inherit;
  background:var(--paint-ink);opacity:.5;
  -webkit-mask:url(assets/hero/paint-wash.png?v=mtdfum5g) repeat center/504px 2032px;
  mask:url(assets/hero/paint-wash.png?v=mtdfum5g) repeat center/504px 2032px}
/* the brush ink is one step off the panel's own fill, so the texture reads as brushwork in
   the same colour rather than as a stain in a different one */
.cs-hero-band,.cs-next-card,.cs-media-inner.deep{--paint-ink:var(--bg-deep-raised)}
.cs-hero-band.light,.cs-stats-band,.cs-media-inner.wash,.cmp-col.after{--paint-ink:var(--bg-raised)}

/* ================================================ case study pages */
.cs-hero{position:relative;z-index:1;padding:96px 16px 0}
.cs-hero-band{max-width:1240px;margin:0 auto;border-radius:24px;overflow:hidden;position:relative;
  background:var(--bg-deep);color:var(--text-on-deep);padding:clamp(28px,4.5vw,56px)}
.cs-hero-band.light{background:var(--accent-wash);color:var(--text-primary)}

/* ---- the design-system door (2026-08-26 "I just want it to be an icon"): one round
   outlined control in the hero band's top-right corner, sitting on the band's own
   padding grid. No label at rest — the brush glyph is the whole button. The words live
   in the CURSOR (2026-08-28 her ask): hovering rolls the dot out into the same pill
   the case cards use for "I'm curious", reading "View design system" — data-curious
   carries the label, the cursor code does the rest. Hairline border, matching the
   theme-toggle ring, is what "delicate" means on this site. */
.ds-link{position:absolute;top:clamp(28px,4.5vw,56px);right:clamp(28px,4.5vw,56px);z-index:3;
  width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  border:1px solid currentColor;color:var(--text-on-deep);
  transition:background-color var(--motion-fast) var(--ease-standard),
             border-color var(--motion-fast) var(--ease-standard),
             color var(--motion-fast) var(--ease-standard)}
.cs-hero-band.light .ds-link{color:var(--accent)}
.ds-link:hover,.ds-link:focus-visible{background:var(--accent-press);border-color:var(--accent-press);
  color:var(--text-on-accent)}
.ds-link svg{width:15px;height:15px;display:block}
@media (max-width:600px){
  .ds-link{width:32px;height:32px}
  .ds-link svg{width:13px;height:13px}
}
/* below 900px the title's first line can run under the circle (Stack's "Budgeting that"
   did) — clear its lane, but only when the band actually carries the button. Desktop
   widths are left alone so the twoLine() fitting keeps measuring the real column. */
@media (max-width:900px){
  .cs-hero-band:has(.ds-link) .cs-kicker,
  .cs-hero-band:has(.ds-link) .cs-title{padding-right:44px}
}

.cs-kicker{font-family:var(--font-utility);font-size:.75rem;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-bright);margin-bottom:14px}
.cs-hero-band.light .cs-kicker{color:var(--accent-deep)}
.cs-title{font-family:var(--font-display);font-weight:400;font-size:clamp(2rem,4.6vw,3.6rem);line-height:1.12;max-width:22ch;text-wrap:balance;margin-bottom:12px}
/* A HEADLINE THAT STATES THE BUSINESS PROBLEM IS LONGER THAN ONE THAT ASKS A QUESTION.
   "What if your playlist knew how to DJ?" fits a 22ch measure at 3.6rem in two lines; the
   Atlantic headline is a full sentence with the number in it and would run to six. Rather
   than cut her copy, a long one steps down a size and out to a wider measure — the same
   move a magazine makes, and it still reads as the loudest thing on the band. The cut is at
   72 characters because that is where 22ch × 3.6rem stops fitting three lines. */
.cs-title.long{font-size:clamp(1.65rem,3.1vw,2.5rem);line-height:1.2;max-width:32ch}
.cs-tagline{font-size:clamp(1rem,1.6vw,1.2rem);line-height:1.5;max-width:58ch;text-wrap:balance;color:var(--text-on-deep-secondary)}
.cs-hero-band.light .cs-tagline{color:var(--text-secondary)}
.cs-hero-media{margin-top:clamp(24px,4vw,44px);border-radius:var(--r-media);overflow:hidden;box-shadow:0 30px 80px var(--shadow-strong)}
.cs-hero-media img,.cs-hero-media video{width:100%;height:auto}
.cs-hero-phones{margin-top:clamp(24px,4vw,44px);display:flex;justify-content:center;gap:clamp(16px,3vw,42px);align-items:flex-start}
.cs-hero-phones>img{width:min(38%,300px);height:auto;filter:drop-shadow(0 24px 44px var(--shadow-mid))}
.cs-hero-phones .phone-media{width:min(38%,218px);max-width:218px}
.cs-hero-phone-video{margin-top:clamp(24px,4vw,44px);display:flex;justify-content:center}
.cs-hero-phone-video .phone-media{max-width:225px}
.cs-intro{max-width:900px;margin:0 auto;padding:calc(var(--space)*9) calc(var(--space)*6) 0}
.cs-intro p{font-size:clamp(1.05rem,1.7vw,1.25rem);line-height:1.65;margin-bottom:calc(var(--space)*3);max-width:62ch}
.cs-meta{max-width:900px;margin:0 auto;padding:calc(var(--space)*5) calc(var(--space)*6);display:flex;flex-wrap:wrap;gap:calc(var(--space)*6) calc(var(--space)*8)}
.cs-meta div{min-width:120px}
.cs-meta dt{font-family:var(--font-utility);font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:var(--text-secondary);margin-bottom:6px}
.cs-meta dd{font-size:.95rem}
.cs-meta a{color:var(--accent-deep)}
/* The tags under a case study title are the button sheet's chip — same shape, same rank, so
   they now carry .chip and this rule only lays them out. They used to be glass pills, which
   put a frosted panel treatment on a piece of taxonomy and made them read as controls. */
.cs-tags{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}

.cs-stats{max-width:1240px;margin:calc(var(--space)*6) auto 0;padding:0 calc(var(--space)*2)}
.cs-stats-band{border-radius:24px;background:var(--accent-wash);padding:clamp(20px,3.5vw,40px);display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}
.stat-card{border-radius:var(--r-card);padding:22px 22px 20px}
.stat-value{font-family:var(--font-display);font-size:clamp(1.9rem,3.4vw,2.8rem);color:var(--accent-deep);line-height:1.05;margin-bottom:8px}
.stat-label{font-size:.88rem;line-height:1.45;color:var(--text-secondary)}

.cs-row{max-width:1100px;margin:0 auto;padding:calc(var(--space)*10) calc(var(--space)*6) 0;display:grid;grid-template-columns:220px 1fr;gap:calc(var(--space)*6)}
.cs-label{font-family:var(--font-utility);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);position:sticky;top:96px;align-self:start;padding-top:6px}
/* the case-study equivalent of .section-title, so it takes the same reference blue.
   .cs-title is deliberately NOT blue — it sits on the deep teal band, where the blue goes
   muddy; on that band the light cream is the display colour. */
/* a section heading breaks as AT MOST two lines: an inline per-heading
   max-width (see twoLine() in the case-study template — titles, taglines and
   captions get the same treatment) forces the wrap on long strings, and balance
   splits the pair evenly instead of leaving a one-word second line. A heading
   short enough to fit this 34ch measure on one line gets no inline clamp and
   stays a single line — never stretch a short heading across two. */
.cs-heading{font-family:var(--font-display);font-weight:400;font-size:clamp(1.5rem,2.8vw,2.2rem);line-height:1.2;margin-bottom:calc(var(--space)*3);max-width:34ch;text-wrap:balance;color:var(--ink-blue)}
.cs-body p{color:var(--text-secondary);line-height:1.65;font-size:1.02rem;margin-bottom:calc(var(--space)*2.5);max-width:62ch}
.cs-quote{border-left:3px solid var(--accent);padding:6px 0 6px 22px;margin:calc(var(--space)*3) 0}
.cs-quote p{font-family:var(--font-display);font-size:clamp(1.2rem,2vw,1.6rem);line-height:1.35;color:var(--text-primary);margin-bottom:8px}
.cs-quote cite{font-family:var(--font-utility);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--text-secondary);font-style:normal}
@media (max-width:820px){.cs-row{grid-template-columns:1fr;gap:calc(var(--space)*2)}.cs-label{position:static}}

/* width:100% because the auto side-margins switch off flex-stretch inside .cs-section,
   and a band that shrink-wraps is sized by its CAPTION — so browser-framed videos
   (absolutely-positioned contents, no intrinsic width) rendered 755/728/576px wide in
   one scroll (2026-08-28 "jumping around in size"). Full width pins every band to the
   same 1240px rail the hero band sits on. */
.cs-media-band{width:100%;max-width:1240px;margin:calc(var(--space)*7) auto 0;padding:0 calc(var(--space)*2)}
.cs-media-inner{border-radius:24px;padding:clamp(20px,4vw,56px);display:flex;flex-direction:column;align-items:center;gap:18px}
.cs-media-inner.wash{background:var(--accent-wash)}
.cs-media-inner.deep{background:var(--bg-deep)}
.cs-media-frame{border-radius:var(--r-media);overflow:hidden;width:100%;box-shadow:0 24px 60px var(--shadow-mid);position:relative}
.cs-media-frame img,.cs-media-frame video{width:100%;height:auto}
.cs-phones{display:flex;justify-content:center;gap:clamp(14px,3vw,40px);width:100%;align-items:flex-start}
.cs-phones>img{width:min(42%,300px);height:auto;filter:drop-shadow(0 22px 40px var(--shadow-mid))}
.cs-phones .phone-media{width:min(42%,218px);max-width:218px}
.cs-phone-col{display:flex;flex-direction:column;align-items:center;gap:12px;width:min(42%,218px)}
.cs-phone-col .phone-media{width:100%;max-width:218px}
.cs-phone-label{font-family:var(--font-utility);font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--text-caption)}
.cs-media-inner.deep .cs-phone-label{color:var(--text-on-deep-secondary)}
/* the empty slot: a phone-shaped dashed outline (same ratio and corner radius as
   .phone) holding a sentence instead of a screen */
.cs-phone-none{width:100%;max-width:218px;aspect-ratio:1.1036/2.2705;box-sizing:border-box;
  border:1.5px dashed var(--line);border-radius:16.85%/8.19%;
  display:flex;align-items:center;justify-content:center;padding:14%}
.cs-phone-none p{font-family:var(--font-utility);font-size:.85rem;line-height:1.55;
  color:var(--text-secondary);text-align:center;text-wrap:balance;margin:0}
.cs-media-inner.deep .cs-phone-none{border-color:var(--line-on-deep)}
.cs-media-inner.deep .cs-phone-none p{color:var(--text-on-deep-secondary)}
/* 1rem = 12pt — the floor for caption legibility. --text-caption exists because
   --text-secondary is only 3.9:1 on the accent-wash band these captions sit on;
   the darker step clears WCAG AA (4.5:1) on both wash and raised fills. */
.cs-caption{font-family:var(--font-utility);font-size:1rem;letter-spacing:.03em;line-height:1.6;color:var(--text-caption);max-width:58ch;text-wrap:balance;text-align:center}
.cs-media-inner.deep .cs-caption{color:var(--text-on-deep-secondary)}

/* before / after compare */
.cs-compare{max-width:1100px;margin:calc(var(--space)*6) auto 0;padding:0 calc(var(--space)*6);display:grid;grid-template-columns:1fr 1fr;gap:16px}
.cmp-col{border-radius:var(--r-card);padding:clamp(18px,2.6vw,30px)}
.cmp-col.before{background:var(--bg-raised);border:1px solid var(--line)}
.cmp-col.after{background:var(--accent-wash);border:1px solid var(--line-accent)}
.cmp-head{font-family:var(--font-utility);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;margin-bottom:18px;color:var(--text-secondary)}
.cmp-col.after .cmp-head{color:var(--accent-deep)}
.cmp-item{margin-bottom:20px}
.cmp-item:last-child{margin-bottom:0}
.cmp-item h4{font-size:.98rem;margin-bottom:6px}
.cmp-col.before .cmp-item h4{color:var(--text-secondary)}
.cmp-item p{font-size:.9rem;line-height:1.55;color:var(--text-secondary)}
.cmp-note{margin-top:6px;font-size:.8rem;color:var(--accent-deep);font-family:var(--font-utility)}
@media (max-width:760px){.cs-compare{grid-template-columns:1fr}}

/* ---- one section at a time (2026-08-25, from the Framer reference she sent) ------------
   Each .cs-section is sized to own the screen — min-height centres its content on the
   viewport — and app.js drives a scroll-LINKED crossfade over it: a section materialises
   as it approaches the middle of the screen and dissolves as it leaves, so the reader
   always holds exactly one thought. Inside these sections the old one-shot .fade
   entrances are retired (the section IS the fade — two fades stacked read as flicker).
   .cs-scroll is stamped by app.js only when the effect actually runs, so reduced motion
   and no-JS get plain stacked sections with the classic entrances. */
.cs-section{min-height:86svh;display:flex;flex-direction:column;justify-content:center;
  padding:calc(var(--space)*4) 0}
.cs-section .cs-row{padding-top:0}
.cs-scroll .cs-section{will-change:opacity}
.cs-scroll .cs-section .fade{opacity:1;transform:none;transition:none}
/* media keeps its NATURAL size (2026-08-25: the svh caps that squeezed a section onto
   one screen made the phone screens "way too small" — reverted). A tall section simply
   scrolls; the crossfade, not shrinking, is what delivers one-section-at-a-time. */
/* phones: content runs taller than the screen, so pinning each section to a viewport
   would leave dead cream between them — the crossfade still runs, sizing goes back to
   natural flow */
@media (max-width:820px){.cs-section{min-height:0;padding:calc(var(--space)*8) 0 calc(var(--space)*2)}}

/* The next-project tease, her spec: the pop-up shows up IN the box. On hover the
   card grows 128px upward (negative margin + equal extra bottom padding, so the
   page below never moves), the text — anchored to the card's top — rides up with
   the top edge, and the next case's cover device rises inside the card from its
   bottom edge, clipped by the card's own rounded corners. The 144px of margin
   above the card is the room the expansion grows into. */
.cs-next{max-width:1240px;margin:calc(var(--space)*18) auto calc(var(--space)*4);padding:0 calc(var(--space)*2)}
.cs-next-card{position:relative;overflow:hidden;display:flex;justify-content:space-between;align-items:flex-start;gap:20px;border-radius:24px;padding:clamp(24px,4vw,48px);text-decoration:none;
  background:var(--bg-deep);color:var(--text-on-deep);
  transition:margin var(--motion-base) var(--ease-standard),padding var(--motion-base) var(--ease-standard)}
.cs-next-copy{position:relative;z-index:1}
.cs-next-card .k{font-family:var(--font-utility);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-bright);margin-bottom:10px}
.cs-next-card .t{font-family:var(--font-display);font-size:clamp(1.5rem,3.2vw,2.6rem);line-height:1.15}
.cs-next-peek{position:absolute;z-index:0;top:100%;right:clamp(28px,6vw,84px);pointer-events:none;
  transition:transform var(--motion-base) var(--ease-standard)}
.cs-next-peek.is-phone{width:190px}
.cs-next-peek.is-mac{width:470px}
@media (hover:hover) and (min-width:900px) and (prefers-reduced-motion:no-preference){
  .cs-next-card:hover,.cs-next-card:focus-visible{margin-top:-128px;padding-bottom:calc(clamp(24px,4vw,48px) + 128px)}
  /* a fixed 190px slice rises whatever the device: with top:100% the visible
     height IS the translate, so phone and MacBook tease equally */
  .cs-next-card:hover .cs-next-peek,.cs-next-card:focus-visible .cs-next-peek{transform:translateY(-190px)}
}

/* A play button is .play-btn + .btn.icon.solid — the shape, size and colour all come from
   the button system, and this rule only says where it sits and when it appears. It carries
   .solid rather than the sheet's teal outline because it is the one control that does not
   sit on the page cream: it floats over a screenshot or a video frame, and a transparent
   pill with a 1.5px line disappears against arbitrary artwork. A filled terracotta disc is
   the same system saying the same thing on an unknown background. */
.play-btn{position:absolute;bottom:16px;right:16px;z-index:6;display:none}
/* min-width/height too, or .btn's own min-height wins and the disc turns into a stadium */
.phone-media .play-btn.icon{bottom:9cqw;right:50%;transform:translateX(50%);
  width:36px;min-width:36px;height:36px;min-height:36px;font-size:.8rem}
.rm-mode .play-btn{display:inline-flex}
/* the button system brings its own teal focus ring (see .btn:focus-visible); this is the
   catch-all for every other link and control on the site */
a:focus-visible,button:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px}

/* settle fail-safe: nothing ever stays invisible, even if transitions never run */
.settled .w{opacity:1;transform:none;filter:none}
.settled .fade{opacity:1;transform:none}
/* flora and canvas keep their translateX(-50%) centering — force OPACITY only there;
   transform:none on either shoves it half a viewport right (found the hard way) */
.settled .hero h1,.settled .hero-cta-row{opacity:1;transform:none}
.settled .hero-flora{opacity:1}

/* reduced motion */
.rm-mode .w,.rm-mode .fade{opacity:1;transform:none;filter:none;transition:none}
.rm-mode .hero h1,.rm-mode .hero-cta-row{opacity:1;transform:none;transition:none}
.rm-mode .hero-flora,.rm-mode .stick-cv{opacity:1;transition:none}
.rm-mode .wm-m,.rm-mode .wm-k,.rm-mode .wm-d{animation:none;transition:none}
.rm-mode .wordmark:hover .wm-m,.rm-mode .wordmark:hover .wm-k{transform:none}
.rm-mode .wordmark:hover .wm-d{opacity:1;transform:none}
.rm-mode .nav-links a::after,.rm-mode .btn::after,.rm-mode .theme-toggle::after{transition:none}
.rm-mode .hero-line span.hs,.rm-mode .hero-line:hover span.hs{color:var(--ink-blue);transition:none}
@media (prefers-reduced-motion:reduce){
  .w,.fade{opacity:1 !important;transform:none !important;filter:none !important;transition:none !important}
  .hero h1,.hero-cta-row{opacity:1 !important;transform:none !important;transition:none !important}
  .hero-flora,.stick-cv{opacity:1 !important;transition:none !important}
  .wm-m,.wm-k,.wm-d{animation:none !important;transition:none !important;transform:none !important;opacity:1 !important}
  .hero-line span.hs{transition:none !important}
  .play-btn{display:flex}
  html{scroll-behavior:auto}
}
