/* =========================================================================
   Squibb Media — squibbmedia.com
   Design system extracted from "Squibb Media Branding.sketch" › Web 2026
   Mobile first (375), fluid up through tablet (768) and desktop (1080+).
   ========================================================================= */

/* ---------- Fallback face -------------------------------------------------
   Avenir Next ships with macOS/iOS, so on Apple hardware nothing below is ever
   fetched — the browser only requests a @font-face file when the families
   ahead of it in the stack are all missing. Self-hosted rather than pulled from
   Google so there is no third-party round trip and our own cache headers apply. */
@font-face {
  font-family: "Nunito Sans";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("../assets/fonts/nunito-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Nunito Sans";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("../assets/fonts/nunito-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------- Cross-document view transitions -------------------------------
   The masthead, mini-bar and footer are the same objects on every page, so they
   are named and therefore carried across the navigation rather than torn down
   and repainted. That is what stops the mark and the segmented control from
   blinking between pages. Ignored by browsers that do not support it. */
@view-transition { navigation: auto; }

/* ---------- Theme eclipse --------------------------------------------------
   Light/dark toggles from the a11y sheet animate via document.startViewTransition
   (see switchTheme in js/site.js). During that one transition, the persistent
   per-element names are suppressed so the whole page is captured as a single
   root snapshot, and the new appearance is clipped in over the old along a
   top-left -> bottom-right diagonal — an eclipse terminator sweeping across.
   Scoped to .theme-eclipse, so ordinary navigation transitions are untouched. */
.theme-eclipse .masthead__lockup,
.theme-eclipse .masthead .seg,
.theme-eclipse .minibar__logo,
.theme-eclipse .minibar .seg,
.theme-eclipse .sitefoot__lockup,
.theme-eclipse .masthead .seg__thumb,
.theme-eclipse .minibar .seg__thumb,
.theme-eclipse .fabs--masthead .fab--a11y,
.theme-eclipse .fabs--masthead a.fab,
.theme-eclipse .minibar a.fab { view-transition-name: none; }

@property --eclipse {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}
html.theme-eclipse::view-transition-old(root) { animation: none; }   /* old stays put beneath */
html.theme-eclipse::view-transition-new(root) {
  /* Soft diagonal terminator sweeping top-left -> bottom-right. The band is a
     wide gaussian falloff (its width is the blur); --eclipse animates its centre
     via @property. This is the version that read well — smooth and cheap,
     because only a plain gradient is recomputed per frame. */
  --eclipse: -20%;
  -webkit-mask-image: linear-gradient(135deg, #000 calc(var(--eclipse) - 20%), transparent calc(var(--eclipse) + 20%));
          mask-image: linear-gradient(135deg, #000 calc(var(--eclipse) - 20%), transparent calc(var(--eclipse) + 20%));
  animation: theme-eclipse 820ms var(--ease) both;
}
@keyframes theme-eclipse {
  from { --eclipse: -20%; }
  to   { --eclipse: 120%; }
}

/* Two elevations, one shadow: the a11y sheet sits above the page, so the same
   terminator reaches it a beat later. It snapshots on its own and runs the
   identical gradient wipe with a small delay. Cheap — just a second gradient. */
.theme-eclipse .a11y { view-transition-name: a11y-sheet; }
html.theme-eclipse::view-transition-old(a11y-sheet) { animation: none; }
html.theme-eclipse::view-transition-new(a11y-sheet) {
  --eclipse: -20%;
  -webkit-mask-image: linear-gradient(135deg, #000 calc(var(--eclipse) - 20%), transparent calc(var(--eclipse) + 20%));
          mask-image: linear-gradient(135deg, #000 calc(var(--eclipse) - 20%), transparent calc(var(--eclipse) + 20%));
  /* Draw the gradient at viewport scale, shifted by the sheet's own offset, so
     the terminator is one continuous line with the page's (--sheet-x/y come from
     switchTheme in the JS). The sheet is then just a window onto the same shadow. */
  -webkit-mask-size: 100vw 100vh; mask-size: 100vw 100vh;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: calc(-1 * var(--sheet-x, 0px)) calc(-1 * var(--sheet-y, 0px));
          mask-position: calc(-1 * var(--sheet-x, 0px)) calc(-1 * var(--sheet-y, 0px));
  /* Same ease as the page; 50ms behind at the start, but shorter (590 vs 820ms)
     so it pulls ahead and its far corner clears before the page's does. */
  animation: theme-eclipse 590ms var(--ease) 50ms both;
}
@media (prefers-reduced-motion: reduce) {
  html.theme-eclipse::view-transition-new(root),
  html.theme-eclipse::view-transition-old(root),
  html.theme-eclipse::view-transition-new(a11y-sheet),
  html.theme-eclipse::view-transition-old(a11y-sheet) { animation: none; }
}

/* ---------- Tokens ---------- */
:root {
  /* ---------- Colour: semantic roles -------------------------------------
     Components reference ONLY the roles below (--surface, --text, --brand…),
     never a raw colour. Light and dark are two value sets for the same roles,
     and every accent derives from ONE hue — so re-skinning the brand or
     flipping the theme never touches a component rule.

     Theme resolution: follow the OS by default; [data-theme] on <html> pins it
     (set by the toggle in js/site.js, persisted in localStorage). */

  /* Brand. Change --brand-h alone to re-skin the site. #DE4886 == hsl(335 69% 58%). */
  --brand-h: 335;
  --brand-s: 69%;
  --brand-l: 58%;
  --brand:       hsl(var(--brand-h) var(--brand-s) var(--brand-l));
  --brand-press: hsl(var(--brand-h) var(--brand-s) calc(var(--brand-l) - 8%));
  --on-brand:    #FFFFFF;   /* text/glyphs on a brand fill */

  /* Surfaces */
  --surface:        #FFFFFF;   /* cards, header, footer, pills */
  --surface-sunken: #F7F7F7;   /* the page behind them */
  --surface-track:  #E9E9E9;   /* segmented rail, paddles, scroll-top */
  --scrim:          rgba(255,255,255,.86);   /* frosted sticky bar */

  /* Text. --text-muted is the lightest grey that still clears WCAG AA (4.5:1)
     at 11-16px on both --surface and --surface-sunken. */
  --text:        #6A6A6A;
  --text-strong: #000000;
  --text-soft:   #707070;
  --text-muted:  #767676;
  --on-solid:    #FFFFFF;   /* text on a solid --text fill */

  /* Lines */
  --border:       #E0E0E0;
  --border-image: rgba(64,70,80,.30);   /* image hairline — slate, sampled from --edge's darker stops so it reads on light shots */
  --rule:         rgba(106,106,106,.5);

  /* Dev chips (A/B + watercolour switches) — dark on light, inverted for dark. */
  --chip-bg: rgba(20,20,20,.88);
  --chip-fg: #FFFFFF;

  /* The watercolour egg stains white paper by darkening it; on a dark ground it
     has to lighten instead or it vanishes entirely. */
  --wash-blend: multiply;
  /* 1 flips the black-plate wordmark and mono client marks so they read on dark. */
  --mark-invert: 0;

  /* Shape */
  --r-card:  20px;
  --r-media: 4%;      /* fallback only — shots() stamps a precise h%/v% split inline per image */
  --r-video: 4px;     /* embedded video frames — tighter than cards/media */
  --r-pill:  999px;

  /* Rhythm */
  --pad:      40px;
  /* Effectively uncapped below tablet (bigger than any phone in portrait):
     --pad alone should govern the mobile margin. 311px was calibrated to a
     375px-wide artboard and, on every phone wider than that (most of them),
     was silently overriding --pad — changing --pad had no visible effect
     until this was fixed alongside it. */
  --wrapmax:  2000px;
  --gap-sec:  56px;
  --gap-media: 32px;     /* between images in a gallery (was 16) */
  --gap-blocks: var(--gap-sec);  /* between media sections on a case page (was 48) */
  /* Case-page rhythm. Both positive and independent: --gap-section is the break
     between titled sections, --gap-heading is how close a heading sits to the
     thing it introduces. These used to be one number fighting a negative
     margin, which is why the heading gap was hard to reason about. */
  /* The masthead stack: mark -> tagline -> nav, one even step throughout. Its
     own token because no existing one means this — --gap-media is 32px too but
     is the gallery's, and would tie the brand lockup to image spacing. */
  --gap-lockup: 32px;
  --gap-section: 78px;
  --gap-heading: 25px;
  --fab-gap:  12px;    /* consistent gap between stacked fixed buttons */
  --next-top: 50%;     /* vertical anchor shared by the next paddle and the scroll-top FAB above it */
  --font-scale: 1;     /* content text multiplier, set by the accessibility sheet */
  /* Handedness: the pagination cluster hugs whichever screen edge is under the
     thumb. Default right; [data-hand="left"] flips these four and nothing else. */
  --hand-r: 0px;                     /* pin to the right edge */
  --hand-l: auto;
  --hand-radius: var(--r-pill) 0 0 var(--r-pill);
  --hand-origin: 100% 50%;           /* scales grow from the pinned edge */
  --hand-off: 100%;                  /* scroll-top FAB slides off this edge */
  --hand-nudge: -3px;                /* rollover arrow leans inward */

  /* Elevation */
  --sh-card: 0 2px 12px rgba(0,0,0,.05);
  --sh-pill: 0 2px 8px rgba(0,0,0,.18);
  --sh-lift: 0 8px 28px rgba(0,0,0,.10);
  --hairline: rgba(0,0,0,.04);   /* 1px ring that separates a FAB from the page */
  /* Hairline edge for cards and the sheet. A radial wash rather than a flat
     stroke: brighter at the top-left, deepening toward the bottom-right, and
     kept mostly transparent so it picks up whatever sits beneath it. That is
     what makes the edge read as crisp without reading as a drawn box. */
  --edge: radial-gradient(140% 140% at 0% 0%,
            rgba(120,124,130,.45) 0%,
            rgba(70,74,80,.24) 48%,
            rgba(18,20,24,.16) 100%);

  /* Dark palette, defined once here and merely aliased by the two blocks below,
     so there is a single place to tune it. */
  --d-surface:        #1D1F22;
  --d-surface-sunken: #141619;
  --d-surface-track:  #2B2E32;
  --d-scrim:          rgba(20,21,23,.86);
  --d-text:           #B9BCC0;
  --d-text-strong:    #FFFFFF;
  --d-text-soft:      #A9ACB1;
  --d-text-muted:     #9A9EA4;
  --d-border:         #2A2D31;
  --d-border-image:   rgba(255,255,255,.18);   /* image hairline for dark — light, from --d-edge */
  --d-rule:           rgba(185,188,192,.35);
  /* Same soft shadows as light. The heavier dark set read as a hard drop rather
     than elevation, especially on cards. */
  --d-sh-card: 0 2px 12px rgba(0,0,0,.05);
  --d-sh-pill: 0 2px 8px rgba(0,0,0,.18);
  --d-sh-lift: 0 8px 28px rgba(0,0,0,.10);
  --d-hairline: rgba(255,255,255,.13);
  --d-edge: radial-gradient(140% 140% at 0% 0%,
            rgba(255,255,255,.20) 0%,
            rgba(255,255,255,.09) 48%,
            rgba(255,255,255,.035) 100%);

  /* ---------- Motion ------------------------------------------------------
     Principles, so everything on the site moves like one object:

     1. Warm, not springy-for-its-own-sake. One gentle overshoot, ~4%, then
        settle. Nothing wobbles twice.
     2. Things enter from where they came from and leave the way they arrived.
        Travel is short — 6-16px — because the eye reads position, not distance.
     3. Arrivals are slower than departures. Leaving is 120-180ms and linear-ish;
        arriving is 380-620ms on a spring. That asymmetry is what reads as human.
     4. Nothing animates that the user did not cause, except a single quiet
        entrance per element the first time it is seen.
     5. Every one of these collapses to nothing under prefers-reduced-motion.

     Use the tokens. Do not hand-roll a duration or a curve. */

  --ease:        cubic-bezier(.2,.7,.3,1);   /* standard decelerate */
  --ease-exit:   cubic-bezier(.4,0,.75,.3);  /* accelerate away     */
  --ease-bleed:  cubic-bezier(0,.5,.15,1);   /* pigment hitting paper: full speed instantly, long slow spread.
                                                Same curve as the back-to-top tween. */

  /* Damped springs as linear() ramps. --spring is the house curve (3.8%
     overshoot); --spring-lg has a touch more give (6.3%) for large type and
     longer travel. --spring-slide is a calmer relative (~1.5% overshoot) for
     the project pagination / swipe carousel, where the whole page travels and a
     big bounce reads as sloppy. Browsers without linear() fall back to `ease`
     and everything still works, just without the bounce. */
  --spring:    linear(0, 0.0438 3.85%, 0.1492 7.69%, 0.2849 11.5%, 0.4287 15.4%, 0.5659 19.2%, 0.6877 23.1%, 0.79 26.9%, 0.8716 30.8%, 0.9335 34.6%, 0.9779 38.5%, 1.008 42.3%, 1.026 46.2%, 1.035 50%, 1.038 53.8%, 1.037 57.7%, 1.033 61.5%, 1.028 65.4%, 1.023 69.2%, 1.017 73.1%, 1.013 76.9%, 1.009 80.8%, 1.005 84.6%, 1.003 88.5%, 1.001 92.3%, 0.9998 96.2%, 1 100%);
  --spring-slide: linear(0, 0.0374 4%, 0.1273 8%, 0.2438 12%, 0.3688 16%, 0.4903 20%, 0.6013 24%, 0.6981 28%, 0.7792 32%, 0.8451 36%, 0.8968 40%, 0.9362 44%, 0.9651 48%, 0.9856 52%, 0.9993 56%, 1.0079 60%, 1.0127 64%, 1.0148 68%, 1.0151 72%, 1.0142 76%, 1.0127 80%, 1.0109 84%, 1.009 88%, 1.0072 92%, 1.0056 96%, 1 100%);
  --spring-lg: linear(0, 0.04482 3.85%, 0.1541 7.69%, 0.2963 11.5%, 0.4482 15.4%, 0.5937 19.2%, 0.7229 23.1%, 0.8305 26.9%, 0.915 30.8%, 0.9774 34.6%, 1.02 38.5%, 1.046 42.3%, 1.06 46.2%, 1.063 50%, 1.06 53.8%, 1.053 57.7%, 1.044 61.5%, 1.035 65.4%, 1.026 69.2%, 1.017 73.1%, 1.011 76.9%, 1.005 80.8%, 1.001 84.6%, 0.9987 88.5%, 0.9971 92.3%, 0.9962 96.2%, 1 100%);

  --dur-tap:    110ms;   /* press feedback            */
  --dur-exit:   160ms;   /* something leaving         */
  --dur-hover:  220ms;   /* hover / colour            */
  --dur-move:   380ms;   /* position + size, on spring*/
  --dur-enter:  520ms;   /* arrivals, on spring       */
  --dur-type:   620ms;   /* large type, on spring-lg  */
  --dur-pop:    820ms;   /* easter-egg logo pop        */

  --travel:     10px;    /* standard entrance offset  */
  --lift:       -2px;    /* standard hover lift       */
  --press:      .97;     /* standard press scale      */
}

/* ---------- Dark theme ----------------------------------------------------
   Same roles, different values. Two selectors, identical bodies: follow the OS
   unless the user has pinned light, and honour an explicit dark pin. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  color-scheme: dark;
  --surface: var(--d-surface);
  --surface-sunken: var(--d-surface-sunken);
  --surface-track: var(--d-surface-track);
  --scrim: var(--d-scrim);
  --text: var(--d-text);
  --text-strong: var(--d-text-strong);
  --text-soft: var(--d-text-soft);
  --text-muted: var(--d-text-muted);
  --border: var(--d-border);
  --border-image: var(--d-border-image);
  --rule: var(--d-rule);
  --sh-card: var(--d-sh-card);
  --sh-pill: var(--d-sh-pill);
  --sh-lift: var(--d-sh-lift);
  --hairline: var(--d-hairline);
  --edge: var(--d-edge);
  --brand-l: 62%;              /* lifted so the accent holds its punch on dark */
  --chip-bg: rgba(240,240,240,.92);
  --chip-fg: #17181A;
  --wash-blend: screen;
  --mark-invert: 1;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --surface: var(--d-surface);
  --surface-sunken: var(--d-surface-sunken);
  --surface-track: var(--d-surface-track);
  --scrim: var(--d-scrim);
  --text: var(--d-text);
  --text-strong: var(--d-text-strong);
  --text-soft: var(--d-text-soft);
  --text-muted: var(--d-text-muted);
  --border: var(--d-border);
  --border-image: var(--d-border-image);
  --rule: var(--d-rule);
  --sh-card: var(--d-sh-card);
  --sh-pill: var(--d-sh-pill);
  --sh-lift: var(--d-sh-lift);
  --hairline: var(--d-hairline);
  --edge: var(--d-edge);
  --brand-l: 62%;              /* lifted so the accent holds its punch on dark */
  --chip-bg: rgba(240,240,240,.92);
  --chip-fg: #17181A;
  --wash-blend: screen;
  --mark-invert: 1;
}
:root[data-theme="light"] { color-scheme: light; }

@media (min-width: 768px) {
  :root { --pad: 40px; --wrapmax: 680px; --gap-sec: 80px;
           --gap-section: 112px; --gap-heading: 16px; }
}
@media (min-width: 1080px) {
  :root { --pad: 48px; --wrapmax: 940px; --gap-sec: 104px;
           --gap-section: 117px; --gap-heading: 32px; }
}

/* Case-study pages read narrower than the site's normal column — the whole grid
   (text, chips, media) sits at the width the screenshots used to have, so nothing
   is wider than the images. Scoped to work/* views; custom properties inherit, so
   every .wrap inside (case-head, shots, the Back button) picks it up. */
/* Mobile: widen the case-page gutter a touch so the fixed pagination paddles
   (right edge, or left when left-handed) clear the content instead of sitting
   over it. */
@media (max-width: 767px) {
  /* The pagination controls used to live in the side gutter, so the column was
     held in from the edges to clear them. They now dock at bottom-centre and
     float above the content, freeing the gutter to shrink to 32px — the media
     runs wider and detail reads larger. */
  .view[data-route^="work/"] { --pad: 32px; }
}
@media (min-width: 768px) {
  .view[data-route^="work/"] { --wrapmax: 480px; }
}
@media (min-width: 1080px) {
  .view[data-route^="work/"] { --wrapmax: 740px; }
}

/* Fixed navigation chrome opts out of the content text-size setting, so a
   "larger" choice enlarges reading text without reflowing the controls. */
.minibar, .seg, .fabs, .paddle, .scrolltop,
.huepick, .handswitch { --font-scale: 1; }

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
/* iOS paints a grey blob over any tapped link/label; our own :active and
   checked states already give the feedback. */
a, button, label, input, summary { -webkit-tap-highlight-color: transparent; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="on"]) { scroll-behavior: auto; }
  :root:not([data-motion="on"]) *,
  :root:not([data-motion="on"]) *::before,
  :root:not([data-motion="on"]) *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
/* User forced motion off — same kill, independent of the OS setting. */
:root[data-motion="off"] { scroll-behavior: auto; }
:root[data-motion="off"] *,
:root[data-motion="off"] *::before,
:root[data-motion="off"] *::after {
  animation-duration: .01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .01ms !important;
}
body {
  margin: 0;
  background: var(--surface-sunken);
  color: var(--text);
  font-family: "Avenir Next", "Avenir", "Nunito Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: calc((17px) * var(--font-scale, 1));
  line-height: 1.55;
  letter-spacing: .01em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, p, ul { margin: 0; }
ul { padding: 0; list-style: none; }
button { font: inherit; }

:where(a, button, [tabindex]):focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 3px;
  border-radius: 6px;
}
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: 50%; top: 8px; transform: translate(-50%, -150%);
  background: var(--text-strong); color: var(--surface); padding: 10px 18px; border-radius: var(--r-pill);
  font-size: calc((14px) * var(--font-scale, 1)); font-weight: 600; z-index: 100; transition: transform var(--dur-move) var(--spring);
}
.skip-link:focus { transform: translate(-50%, 0); }

/* Names must be unique per document, hence the masthead/mini-bar split. */
.masthead__lockup  { view-transition-name: brand-lockup; }
.masthead .seg     { view-transition-name: brand-nav; }
.minibar__logo     { view-transition-name: mini-logo; }
.minibar .seg      { view-transition-name: mini-nav; }
.sitefoot__lockup  { view-transition-name: foot-lockup; }

::view-transition-group(*) { animation-duration: var(--dur-hover); }
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="on"]) ::view-transition-group(*),
  :root:not([data-motion="on"]) ::view-transition-old(*),
  :root:not([data-motion="on"]) ::view-transition-new(*) { animation: none !important; }
}

/* ---------- Views ----------------------------------------------------------
   All of them are in the document at once; the router shows one. The swap is
   hand-rolled rather than left to the view-transition API because it has to
   survive Firefox and because the header is animating live underneath it — a
   root snapshot would freeze the segmented control mid-slide. Departures are
   short and flat, arrivals longer and on a spring, per the motion principles. */
.view          { display: block; }
.view[hidden]  { display: none !important; }
.view--out     { animation: view-out var(--dur-exit) var(--ease-exit) both; }
.view--in      { animation: view-in var(--dur-enter) var(--spring-slide) both; }

@keyframes view-out { to   { opacity: 0; } }
@keyframes view-in  {
  from { opacity: 0; transform: translateY(var(--travel)); }
  to   { opacity: 1; transform: none; }
}
/* On a view whose entrance is carried by its cards fanning in (see .is-fanning
   and staggerCards() in the router), the container itself only fades — otherwise
   the whole grid slides up as one box under the fan. */
.view--in.view--cards { animation-name: fade; }

/* ---------- Mobile carousel ------------------------------------------------
   pan-y hands the horizontal axis to the router while the browser keeps
   vertical scrolling native — so a swipe between projects never feels like it
   captured the page. Only the drill-in project views are swipeable. */
@media (max-width: 767px) {
  .view[data-detail="1"] { touch-action: pan-y; }
}
/* The incoming page during a slide: pinned under the sticky bar, showing its
   own top — which is exactly where entryScrollY lands once it commits, so the
   hand-off at the end of the slide is invisible. */
.view--peek {
  position: fixed;
  top: var(--peek-top, 0px);
  left: 0; right: 0; bottom: 0;
  z-index: 40;
  overflow: hidden;
  background: var(--surface-sunken);
  will-change: transform;
}
.view--peek[hidden] { display: none !important; }
@media (prefers-reduced-motion: reduce) {
  .view--out, .view--in { animation: none; }
}

/* ---------- Layout ---------- */
.wrap {
  width: min(100% - (var(--pad) * 2), var(--wrapmax));
  margin-inline: auto;
}
.section { padding-block: var(--gap-sec); }
main { background: var(--surface-sunken); }

/* ---------- Icons ---------- */
.icon { width: 1em; height: 1em; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.icon--solid { fill: currentColor; stroke: none; }
#icon-sprite { display: none; }

/* =========================================================================
   Masthead
   ========================================================================= */
.masthead {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding-block: 64px 32px;
  text-align: center;
}
.masthead__lockup { display: grid; justify-items: center; gap: var(--gap-lockup); }
.logo { width: 104px; }
.logo__mark { width: 100%; height: auto; display: block; filter: invert(var(--mark-invert)); }
/* Solid-black marks flip with the theme. Every mark in the client garden is
   monochrome black today, so the whole set flips rather than a list of
   filenames that silently misses anything added later — a colour mark added
   there must opt out with .is-color. Colour marks elsewhere (Chorus) are
   untouched because they carry neither class. */
.is-mono,
.clients img:not(.is-color) {
  filter: invert(var(--mark-invert));
}
.tagline {
  font-size: calc((clamp(22px, 5.4vw, 34px)) * var(--font-scale, 1));
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: .01em;
  color: var(--text-strong);
}

/* ---------- "[Products|Design] for humanity." ------------------------------
   The leading word arrives on a spring; "for humanity." is set separately and
   never animates. The slot sizes itself to whichever word is in it, so both
   states are optically centred at rest and the phrase glides the ~18px between
   them on the same spring during a swap — a locked-width slot would keep the
   phrase still but leave "Design" visibly off-centre in a Products-sized box.
   To lock it instead: give .tagline__slot a fixed `width` and
   `justify-content: center`. */
.tagline--swap { display: inline-flex; flex-wrap: wrap; justify-content: center; gap: 0 .28em; }

/* Both words sit stacked in one grid cell so either can be measured without a
   hidden clone. The slot's width is the thing that springs; the phrase after it
   rides along. Before the script runs the inactive word is display:none, so the
   slot is naturally the right width and there is nothing to correct on load. */
.tagline__slot {
  display: grid;
  grid-template-columns: 100%;
  justify-items: center;
  transition: width var(--dur-move) var(--spring);
}
.tagline__word {
  grid-area: 1 / 1;
  white-space: nowrap;
  transform-origin: 50% 85%;
}
.tagline__word.is-off  { display: none; }
.js .tagline__word     { display: block; }
.js .tagline__word.is-off { opacity: 0; pointer-events: none; }
.tagline__rest { display: inline-block; }

/* Suppress the spring while the script takes its first measurement. */
.tagline__slot.is-placing { transition: none; }

.tagline__word.is-in  { animation: word-in  var(--dur-type) var(--spring-lg) both; }
.tagline__word.is-out { animation: word-out var(--dur-exit) var(--ease-exit) both; }

/* On phones "Product Design for humanity." wraps but "Products for humanity."
   doesn't, so the headline changes line count on swap and shoves the lockup.
   Force the break: the swap word is always line 1, "for humanity." always line
   2 — constant height, so nothing below it moves. */
@media (max-width: 767px) {
  .tagline--swap { flex-direction: column; align-items: center; }
}

@keyframes word-in {
  0%   { opacity: 0; transform: translateY(.34em) scale(.94); }
  40%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}
@keyframes word-out {
  to { opacity: 0; transform: translateY(-.28em) scale(.96); }
}



/* ---------- Segmented control ---------------------------------------------
   The pink thumb is a separate element sitting in the same grid cell as the
   active tab, so it can carry its own view-transition-name and physically slide
   between tabs on navigation instead of blinking off one and on to the other. */
/* Same step as the rest of the masthead stack, so mark, tagline and nav sit on
   one even rhythm. The mini bar zeroes it — see .minibar .seg. */
.seg { display: flex; justify-content: center; margin-top: var(--gap-lockup); }
.seg__group {
  position: relative;
  display: grid;
  grid-template-columns: auto auto;
  background: var(--surface-track);
  border-radius: var(--r-pill);
  isolation: isolate;
}
.seg__item {
  grid-row: 1;
  position: relative; z-index: 1;   /* above .seg__thumb, so the whole tab is the hit target */
  display: grid; place-items: center;
  min-height: 41px;
  padding: 8px 20px;
  border-radius: var(--r-pill);
  font-size: calc((18px) * var(--font-scale, 1)); font-weight: 600;
  color: var(--text);
  transition: color var(--dur-hover) var(--ease), transform var(--dur-tap) var(--ease);
}
.seg__label { pointer-events: none; }   /* clicks on the text resolve to the anchor, not the glyphs — whole tab is one hit target */
.seg__item:nth-of-type(1) { grid-column: 1; }
.seg__item:nth-of-type(2) { grid-column: 2; }
.seg__item:hover { color: var(--text-strong); }
.seg__item:active { transform: scale(var(--press)); }
.seg__item[aria-current="page"] { color: var(--on-brand); }

/* Base state, and the whole story without JS: the thumb simply occupies the
   active tab's grid cell. */
.seg__thumb {
  grid-row: 1;
  grid-column: 1;
  z-index: -1;
  pointer-events: none;
  border-radius: var(--r-pill);
  background: var(--brand);
  box-shadow: var(--sh-pill);
}
.seg__group[data-active="design"] .seg__thumb { grid-column: 2; }

/* Hydrated: same geometry, now measured and absolutely placed so it can slide
   and resize between the two tabs on the house spring. */
.js .seg__thumb {
  position: absolute;
  top: 0; left: 0; height: 100%;
  grid-column: 1;
  width: var(--thumb-w, 0px);
  transform: translateX(var(--thumb-x, 0px));
  transition: transform var(--dur-move) var(--spring),
              width var(--dur-move) var(--spring);
}
.js .seg__group[data-active="design"] .seg__thumb { grid-column: 1; }
.seg__group.is-placing .seg__thumb { transition: none; }
/* Two thumbs exist per page — masthead and sticky bar — and a duplicated
   view-transition-name aborts the whole transition, so they are named apart. */
.masthead .seg__thumb { view-transition-name: seg-thumb; }
.minibar  .seg__thumb { view-transition-name: seg-thumb-mini; }
::view-transition-group(seg-thumb),
::view-transition-group(seg-thumb-mini) {
  animation-duration: var(--dur-move);
  animation-timing-function: var(--spring);
}
/* The thumb is a flat pill, so cross-fading the two snapshots would only add
   mush to a move that already reads perfectly on its own. */
::view-transition-old(seg-thumb),      ::view-transition-new(seg-thumb),
::view-transition-old(seg-thumb-mini), ::view-transition-new(seg-thumb-mini) {
  animation: none; mix-blend-mode: normal;
}
::view-transition-new(seg-thumb),
::view-transition-new(seg-thumb-mini) { opacity: 1; }

/* ---------- Email FAB ------------------------------------------------------
   Was a pill sitting inside the segmented control; it competed with the tabs for
   the same reading. Now a circle in the top-right of the masthead, repeated in
   the sticky bar so there is always exactly one on screen.
   To revert: delete these rules, drop mailfab() from masthead() in
   _tools/generate.py, put the old anchor back at the end of segnav() —
       '<a class="seg__mail" href="mailto:%s" aria-label="Email Squibb Media">%s</a>'
   — and restore these two rules:
       .seg { gap: 8px; }
       .seg__mail { display: grid; place-items: center; min-width: 66px;
                    min-height: 41px; border-radius: var(--r-pill);
                    background: var(--surface-track); color: var(--text);
                    font-size: calc((20px) * var(--font-scale, 1)); }
       .seg__mail:hover { background: #DEDEDE; color: var(--text-strong); }
       .minibar .seg__mail { min-width: 44px; min-height: 34px; font-size: calc((16px) * var(--font-scale, 1)); } */
.fab {
  position: relative;
  display: grid; place-items: center;
  width: 48px; height: 48px;
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  font-size: calc((20px) * var(--font-scale, 1));
  box-shadow: var(--sh-card), 0 0 0 1px var(--hairline);
  transition: transform var(--dur-move) var(--spring),
              color var(--dur-hover) var(--ease),
              box-shadow var(--dur-hover) var(--ease);
}
.fab .icon { position: relative; z-index: 1; }
.fab:hover { color: var(--brand); transform: translateY(var(--lift)); box-shadow: var(--sh-lift), 0 0 0 1px var(--hairline); }
.fab:active { transform: scale(var(--press)); transition-duration: var(--dur-tap); }


/* The two FABs travel as a pair, so the group carries the position and each
   button only carries its own size. Distinct view-transition-names per button:
   a duplicated name silently aborts the whole transition. */
.fabs { display: flex; align-items: center; gap: 10px; }
.fabs--masthead {
  position: absolute;
  top: 20px; right: clamp(16px, 4vw, 48px);
  /* Above the sheet (z 91): the desktop popover animates up over the FAB's lower
     half on the way in, and at z-index 1 the FAB got buried and ate the click.
     The FAB floats above it instead — and now toggles, so it always closes it. */
  z-index: 92;
}
.fabs--masthead .fab--a11y  { view-transition-name: a11y-fab; }
.fabs--masthead a.fab       { view-transition-name: mail-fab; }
.minibar a.fab              { view-transition-name: mail-fab-mini; }

.fab--a11y { border: 0; padding: 0; cursor: pointer; font: inherit; }
.fab--mini { width: 38px; height: 38px; font-size: calc((16px) * var(--font-scale, 1)); }
@media (min-width: 768px) { .fabs--masthead { top: 28px; } }
@media (min-width: 1080px) {
  .fabs--masthead { top: 36px; }
  .fab--masthead { width: 54px; height: 54px; }
}

/* Only the OPEN sheet is interactive. While it fades out (is-open removed but
   not yet hidden) it must be click-through, or it keeps eating clicks on
   whatever it overlaps — notably the FAB. */
.a11y:not(.is-open) { pointer-events: none; }

/* ---------- Accessibility sheet -------------------------------------------
   The a11y FAB (beside mail) opens a bottom sheet of deliberate display and
   accessibility controls. Native radios in fieldsets, so it is a real radiogroup
   for keyboard and screen readers; the segmented look is pure CSS via :has(). */
.a11y-scrim {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0,0,0,.42);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-move) var(--ease), visibility 0s linear var(--dur-move);
}
.a11y-scrim.is-open { opacity: 1; visibility: visible; transition-delay: 0s; }

.a11y {
  position: fixed; z-index: 91;
  left: 0; right: 0; bottom: calc(-1 * var(--sheet-overhang, 80px)); margin: 0 auto;
  width: 100%; max-width: 460px;
  max-height: 88vh; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--surface);
  border-radius: var(--r-card) var(--r-card) 0 0;   /* matches project cards */
  box-shadow: var(--sh-lift);
  padding: 10px 22px calc(28px + env(safe-area-inset-bottom, 0px) + var(--sheet-overhang, 80px));
  transform: translateY(101%);
  transition: transform var(--dur-enter) var(--spring-lg);
}
.a11y.is-open { transform: translateY(0); }

/* Tablet and up: a popover anchored under the top-right FAB, dropping down from
   that corner — minimal mouse travel to the first control. No bottom overhang,
   no grip; a light click-catcher scrim instead of the modal dim. */
@media (min-width: 768px) {
  .a11y {
    top: 84px; right: calc(clamp(16px, 4vw, 48px) + 48px + 10px);   /* = a11y FAB's right edge */
    left: auto; bottom: auto; margin: 0;
    width: 340px; max-width: 340px;
    max-height: min(80vh, 600px);
    border-radius: var(--r-card);   /* matches project cards */
    padding: 18px 24px 24px;
    transform: translateY(-40px);
    opacity: 0;
    transition: transform var(--dur-enter) var(--spring-lg), opacity var(--dur-move) var(--ease);
  }
  .a11y.is-open { transform: none; opacity: 1; }
  .a11y__grip { display: none; }
  /* Popover, not a modal: the scrim only dims. It must not take pointer
     events, or it swallows the first click on whatever you aimed at. */
  .a11y-scrim { background: rgba(0,0,0,.12); pointer-events: none; }
}
@media (min-width: 1080px) { .a11y { top: 100px; right: calc(clamp(16px, 4vw, 48px) + 54px + 10px); } }
.a11y__grip {
  width: 40px; height: 4px; border-radius: 999px;
  background: var(--border); margin: 4px auto 14px;
}
.a11y__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.a11y__title { font-size: calc(19px * var(--font-scale, 1)); font-weight: 700; color: var(--text-strong); letter-spacing: -.01em; }
.a11y__close {
  width: 34px; height: 34px; display: grid; place-items: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: var(--surface-track); color: var(--text);
}
.a11y__close .icon { width: 18px; height: 18px; stroke-width: 2.2; }

.a11y-row { border: 0; margin: 0 0 20px; padding: 0; }
.a11y-row:last-of-type { margin-bottom: 4px; }
.a11y-row__label {
  /* flex so the shortcut chip can sit right after the label text */
  display: flex; align-items: center; gap: 8px;
  padding: 0; margin-bottom: 10px;
  font-size: calc(15px * var(--font-scale, 1)); font-weight: 600; letter-spacing: -.005em;
  color: var(--text);
}
/* Shortcut hint. A quiet key cap, not a button — it documents the shortcut
   rather than offering another way to press it. */
.a11y-key {
  display: inline-grid; place-items: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 5px;
  background: var(--surface-track);
  color: var(--text-muted);
  font: inherit;
  font-size: calc(10px * var(--font-scale, 1));
  font-weight: 700; letter-spacing: .04em; line-height: 1;
}
/* Hidden where the shortcut can't be typed. */
@media (hover: none) and (pointer: coarse) { .a11y-key { display: none; } }

/* Shortcut toast. Same pill vocabulary and motion tokens as the rest of the
   chrome: rises on the standard spring, leaves on the exit ease. */
.toast {
  /* Drops in from the top edge and settles in the top-right, inset from both
     edges by the page gutter token. */
  position: fixed;
  top: var(--pad); right: var(--pad);
  left: auto; bottom: auto;
  z-index: 95;
  display: flex; align-items: baseline; gap: 8px;
  padding: 10px 16px;
  border-radius: var(--r-pill);
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: var(--chip-fg);
  box-shadow: var(--sh-lift);
  pointer-events: none;
  opacity: 0;
  transform: translateY(-40px);          /* drops down into place from the top */
  transition: opacity var(--dur-exit) var(--ease-exit),
              transform var(--dur-exit) var(--ease-exit);
}
.toast.is-in {
  opacity: 1;
  transform: translateY(0);
  transition: opacity var(--dur-move) var(--ease),
              transform var(--dur-move) var(--spring);
}
.toast__label { font-size: calc(13px * var(--font-scale, 1)); opacity: .6; }
.toast__value { font-size: calc(13px * var(--font-scale, 1)); font-weight: 600; }
.a11y-seg {
  position: relative; isolation: isolate;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  background: var(--surface-track); border-radius: var(--r-pill); padding: 3px;
}
.a11y-seg__thumb {
  position: absolute; z-index: 0;
  top: 3px; bottom: 3px; left: 3px;
  width: calc((100% - 6px) / var(--n, 3));
  border-radius: var(--r-pill);
  background: var(--surface); box-shadow: var(--sh-pill);
  transform: translateX(calc(var(--i, 0) * 100%));
  transition: transform var(--dur-move) var(--spring);
}
.a11y-seg:has(.a11y-seg__opt:nth-of-type(1) input:checked) { --i: 0; }
.a11y-seg:has(.a11y-seg__opt:nth-of-type(2) input:checked) { --i: 1; }
.a11y-seg:has(.a11y-seg__opt:nth-of-type(3) input:checked) { --i: 2; }
.a11y--sync .a11y-seg__thumb { transition: none; }
.a11y-seg__opt { position: relative; z-index: 1; display: block; min-width: 0; }
.a11y-seg__opt input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.a11y-seg__opt span {
  display: grid; place-items: center; padding: 10px 6px;
  border-radius: var(--r-pill); font-size: calc(14px * var(--font-scale, 1)); font-weight: 600;
  color: var(--text); white-space: nowrap;
  transition: color var(--dur-hover) var(--ease), background-color var(--dur-hover) var(--ease);
}
.a11y-seg__opt:has(input:checked) span { color: var(--text-strong); }
.a11y-seg__opt input:focus-visible + span { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Handedness is only meaningful for a reaching thumb — mobile only. */
.a11y-row--mobile { display: none; }
@media (max-width: 767px) { .a11y-row--mobile { display: block; } }
.a11y-note { margin-top: 14px; font-size: calc(12px * var(--font-scale, 1)); line-height: 1.5; color: var(--text-muted); }

/* Both glyphs ship; the one matching the RESOLVED theme is shown, and it is the
   theme you would switch TO — moon while light, sun while dark. */
.fab__glyph {
  grid-area: 1 / 1;
  display: grid; place-items: center;
  transition: opacity var(--dur-hover) var(--ease),
              transform var(--dur-move) var(--spring);
}
.fab__glyph--sun  { opacity: 0; transform: scale(.55) rotate(-60deg); }
.fab__glyph--moon { opacity: 1; transform: none; }
:root[data-resolved="dark"] .fab__glyph--sun  { opacity: 1; transform: none; }
:root[data-resolved="dark"] .fab__glyph--moon { opacity: 0; transform: scale(.55) rotate(60deg); }
@media (prefers-reduced-motion: reduce) { .fab__glyph { transform: none !important; } }

.masthead { position: relative; overflow: hidden; }

/* ---------- Easter egg: watercolour bloom ----------------------------------
   Fires when you fling the page to the top hard (see js/site.js). The logo pops
   on a spring, and as it first returns to 100% a wash of pigment blooms from
   behind it. Two modes, A/B: 'fade' clears itself in ~3s; 'paint' leaves the
   wash so repeat trips build up a painting. */
.wc { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.wc__drop {
  position: absolute;
  /* Multiply so the pigment reads as ink soaking into the white paper rather
     than a coloured pane laid on top; blur is what kills every hard edge. */
  mix-blend-mode: var(--wash-blend);
  filter: blur(14px);
  opacity: 0;
  will-change: transform, opacity;
}
/* Everything in the header sits above the wash. .wc is positioned, so without
   this it would paint over the non-positioned content in .wrap (the segmented
   control especially). The mail FAB keeps its own `position: absolute` — only
   the z-index is added here. */
.masthead > .wrap { position: relative; z-index: 1; }
.fab--masthead    { z-index: 1; }

.masthead .logo { transform-origin: 50% 50%; }

.masthead .logo.is-pop { animation: logo-pop var(--dur-pop) linear both; }
/* Quick pop up, back through 100% (that crossing is when the wash drops), then
   two softening overshoots as it settles. */
@keyframes logo-pop {
  0%   { transform: scale(1); }
  18%  { transform: scale(1.16); }
  38%  { transform: scale(1); }
  56%  { transform: scale(1.055); }
  72%  { transform: scale(.982); }
  86%  { transform: scale(1.014); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .masthead .logo.is-pop { animation: none; }
  .wc { display: none; }
}

/* Compact sticky bar — appears once the masthead scrolls away */
.minibar {
  position: fixed; inset: 0 0 auto 0; z-index: 50;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 10px max(16px, calc((100vw - var(--wrapmax)) / 2 - 8px));
  background: var(--scrim);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--border);
  transform: translateY(-100%);
  transition: transform var(--dur-move) var(--spring);
}
/* The spring settles with a small downward overshoot, which would dip this
   top-pinned bar below the screen edge and flash a gap above it. This band
   extends the bar's background (scrim + blur) up past the top edge, so the
   overshoot is masked — it just slides the band down into that off-screen
   reserve instead of revealing empty space. It moves with the bar. */
.minibar::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 100%;
  height: 40px;
  background: var(--scrim);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
}
.minibar.is-visible { transform: translateY(0); }
.minibar .logo { width: 56px; }
.minibar .seg { margin-top: 0; }
.minibar .seg__item { min-height: 34px; padding: 5px 14px; font-size: calc((14px) * var(--font-scale, 1)); }

/* =========================================================================
   Pills — chips, buttons, links
   ========================================================================= */
.pill {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 36px;
  padding: 8px 18px;
  border-radius: var(--r-pill);
  font-size: calc((12px) * var(--font-scale, 1)); font-weight: 500; letter-spacing: .02em;
  /* Tight line-height: the inherited prose line-height (~1.8) gave the label a
     tall line-box, so its glyphs sat high and off the centred icon. */
  line-height: 1;
  white-space: nowrap;
}
.pill .icon { width: 14px; height: 14px; }

.pill--tag { background: var(--surface); color: var(--text); box-shadow: var(--sh-card); cursor: default; }
.pill--nav {
  background: var(--text); color: var(--on-solid);
  transition: background-color var(--dur-hover) var(--ease),
              transform var(--dur-move) var(--spring);
}
.pill--nav:hover  { background: #565656; transform: translateY(var(--lift)); }
.pill--nav:active { transform: scale(var(--press)); transition-duration: var(--dur-tap); }

.taglist { display: flex; flex-wrap: wrap; gap: 10px; }

.btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  width: 100%;
  min-height: 55px;
  padding: 12px 24px;
  border: 0; border-radius: var(--r-pill);
  background: var(--brand); color: var(--on-brand);
  font-size: calc((18px) * var(--font-scale, 1)); font-weight: 600; letter-spacing: .01em;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease),
              transform var(--dur-move) var(--spring),
              box-shadow var(--dur-hover) var(--ease);
}
.btn:hover  { background: var(--brand-press); transform: translateY(var(--lift)); box-shadow: var(--sh-lift); }
.btn:active { transform: scale(var(--press)); transition-duration: var(--dur-tap); }

/* The Download button lives in a compact product-cta card, where full width is
   correct at any size. "View Project" sits directly in the case-study column,
   which runs up to 940px — full width there reads as a banner, not a button.
   btn--fit keeps the same full-width mobile CTA (easiest target on a phone,
   consistent with everything else at that size) and switches to sized-to-its-
   label from tablet up. */
@media (min-width: 768px) {
  .btn--fit { width: auto; display: inline-flex; padding-inline: 40px; }
}
.btn__note {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: calc((11px) * var(--font-scale, 1)); font-weight: 500; opacity: .92;
}
.btn__note .icon { width: 11px; height: 11px; }
/* "Coming soon" / archive-locked states keep the brand colour — the note under
   the label carries the meaning, so the button still reads as the design intends. */
.btn[aria-disabled="true"] { cursor: default; }
.btn[aria-disabled="true"]:hover { background: var(--brand); transform: none; box-shadow: none; }

/* =========================================================================
   Cards
   ========================================================================= */
.card {
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--sh-card);
}

/* Design index tiles */
.tiles { display: grid; gap: 32px 20px; }
@media (min-width: 768px) { .tiles { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1080px) { .tiles { grid-template-columns: repeat(3, 1fr); gap: 48px 24px; } }

.tile {
  display: grid; place-items: center;
  min-height: 176px;
  padding: 32px;
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--sh-card);
  transition: transform var(--dur-move) var(--spring), box-shadow var(--dur-hover) var(--ease);
}
/* Gradient hairline. Drawn as a .5px inset ring: the gradient fills the box and
   a mask punches out everything but that ring, which is the only way to get a
   gradient *border* that follows the radius. */
/* Only .tile needs a containing block for the ring below; .a11y is already
   position:fixed and must stay that way — setting it relative here dropped
   the sheet into normal flow, far below the fold. */
.tile { position: relative; }
.tile::after, .a11y::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  padding: .5px;
  background: var(--edge);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
  z-index: 2;
}
.tile:hover  { transform: translateY(-3px); box-shadow: var(--sh-lift); }
.tile:active { transform: scale(.985); transition-duration: var(--dur-tap); }
.tile__mark  { transition: transform var(--dur-move) var(--spring); }
.tile:hover .tile__mark { transform: scale(1.04); }
.tile__mark { display: grid; place-items: center; gap: 12px; text-align: center; }
.tile__mark img, .tile__mark svg { max-height: 104px; width: auto; }
.tile__mark img.is-mono { opacity: .58; }
.tile__mark .icon { width: 96px; height: 96px; color: var(--text); stroke-width: 1.5; }
/* Love You's mark is its app icon — a full-colour squircle, not a logo drawn to
   fit a box. It reads best a touch larger than the flat marks and squared up.
   Two-class specificity so it beats the `.tile__mark img` height cap above. */
.tile__mark .tile__appicon { width: 120px; height: 120px; max-height: none; }
.tile__name { font-size: calc((15px) * var(--font-scale, 1)); font-weight: 600; color: var(--text-soft); letter-spacing: .02em; }

/* Client logo wall */
.clients {
  display: grid; grid-template-columns: repeat(3, 1fr);
  align-items: center; justify-items: center;
  gap: 24px 16px;
  padding-top: 40px;
  border-top: 1px solid var(--border);
}
.clients li { display: grid; place-items: center; width: 100%; }
.clients img, .clients svg { max-height: 56px; max-width: 100%; width: auto; opacity: .3; }
@media (min-width: 768px) {
  .clients { grid-template-columns: repeat(6, 1fr); gap: 32px; }
}

/* Product CTA card */
.product-cta {
  display: grid; gap: 20px;
  padding: 20px;
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--sh-card);
  border: 1px solid rgba(151,151,151,.28);
}
.product-cta__app { display: flex; align-items: center; gap: 16px; }
.product-cta__icon { width: 48px; height: 48px; flex: none; }
.product-cta__name { font-size: calc((18px) * var(--font-scale, 1)); font-weight: 600; color: var(--text); line-height: 1.25; }
.product-cta__by { font-size: calc((12px) * var(--font-scale, 1)); color: var(--text-soft); }
@media (min-width: 768px) { .product-cta { max-width: 420px; } }

/* =========================================================================
   Product page
   ========================================================================= */
.hero-shots { display: grid; justify-items: center; }
.hero-shots img { width: 100%; max-width: 560px; }
.hero-shots--pair { grid-template-columns: 1fr 1fr; gap: 12px; align-items: end; }
@media (min-width: 768px) { .hero-shots--pair { gap: 32px; max-width: 620px; margin-inline: auto; } }

.product { display: grid; gap: 32px; }
.product__lockup { display: flex; align-items: center; gap: 16px; }
.product__lockup img { width: 48px; height: 48px; }
.product__title {
  font-size: calc((clamp(30px, 7.5vw, 44px)) * var(--font-scale, 1));
  font-weight: 700; color: var(--text); letter-spacing: -.01em; line-height: 1.05;
}
.product__sub { font-size: calc((clamp(18px, 3vw, 22px)) * var(--font-scale, 1)); font-weight: 600; color: var(--text); }
.prose p { font-size: calc((clamp(17px, 2.2vw, 19px)) * var(--font-scale, 1)); color: var(--text); }
/* Inline text links in body copy — brand-coloured with a light underline. Pills
   (e.g. the View website button) keep their own treatment. */
.prose a:not(.pill) { color: var(--brand); text-decoration: underline; text-underline-offset: 2px;
  text-decoration-thickness: 1px; transition: color var(--dur-hover) var(--ease); }
.prose a:not(.pill):hover { color: var(--brand-press); }
.prose p + p { margin-top: 1.1em; }
/* Award credit — an inline line at the end of the copy, body-text colour,
   outline glyph. Not a chip, not clickable. */
.case-credit { display: flex; align-items: center; gap: 9px; margin-top: 1.4em;
  color: var(--text); font-size: calc((clamp(15px, 2vw, 16px)) * var(--font-scale, 1)); font-weight: 600; }
.case-credit__glyph { width: 1.15em; height: 1.15em; color: currentColor; flex: none; }
.note { font-size: calc((clamp(17px, 2.2vw, 19px)) * var(--font-scale, 1)); color: var(--text-soft); }

@media (min-width: 1080px) {
  .product-layout { display: grid; grid-template-columns: 1.05fr .95fr; gap: 64px; align-items: center; }
  .product-layout .hero-shots img { max-width: none; }
}

/* =========================================================================
   Case studies
   ========================================================================= */

/* ---------- Hero banner -----------------------------------------------------
   A full-bleed photo at the top of a case study — Apple and Chorus only, so
   far, both via case_hero() in _tools/generate.py. overflow:hidden + an image
   taller than its box (130%) is what gives the parallax below room to move
   without ever exposing an edge. */
.case-hero {
  position: relative;
  overflow: hidden;
  height: clamp(220px, 34vw, 380px);
  background: var(--surface-track);   /* visible briefly if the photo loads slow */
}
.case-hero__img {
  position: absolute; top: -15%; left: 0;
  width: 100%; height: 130%;
  object-fit: cover;
}
.case-hero__back {
  position: absolute; top: 20px; left: 0; right: 0;
}
@media (min-width: 768px) { .case-hero__back { top: 28px; } }

/* The Back pill normally reads fine on the plain page background; floating on
   a photo is a different problem — a mid-tone photo can match its grey almost
   exactly and the pill disappears. The white border is what keeps an edge
   visible against any photo, light or dark. */
.pill--on-photo {
  border: 1.5px solid rgba(255,255,255,.92);
  box-shadow: 0 1px 6px rgba(0,0,0,.35);
}

.case-head { display: grid; gap: 24px; }
/* Contained lead media (image or video) above the logo + copy on case pages. */
.case-lead { margin-bottom: var(--gap-blocks); }
/* Video leads only: edge-to-edge on mobile (they read too small in the narrow
   column). Desktop keeps them contained in the grid. */
@media (max-width: 767px) {
  .case-lead--bleed { width: 100vw; margin-inline: calc(50% - 50vw); }
  .case-lead--bleed .video-embed {
    max-width: none; margin-inline: 0;
    border-radius: 0;
  }
  .case-lead--bleed .video-embed::after { display: none; }   /* no hairline at the screen edge */
  /* Drop the section's top padding so the full-bleed video sits flush under the
     sticky navbar — no gutter of negative space above it. */
  .section:has(> .wrap > .case-lead--bleed) { padding-top: 0; }
}
.case-head__text { display: grid; gap: 16px; justify-items: start; }
.case-head__logo { display: flex; align-items: flex-end; }
/* The logo used to live in a tall white card that overlapped the hero photo.
   It now sits bare at the top of the text column, so it is sized to lead the
   headline rather than to fill a box: same relative weighting between the
   three marks as before, scaled down to headline scale and left-aligned with
   everything else in the column. */
.case-head__logo img, .case-head__logo svg { height: 32px; width: auto; max-height: none; }
.case-head__logo img.is-mono { opacity: .58; height: 40px; }   /* apple-mark.svg is cropped to the glyph, so this is 40px of actual mark */
/* A long wordmark (Dockers) at glyph height dominates the headline. Size it to
   a cap height instead — width follows. Comes after is-mono so it wins. */
/* Optical normalisation of the marks above a title. The Apple glyph at 40px
   (is-mono) reads right; wider marks feel bigger at the same height, so they
   step down: a wide monogram (EA) to 32, a long wordmark (Dockers) to 20. */
.case-head__logo img.cl-wide { height: 41px; }        /* wide monogram (EA) */
.case-head__logo img.cl-wordmark { height: 20px; }
.case-head__logo img.cl-stacked { height: 48px; }     /* a stacked colour wordmark (Meow Mix) needs height to read */
.case-head__logo img.cl-icon { height: 44px; }        /* a square badge mark (Freebord) — sized for presence, not a wordmark */
.case-head__logo .icon { width: 44px; height: 44px; color: var(--text); stroke-width: 1.5; }


.case-title {
  font-size: calc((clamp(32px, 9vw, 64px)) * var(--font-scale, 1));
  font-weight: 700; color: var(--text);
  line-height: 1.02; letter-spacing: -.015em;
}
.case-lede {
  font-size: calc((clamp(19px, 3.2vw, 26px)) * var(--font-scale, 1));
  font-weight: 600; color: var(--text); line-height: 1.35;
}
.case-body { display: grid; gap: 24px; }

/* Deliberately never a two-column stack: the client mark reads as the top of
   the column, with everything flowing under it. Side-by-side left the mark
   floating in its own tall white card next to a wall of text. */
@media (min-width: 768px) {
  .case-head { gap: 32px; }
  .case-head__text { gap: 20px; }
}

/* Section rule + title. The icon (when present — Chorus's own product icons,
   captured from their live site) stacks above the heading rather than sitting
   beside it, so the title and subtitle keep the same strong left edge as
   everything else in the column instead of being pushed in by the icon's width.
   .section-title__text carries the h3+p grid so the layout is identical with
   or without an icon. */
/* A case page is a stack of titled sections. */
.case-sections { display: grid; gap: var(--gap-section); margin-top: var(--gap-section); }
/* A product's own impact tiles follow its screens inside the same section, so
   they take the within-section step rather than the between-sections one. */
.case-section > .shots + .impact-grid { margin-top: var(--gap-media); }
.case-section > .section-title,
.case-section > .section-card { margin-bottom: var(--gap-heading); }

.section-title {
  /* Spacing lives on .case-section (both values positive); this rule is just
     the heading itself. */
  display: grid;
  gap: 14px;
  justify-items: start;
}
.section-title__icon {
  width: 40px; height: 40px;
}
.section-title__text { display: grid; gap: 4px; }
.section-title h3 {
  font-size: calc((clamp(21px, 3.4vw, 30px)) * var(--font-scale, 1));
  font-weight: 600; color: var(--text); letter-spacing: -.005em; line-height: 1.15;
}
.section-title p { font-size: calc((clamp(14px, 1.8vw, 16px)) * var(--font-scale, 1)); color: var(--text-soft); }

@media (min-width: 768px) {
  .section-title__icon { width: 48px; height: 48px; }
}

/* Products & Features: one row per product, a device glyph saying which
   surfaces it shipped on, on the same card as the impact tiles. Single column
   at every size — these are full product names, and two-up wrapped nearly all
   of them. */
.productlist { display: grid; gap: 8px; }
/* Two-up only in the widest column. At 1080+ each row gets 366px and nothing
   wraps; in the 480px tablet column two-up leaves 236px, which wrapped 4 of
   Apple's 15 names and 6 of Chorus's 16 — a ragged grid for no gain, so tablet
   stays single-column with the phone. */
@media (min-width: 1080px) {
  .productlist { grid-template-columns: 1fr 1fr; }
}
/* Scoped under .productlist: a bare .product already belongs to the Love You
   product page, and a second component by that name silently reformatted it. */
.productlist__row {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: calc((clamp(16px, 2vw, 17px)) * var(--font-scale, 1));
  line-height: 1.3;
  color: var(--text);
}
/* Fixed box so every name starts on the same left edge regardless of glyph. */
.productlist__glyph { width: 24px; height: 24px; color: var(--text-soft); flex: none; }

/* Placeholder shown on Products while a product is behind its flag. */
.product-soon {
  font-size: calc((clamp(19px, 3.2vw, 26px)) * var(--font-scale, 1));
  font-weight: 600; color: var(--text-soft);
  text-align: center;
  padding-block: var(--gap-sec);
}

/* ---------- Product section header --------------------------------------
   A level below .section-title: that labels a part of the case study, this
   labels a product inside it. Sketch has it as a tinted card — icon(s) left,
   name and one line of description right — so it reads as an object rather
   than a heading. The design-system header carries the whole icon set, which
   is why the icons are a wrapping row and not a single slot. */
.section-card {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px;
  background: var(--surface-track);
  border: 1px solid var(--border);
  border-radius: 12px;
}
.section-card__icons { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; flex: none; }
.section-card__icons img { width: 32px; height: 32px; display: block; }
/* The open slot: a dashed outline saying the set keeps growing — you can keep
   building apps on the system. Dash and radius match the boards (1px, 4/2) and
   the icons' own 4px corner. */
.section-card__plus {
  font-size: calc((16px) * var(--font-scale, 1));
  font-weight: 600; color: var(--text-soft);
  width: 32px; height: 32px; display: grid; place-items: center;
  border: 1px dashed var(--rule);
  border-radius: 4px;
}
.section-card__text { display: grid; gap: 2px; }
.section-card h3 {
  font-size: calc((16px) * var(--font-scale, 1));
  font-weight: 600; line-height: 1.3; color: var(--text);
}
.section-card p {
  font-size: calc((13px) * var(--font-scale, 1));
  line-height: 1.35; color: var(--text-soft);
}
/* Single-icon headers read left-to-right at every width — icon, then the name
   beside it. The design-system header is the exception, and stacks: its seven
   slots are the subject, so they get a full row of their own with the copy
   under them, as on the board. */
.section-card--set { flex-direction: column; align-items: stretch; }
.section-card--set .section-card__icons { width: auto; }

/* ---------- Fact lists + impact tiles ------------------------------------
   Both mirror blocks on the Sketch case boards. The fact list is the two-column
   run of short items under App Ecosystem / Team; the trailing "And more..."
   spans the full width. Columns are equal so the two read as one block, and the
   list stays two-up at every size — these are short phrases, and a single column
   would run the section twice as long for no gain. */
.factlist {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Row gap has to clear the line spacing: at body size most of these items
     wrap to two lines in a phone column, and a tighter gap ran the second line
     of one item into the first line of the next. */
  gap: 16px 20px;
  /* Same size as body copy (.prose p) — these read as prose, not captions. */
  font-size: calc((clamp(17px, 2.2vw, 19px)) * var(--font-scale, 1));
  line-height: 1.35;
  color: var(--text-soft);
}
.factlist__more { grid-column: 1 / -1; color: var(--text-muted); }

/* The metric is the point of a tile, so it gets the brand colour and the size;
   everything around it stays quiet. Two-up on phones, matching the boards; from
   tablet up the tiles auto-fit against a wider minimum, which lands 2-up in the
   480px column and 3-up in the 740px one. Left to auto-fit alone it packed 4
   tiles into the wide column, too narrow for the wordier metrics. */
.impact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
@media (min-width: 768px) {
  .impact-grid { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
}
.impact {
  display: flex; flex-direction: column;
  gap: 6px;
  padding: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
}
.impact__metric {
  font-size: calc((32px) * var(--font-scale, 1));
  font-weight: 700; line-height: 1.05; letter-spacing: -.015em;
  color: var(--brand);
  text-wrap: balance;
}
/* Numerals only — see impact_card(). Avenir Next's digits are drawn for text
   sizes and read loose at 32px; the words around them do not. */
.impact__num {
  letter-spacing: -0.035em;
  /* letter-spacing also lands after the LAST character, which was pulling the
     word after the number in with it ("4x faster"). Give that back so only the
     gaps between digits close up. */
  margin-right: 0.035em;
}
.impact__metric--sm { font-size: calc((24px) * var(--font-scale, 1)); }
.impact__metric--xs { font-size: calc((20px) * var(--font-scale, 1)); line-height: 1.15; }
.impact__eyebrow,
.impact__label {
  font-size: calc((14px) * var(--font-scale, 1));
  line-height: 1.35; color: var(--text-soft);
}
.impact__product {
  font-size: calc((11px) * var(--font-scale, 1));
  font-weight: 600; color: var(--text-soft);
}
/* Pushed to the bottom so fine print lines up across a row of uneven tiles. */
.impact__fine {
  margin-top: auto; padding-top: 2px;
  font-size: calc((12px) * var(--font-scale, 1));
  line-height: 1.35; color: var(--text-muted);
}

/* ---------- Media stacks --------------------------------------------------
   A single column at every width, deliberately: a 2-up masonry ran two
   unrelated screens side by side, which read as busy/confusing rather than
   as a sequence — this is a walkthrough of one product, not a wall of tiles.
   One column lets each screenshot be read fully before the next one arrives,
   the way you'd actually look at them.

   Full width of .wrap — flush on both edges, same as the tags, headings and
   body copy above it, rather than capped to its own narrower measure. .wrap's
   own max-width is already the page's tuned reading width at each breakpoint
   (940px desktop), so reusing it here is what keeps the gallery reading as
   part of the same column instead of a separate, arbitrarily-sized block
   floating in it. .shots--solo keeps its own narrower 560px cap below — a
   single image benefits from staying a comfortable size regardless of how
   wide the column runs, since there's no sequence to justify filling it. */
.shots {
  display: grid;
  gap: var(--gap-media);
  width: 100%;
}
/* Gallery fills the (now-narrowed) case column. .shots--solo keeps its own
   560px cap and is left out. */
.shot {
  width: 100%;
  background: transparent;
  /* The generator sets a precise border-radius inline per image — see the
     SHOT_RADIUS_RATIO note in shots()/img() in _tools/generate.py. This is
     only the fallback for a .shot rendered outside that path. */
  border-radius: var(--r-media);
  border: 1px solid var(--border-image);
}

.shots--solo .shot { width: auto; max-width: min(100%, 560px); margin: 0; }

/* Embedded video, matching the gallery's box: same radius, border and
   width path as a .shot, but a fixed 16/9 frame the iframe fills. */
.video-embed {
  /* Full content width in the clip's own aspect ratio (set inline per clip), so
     it fills the column horizontally with no letterbox. */
  position: relative; width: 100%;
  margin-inline: auto;
  aspect-ratio: 16 / 9;   /* fallback; overridden inline */
  border-radius: var(--r-video);
  overflow: hidden;
  background: #000;
}
/* Border as an overlay, not a box-model border: a 1px border on the frame shrinks
   the absolutely-positioned iframe by 1px per side, which shifts its aspect ratio
   just enough that Vimeo pillarboxes the video with thin dark bars. Drawn on
   ::after, the iframe fills the frame exactly and its aspect matches the clip. */
.video-embed::after {
  content: ""; position: absolute; inset: 0;
  border: 1px solid var(--border-image);
  border-radius: inherit;
  pointer-events: none;
}
  /* border comes from the shared gradient edge (::after), same as cards and the
     accessibility sheet. Fills the native-aspect frame exactly. */
.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #000; }

/* ---------- Scroll-driven motion -------------------------------------------
   Parallax only. The per-element fade/rise reveal that used to live here was
   removed: because it is scrubbed by the view() timeline, a programmatic scroll
   (view-swap entry jump, back-to-top tween) re-ran it on everything it passed,
   which read as chugging. Cards get their entrance from staggerCards() instead;
   everything else simply sits at rest. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* ---------- Parallax ----------------------------------------------------
       Two moves, both small enough that you feel them rather than see them.
       The lockup lags the scroll as the masthead leaves; the product hero
       drifts 4% of its own height across the whole viewport pass. Any more and
       it stops being depth and starts being a gimmick. */
    .masthead__lockup {
      animation: mast-lag linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
    .hero-shots img, .case-hero__img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes rise {
  from { opacity: 0; transform: translateY(var(--travel)); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

/* Staggered card entrance. Overrides the scroll-driven `rise` on .tile while it
   plays (higher specificity + a non-view() timeline); the router clears it on
   animationend, dropping each card back to the at-rest scroll timeline. */
.js .tile.is-fanning {
  animation: tile-in var(--dur-move) var(--spring) both;
  animation-delay: var(--fan, 0ms);
}
@keyframes tile-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .tile.is-fanning { animation: none; } }
@keyframes mast-lag { to { transform: translateY(22px); } }
@keyframes drift    { from { transform: translateY(2%); } to { transform: translateY(-2%); } }

/* Case-study foot navigation */
.case-nav {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 12px;
  margin-top: 40px;
}

/* =========================================================================
   Footer
   ========================================================================= */
.sitefoot {
  text-align: center;
  padding-block: 32px;
}

/* Legal/attribution line — deliberately quiet, but --gray-faint is the lightest
   grey on this site that still clears WCAG AA (4.5:1) at small sizes. */
.credit {
  margin-top: 40px;
  font-size: calc((11px) * var(--font-scale, 1));
  line-height: 1.5;
  color: var(--text-muted);
}
.sitefoot__lockup { display: grid; justify-items: center; gap: 16px; }
.sitefoot .logo { width: 52px; }
.sitefoot .tagline { font-size: calc((clamp(22px, 5.4vw, 28px)) * var(--font-scale, 1)); }
.sitefoot__legal { margin-top: 32px; font-size: calc((11px) * var(--font-scale, 1)); color: var(--text-muted); }
.sitefoot__links {
  margin-top: 12px;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 20px;
  font-size: calc((12px) * var(--font-scale, 1)); color: var(--text-muted);
}
.sitefoot__links a { transition: color var(--dur-hover) var(--ease); }
.sitefoot__links a:hover { color: var(--text); }

/* =========================================================================
   Legal / support pages
   ========================================================================= */
.doc { display: grid; gap: 28px; }
.doc .doc__title { font-size: calc((clamp(30px, 7vw, 44px)) * var(--font-scale, 1)); font-weight: 700; color: var(--text); line-height: 1.08; }
.doc h3 { font-size: calc((clamp(19px, 2.6vw, 24px)) * var(--font-scale, 1)); font-weight: 600; color: var(--text); margin-top: 12px; }
.doc p, .doc li { font-size: calc((16px) * var(--font-scale, 1)); color: var(--text); }
.doc ul { display: grid; gap: 8px; padding-left: 20px; list-style: disc; }
.doc a { color: var(--brand); text-decoration: underline; text-underline-offset: 3px; }
.doc .meta { font-size: calc((13px) * var(--font-scale, 1)); color: var(--text-muted); }
.doc .doc-q { font-size: calc((17px) * var(--font-scale, 1)); font-weight: 600; color: var(--text); margin-top: 8px; }
.doc .doc-q + p { margin-top: -18px; }

/* ---------- Masthead scale-up on larger canvases -------------------------
   The Sketch source ships a tablet lockup at 198×126; these steps walk the
   mobile 104×66 mark up to it so the brand keeps its presence on wide screens. */
@media (min-width: 768px) {
  .masthead { padding-block: 80px 40px; }
  .masthead .logo { width: 150px; }
  .sitefoot { padding-block: 48px; }
  .sitefoot .logo { width: 68px; }
}
@media (min-width: 1080px) {
  .masthead { padding-block: 96px 48px; }
  .masthead .logo { width: 198px; }
}

/* =========================================================================
   VARIANT B — project cards + the temporary A/B switch
   Only used by the /b/ build (see the VARIANT B section in generate.py).
   Delete this block along with that section to retire the experiment.
   ========================================================================= */

/* A card per archived project. Unlike the Apple/Chorus tiles — which frame a
   logo on white — these lead with the project's own thumbnail, the way the
   original michaelsquibb.com grid did, with the title and role underneath so
   twenty of them stay scannable. */
/* Rows get noticeably more air than columns — the grid was reading as one
   dense block, which made it hard to scan a row at a time. */
.tiles--proj { gap: 40px 16px; }
@media (min-width: 1080px) { .tiles--proj { gap: 56px 20px; } }

.tile--proj {
  display: block;
  padding: 0;
  min-height: 0;
  overflow: hidden;
  text-align: left;
}
.projcard__img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  background: var(--surface-track);
}
.projcard__meta {
  display: grid;
  gap: 2px;
  padding: 14px 16px 16px;
  /* The lines inside were already left-aligned, but the grid box itself was
     shrink-wrapping to the text and getting centred in the card. Stretch it
     across the card so the block starts at the same left edge as the image. */
  width: 100%;
  justify-items: start;
}
.projcard__name {
  font-size: calc((15px) * var(--font-scale, 1)); font-weight: 600; color: var(--text);
  letter-spacing: .01em; line-height: 1.25;
}
.projcard__sub {
  font-size: calc((12px) * var(--font-scale, 1)); color: var(--text-muted);
  letter-spacing: .02em; line-height: 1.3;
}
/* These cards have no .tile__mark, so cancel the generic mark zoom. */
.tile--proj:hover .tile__mark { transform: none; }

/* ---------- Apple-TV parallax ----------------------------------------------
   A flat photograph can't be split into real depth layers, but the tvOS feel
   comes from three things moving at different rates under a tilt, and those we
   do have: the card tilts toward the pointer, the picture slides against the
   tilt (so it reads as sitting behind the frame), a specular sheen tracks the
   pointer across the glass, and the caption lifts slightly toward it. --px/--py
   are the pointer position, -0.5..0.5, written by the router.

   Pointer-only: a coarse pointer gets none of this, since there is nothing to
   hover with and the carousel owns touch. */
@media (hover: hover) and (pointer: fine) {
  .tiles--proj { perspective: 900px; }
  .tile--proj {
    transform-style: preserve-3d;
    transition: transform var(--dur-move) var(--spring), box-shadow var(--dur-hover) var(--ease);
  }
  .tile--proj:hover {
    transform: rotateX(calc(var(--py, 0) * -6deg))
               rotateY(calc(var(--px, 0) * 8deg))
               translateY(var(--lift));
  }
  /* Layer 1 — the picture, sliding against the tilt and scaled just enough that
     the slide never exposes an edge. */
  .projcard__img {
    transform: translate3d(calc(var(--px, 0) * -14px), calc(var(--py, 0) * -10px), 0) scale(1.10);
  }
  /* Layer 2 — specular sheen, the giveaway that sells the glass. */
  .projcard__sheen {
    position: absolute; inset: 0;
    background: radial-gradient(75% 95% at calc(50% + var(--px, 0) * 60%) calc(50% + var(--py, 0) * 60%),
                rgba(255,255,255,.16), rgba(255,255,255,.06) 45%, rgba(255,255,255,0) 85%);
    opacity: 0;
    transition: opacity var(--dur-hover) var(--ease);
  }
  .tile--proj:hover .projcard__sheen { opacity: 1; }
  /* Layer 3 — the caption, nearest the viewer, so it moves least and with the tilt. */
  .tile--proj:hover .projcard__meta {
    transform: translate3d(calc(var(--px, 0) * 5px), calc(var(--py, 0) * 3px), 40px);
  }
}

.projcard__frame { position: relative; display: block; overflow: hidden; }
/* A card that shows a brand mark instead of a photo: same 16/9 frame, mark
   centred on a recessed panel, sized well within the edges. is-mono still
   inverts with the theme like the glyph elsewhere. */
.projcard__frame--mark {
  position: relative;
  display: grid; place-items: center;
  /* width:100% is load-bearing: with auto width the grid box sized to the logo's
     intrinsic 125px instead of filling the card, so the whole tile rendered
     short. It must match the photo frame's box exactly. */
  width: 100%;
  aspect-ratio: 16 / 9;
  /* A lighter panel than the page (which is --surface-sunken), so the mark reads
     as sitting in its own image area rather than dissolving into the background
     — most noticeable in dark mode. --surface-track is the "behind an image"
     token, so it is the right semantic here too. */
  background: var(--surface-track);
}
/* The sheen was a second grid item, so grid auto-flowed it into its own row and
   pushed the logo up out of centre. Take it out of flow. */
.projcard__frame--mark .projcard__sheen { position: absolute; inset: 0; }
.projcard__logo { display: grid; place-items: center; width: 34%; max-width: 120px; }
.projcard__logo img { width: 100%; height: auto; }
.projcard__sheen { pointer-events: none; }
.projcard__img,
.projcard__meta { transition: transform var(--dur-move) var(--spring); }
/* Touch and no-JS still get the plain zoom on press/focus. */
.tile--proj:focus-visible .projcard__img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  .tile--proj:hover, .tile--proj:hover .projcard__meta { transform: none; }
  .projcard__img { transform: none; }
  .projcard__sheen { display: none; }
}

/* ---------- A/B switch (temporary) --------------------------------------
   Fixed bottom-right, deliberately utilitarian — this is a testing control,
   not part of the design. Sits opposite the email FAB (top-right) so the two
   never collide. */
/* ---------- Pagination cluster + back-to-top (global nav chrome) --------
   All three fixed controls group on the RIGHT edge, stacked and equidistant
   (--fab-gap apart), top to bottom: scroll-top FAB, NEXT (›), PREV (‹). The
   whole cluster hangs off one shared anchor, --next-top, so it moves as a unit
   between breakpoints. Crucially each control is independently positioned by
   `top`, and the FAB fades in/out on opacity+transform WITHOUT taking layout
   space — so NEXT/PREV never shift when the scroll-top FAB appears. They wear
   the segmented control's colours: dark-grey glyph on the light-grey rail. */
/* Desktop: the wrapper is transparent to layout, so each control still
   positions itself on the right edge from its own `top`/`right`. Mobile turns it
   into a real bottom-centre dock (see the max-width block far below). */
.navdock { display: contents; }
.paddle {
  position: fixed; z-index: 55;
  right: var(--hand-r); left: var(--hand-l);
  transform: translateY(-50%);
  display: grid; place-items: center;
  width: 46px; height: 44px;                              /* seg-control height, arrow-only width */
  border-radius: var(--hand-radius);                      /* rounded on the inner side */
  background: var(--surface-track);
  color: var(--text);
  box-shadow: var(--sh-pill);
  transform-origin: var(--hand-origin);                   /* scale grows from the pinned edge */
  transition: color var(--dur-hover) var(--ease),
              transform var(--dur-move) var(--spring);
}
.paddle .icon { width: 22px; height: 22px; stroke-width: 2; transition: transform var(--dur-move) var(--spring); }
.paddle[hidden] { display: none; }
.paddle--next { top: var(--next-top); }
/* PREV sits one paddle-height (44) + one gap below NEXT's centre — the same gap
   the FAB sits above NEXT, so the three read as one evenly-spaced stack. */
.paddle--prev { top: calc(var(--next-top) + 44px + var(--fab-gap)); }
/* Rollover: the arrow leans inward, but the panel itself stays pinned to the
   edge — the background never leaves the right side of the window. */
.paddle:hover  { color: var(--text-strong); }
.paddle:hover .icon { transform: translateX(var(--hand-nudge)); }
.paddle:active { transform: translateY(-50%) scale(var(--press)); transition-duration: var(--dur-tap); }
/* Keyboard-driven press: the arrow keys trigger this so a page has the same
   confirmation as a tap — the panel dips and the arrow leans in the travel
   direction, then springs back when the class is removed. */
.paddle.paddle--pressing { color: var(--text-strong); transform: translateY(-50%) scale(var(--press)); transition-duration: var(--dur-tap); }
.paddle.paddle--pressing .icon { transform: translateX(var(--hand-nudge)); transition-duration: var(--dur-tap); }

/* Left-handed: flip the four hand tokens. The paddles/FAB read every edge,
   radius, origin and off-screen direction from these, so this is the whole
   change. A brief position transition makes an auto-switch mid-session slide
   across rather than jump. */
:root[data-hand="left"] {
  --hand-r: auto;
  --hand-l: 0px;
  --hand-radius: 0 var(--r-pill) var(--r-pill) 0;
  --hand-origin: 0% 50%;
  --hand-off: -100%;
  --hand-nudge: 3px;
}
.paddle, .scrolltop {
  transition: color var(--dur-hover) var(--ease),
              transform var(--dur-move) var(--spring),
              left var(--dur-enter) var(--spring),
              right var(--dur-enter) var(--spring);
}

/* On phones the same cluster drops into the lower-middle, where a right thumb
   naturally rests — nothing else changes, just the shared anchor. */
@media (max-width: 767px) {
  :root { --next-top: 60%; }
}

/* iOS Safari resolves a fixed element's `top: %` against a viewport whose height
   changes as the toolbar collapses on scroll — which slid the whole FAB cluster
   the moment you started scrolling. Pin the anchor to svh (the STABLE small-
   viewport height, toolbar-shown) so the value is a constant and can't move. The
   `%` above stays as the fallback for browsers without svh. */
@supports (top: 1svh) {
  :root { --next-top: 50svh; }
  @media (max-width: 767px) { :root { --next-top: 60svh; } }
}

.scrolltop {
  position: fixed; z-index: 55;
  /* Sits at the BOTTOM of the cluster, one --fab-gap below the prev paddle,
     sharing the --next-top anchor. prev's centre is (--next-top + 44 + gap); add
     its 22px half-height and one more gap to land this control's top edge at
     --next-top + 66 + 2·gap. Position lives in top/right; the transform below is
     reserved for the enter/leave animation. */
  right: var(--hand-r); left: var(--hand-l); bottom: auto;
  top: calc(var(--next-top) + 66px + var(--fab-gap) * 2);
  width: 46px; height: 44px;                              /* matches the paddles */
  display: grid; place-items: center;
  border: 0; padding: 0; cursor: pointer;
  border-radius: var(--hand-radius);                      /* same shape as the paddles */
  background: var(--surface-track);
  color: var(--text);
  box-shadow: var(--sh-pill);
  /* Resting = out: slid fully off the right edge. It arrives and leaves along
     this same horizontal path — out from / back into the window edge — with the
     house asymmetry: a longer sprung arrival, a quick accelerated exit. */
  transform-origin: var(--hand-origin);                   /* press-scale grows from the pinned edge */
  opacity: 0; transform: translateX(var(--hand-off));
  pointer-events: none;
  transition: opacity var(--dur-exit) var(--ease-exit),
              transform var(--dur-exit) var(--ease-exit),
              color var(--dur-hover) var(--ease);
}
.scrolltop .icon { width: 22px; height: 22px; stroke-width: 2; transition: transform var(--dur-move) var(--spring); }
.scrolltop:hover .icon { transform: translateY(-2px); }   /* rollover: arrow lifts toward the top, panel stays pinned to the edge */
.scrolltop.is-in {
  opacity: 1; transform: none; pointer-events: auto;
  transition: opacity var(--dur-enter) var(--ease),
              transform var(--dur-enter) var(--spring),
              color var(--dur-hover) var(--ease);
}
.scrolltop:hover  { color: var(--text-strong); }
.scrolltop:active { transform: scale(var(--press)); transition-duration: var(--dur-tap); }

@media (prefers-reduced-motion: reduce) {
  .scrolltop, .scrolltop.is-in { transform: none; transition: opacity var(--dur-hover) linear; }
  .paddle, .paddle:hover, .paddle:active, .paddle.paddle--pressing { transform: translateY(-50%); transition: color var(--dur-hover) linear; }
}

/* ---------- Mobile: bottom-centre dock -----------------------------------
   The three controls leave the right edge and become one floating group,
   centred at the bottom of the screen and hovering above the content. prev and
   next sit --fab-gap apart; the scroll-top button lives between them but
   collapses to zero width until you scroll, so at the top of a page it reads as
   a clean pair of arrows and then the up-arrow grows in between. Handedness no
   longer applies here — a centred dock is neutral — so the controls take a full
   pill radius on both sides. Bottom offset is a spacing token plus the safe
   area. */
@media (max-width: 767px) {
  .navdock {
    display: flex;
    align-items: center;
    gap: 0;                                    /* spacing is carried by the margins below */
    position: fixed; z-index: 55;
    left: 50%;
    bottom: calc(var(--pad) / 2 + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
  }
  /* Both arrows: static in the flex row, full pill, each nudged toward centre by
     half a gap so an empty middle reads as one --fab-gap between them. */
  .navdock .paddle {
    position: static; top: auto; right: auto; left: auto;
    transform: none;
    border-radius: var(--r-pill);
  }
  /* DOM order is next / scroll-top / prev (for keyboard focus); flex order
     restores the visual ‹ ↑ › so prev stays on the left, next on the right. */
  .navdock .paddle--prev { order: 0; margin-right: calc(var(--fab-gap) / 2); }
  .navdock .scrolltop    { order: 1; }
  .navdock .paddle--next { order: 2; margin-left: calc(var(--fab-gap) / 2); }
  .navdock .paddle:active,
  .navdock .paddle.paddle--pressing { transform: scale(var(--press)); }
  /* Rollover nudge is a hover affordance; on touch it just leans the glyph, keep
     it but it's harmless. Paddle enter/leave no longer slides horizontally. */

  /* Scroll-top: the middle slot. Out = zero width, no margin, so prev/next sit
     together. In = full width with half-gap margins each side, growing the
     button in and pushing the arrows apart. */
  .navdock .scrolltop {
    position: static; top: auto; right: auto; left: auto;
    width: 0; min-width: 0; padding: 0; margin-inline: 0;
    overflow: hidden;
    border-radius: var(--r-pill);
    opacity: 0; transform: none; pointer-events: none;
    transition: width var(--dur-enter) var(--spring),
                margin var(--dur-enter) var(--spring),
                opacity var(--dur-enter) var(--ease),
                color var(--dur-hover) var(--ease);
  }
  .navdock .scrolltop.is-in {
    width: 46px; margin-inline: calc(var(--fab-gap) / 2);
    opacity: 1; transform: none; pointer-events: auto;
  }
  .navdock .scrolltop:active { transform: scale(var(--press)); }

  @media (prefers-reduced-motion: reduce) {
    .navdock .scrolltop { transition: opacity var(--dur-hover) linear, width var(--dur-hover) linear, margin var(--dur-hover) linear; }
  }

  /* The dock floats over the bottom of every page, so lift the footer clear of
     it: the dock's own inset (--pad/2, matching its `bottom` above) + its 44px
     height + a breathing gap + the safe area. Otherwise the footer links sit
     under the buttons. */
  .sitefoot {
    padding-bottom: calc(var(--pad) / 2 + 44px + 24px + env(safe-area-inset-bottom, 0px));
  }
}


/* Theme toggle + brand-hue picker, stacked above the other dev chips. */
.themeswitch {
  position: fixed;
  left: clamp(16px, 4vw, 32px);
  bottom: calc(clamp(16px, 4vw, 32px) + 80px);
  z-index: 60;
  padding: 7px 12px;
  border: 0; cursor: pointer;
  border-radius: var(--r-pill);
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  color: var(--chip-fg);
  font: inherit; font-size: calc((12px) * var(--font-scale, 1)); font-weight: 700; letter-spacing: .06em;
  box-shadow: var(--sh-lift);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-move) var(--ease),
              transform var(--dur-move) var(--spring);
}
.themeswitch:active { transform: scale(var(--press)); transition-duration: var(--dur-tap); }
.themeswitch.is-revealed { opacity: 1; pointer-events: auto; }

.huepick {
  position: fixed;
  left: clamp(16px, 4vw, 32px);
  bottom: calc(clamp(16px, 4vw, 32px) + 120px);
  z-index: 60;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px 7px 9px;
  border-radius: var(--r-pill);
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: var(--sh-lift);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-move) var(--ease);
}
.huepick.is-revealed { opacity: 1; pointer-events: auto; }
.huepick__sw {
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--brand);
  box-shadow: 0 0 0 2px rgba(128,128,128,.28);
  flex: none;
}
.huepick__range { width: 92px; accent-color: var(--brand); }

.handswitch {
  position: fixed;
  left: clamp(16px, 4vw, 32px);
  bottom: calc(clamp(16px, 4vw, 32px) + 160px);
  z-index: 60;
  padding: 7px 12px;
  border: 0; cursor: pointer;
  border-radius: var(--r-pill);
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  color: var(--chip-fg);
  font: inherit; font-size: calc((12px) * var(--font-scale, 1)); font-weight: 700; letter-spacing: .06em;
  box-shadow: var(--sh-lift);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-move) var(--ease), transform var(--dur-move) var(--spring);
}
.handswitch::before { content: "\1F44B  "; }
.handswitch:active { transform: scale(var(--press)); transition-duration: var(--dur-tap); }
.handswitch.is-revealed { opacity: 1; pointer-events: auto; }
