/* ============================================================
   DRAFT0 , SHARED SITE STYLESHEET
   Every page links to this one file for design tokens (colors,
   fonts, spacing) and shared component styles (nav, buttons,
   cards, footer, etc). Change something here and it updates
   everywhere at once. Pages only carry their own <style> block
   for genuinely page-specific layout (e.g. index.html's unique
   hero, or a project page's own brand-visualiser system).

   CACHE-BUSTING: every page loads this file as
   "assets/css/base.css?v=N" (same for assets/js/site.js). Phones
   in particular cache these aggressively against that exact URL,
   so after editing this file, bump the "?v=N" number in EVERY
   HTML page's <link>/<script> tag (a find-and-replace across all
   pages) , otherwise visitors' browsers may keep serving the old
   cached copy indefinitely, even after re-uploading.
   ============================================================ */


/* ============================================================
   DRAFT0 , DESIGN TOKENS
   [BRAND] vars below are placeholders , swap once real branding
   is locked. LIGHT and DARK share the same token names, so
   nothing downstream needs to change when you edit them.
   ============================================================ */
:root{
  --font-display: 'IBM Plex Mono', monospace;
  --font-mono:    'Stack Sans Text', sans-serif;
  --ease: cubic-bezier(.16,.84,.32,1);
  --container: 1280px;
  --gutter: clamp(20px, 5vw, 64px);
  --radius: 2px;
  --radius-dock: 999px;
}

body[data-theme="light"]{
  --paper:      #F1F2EC;
  --paper-line: #D8DACD;
  --ink:        #17160F;
  --ink-soft:   #5B5A4F;
  --accent:     #FF4B21;
  --accent-ink: #17160F;
  --accent-text:#C23A1B;
}
body[data-theme="dark"]{
  --paper:      #131209;
  --paper-line: #29271B;
  --ink:        #F1F0E9;
  --ink-soft:   #8D8B7E;
  --accent:     #FF6A3D;
  --accent-ink: #17160F;
  --accent-text:#FF6A3D;
}

/* ============================================================
   BACKGROUND PATTERN , quick controls
   These feed the "blueprint grid" background further down (search
   for BACKGROUND PATTERN again). Change values here to retune it
   sitewide without touching the pattern rules themselves.
   ============================================================ */
:root{
  /* ---- ONE-LINE GRID CONTROL ----
     Change either of these two values to rescale/fade the ENTIRE
     blueprint grid (minor lines, major lines, and ticks together)
     sitewide, on every page except project.html. Nothing else below
     needs to be touched. project.html uses a separate dot grid with
     its own --dot-* controls , see DOT GRID CONTROL further down.

     NOTE ON UNITS: --grid-opacity and the --bg-*-opacity/--tick-
     opacity knobs below are written as plain unitless NUMBERS
     (0–1), not percentages (0%–100%), even though every other
     opacity-ish value in this file uses %. That's deliberate:
     multiplying two <percentage> values together inside calc() (as
     the old version of this file did , calc(50% * 75% / 100%)) does
     not reliably resolve to a valid <percentage> per the CSS Values
     spec, and at least one major browser drops the ENTIRE
     background-image declaration as invalid when it doesn't ,
     taking the whole grid (and everything layered with it, since
     it's one comma-separated background-image list) down with it,
     with no visible error beyond "the grid is just gone". Plain
     numbers multiply/divide unambiguously in calc(), so the knobs
     below are numbers, and % is appended only once, at the point
     each is actually plugged into a color-mix() call (search for
     "* 100%)" below to see where). */
  --grid-scale: 0.75;      /* multiplies every spacing , 0.5 = tighter/denser grid, 2 = looser/sparser */
  --grid-opacity: 0.5;  /* 0–1 , multiplies every layer's strength together */

  --bg-pattern-size: calc(40px * var(--grid-scale));      /* spacing between minor lines */

  /* ---- PER-LAYER OPACITY CONTROL ----
     Each layer's opacity is expressed relative to --grid-opacity
     above, so the one-line control still fades everything together,
     but you can also independently dial any single layer up/down
     from there. 1 = as strong as --grid-opacity allows; lower
     values fade just that layer further. */
  --bg-minor-opacity: 1;  /* minor (fine) grid lines, relative to --grid-opacity */
  --bg-major-opacity: 0.75;  /* major (bold index) grid lines, relative to --grid-opacity */
  --tick-opacity: 0.75;      /* "+" cross-hairs, relative to --grid-opacity */

  /* unitless product first, %  appended once at the end , see the
     NOTE above for why this order matters in Firefox. */
  --bg-pattern-opacity: calc(var(--grid-opacity) * var(--bg-minor-opacity) * 100%);
  --bg-major-opacity-resolved: calc(var(--grid-opacity) * var(--bg-major-opacity) * 100%);
  --tick-opacity-resolved: calc(var(--grid-opacity) * var(--tick-opacity) * 100%);

  /* blueprint vellum: a heavier "major" grid drawn every Nth minor
     line, like real drafting paper (fine grid + bold index lines).
     --bg-major-every is unitless (a line count), the rest follow
     the same colour/opacity/size pattern as the minor grid above. */
  --bg-major-every: 4;
  --bg-major-size: calc(var(--bg-pattern-size) * var(--bg-major-every));

  /* ---- TICK POSITION + SIZE CONTROL ----
     Nudges/resizes the "+" cross-hairs independently of the grid
     lines underneath them.
     --tick-offset-x/y: 0px 0px keeps ticks exactly on the
       intersections (current default). Positive x moves ticks
       right, positive y moves them down; values wrap every
       --bg-major-size, so e.g. 100px is a half-step at the default
       200px major size.
     --tick-size: arm-to-arm length of the cross (its total width
       and height). Default 14px reads as a small index mark; try
       6px for a barely-there dot-like mark or 40px for something
       much more prominent.
     --tick-thickness: stroke width of the cross's two bars. */
  --tick-offset-x: 60.5px;
  --tick-offset-y: 60.5px;
  --tick-size: 14px;
  --tick-thickness: 2px;

  /* ---- DOT GRID CONTROL (project.html) ----
     Independent from the line-grid controls above, so retuning the
     dots never affects the blueprint grid on every other page.
     --dot-scale/--dot-opacity follow the same one-line pattern as
     --grid-scale/--grid-opacity , --dot-opacity is a unitless
     number for the same calc()-with-percentages reason explained in
     the NOTE ON UNITS above. */
  --dot-scale: 1.5;       /* multiplies dot spacing , 0.5 = denser, 2 = sparser */
  --dot-opacity: 0.75;   /* 0–1 , dot strength */
  --dot-radius: 2px;   /* size of each dot */

  --dot-size: calc(40px * var(--dot-scale));   /* spacing between dots */
  --dot-opacity-resolved: calc(var(--dot-opacity) * 100%);

}
body[data-theme="light"]{
  --bg-pattern-color: var(--paper-line); /* pattern COLOUR (light) , override with any colour, e.g. #D8DACD */
  --bg-major-color: color-mix(in srgb, var(--ink) 22%, var(--paper-line));
  --dot-color: var(--paper-line); /* dot COLOUR (light) , independent from --bg-pattern-color */
}
body[data-theme="dark"]{
  --dot-color: var(--paper-line); /* dot COLOUR (dark) */
  --bg-pattern-color: var(--paper-line); /* pattern COLOUR (dark) */
  --bg-major-color: color-mix(in srgb, var(--ink) 22%, var(--paper-line));
}

*{ box-sizing: border-box; margin:0; padding:0; }

/* ---- accessibility utilities ---- */
:focus-visible{ outline: 2px solid var(--accent-text); outline-offset: 2px; border-radius: 2px; }
.sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
.skip-link{
  position:absolute; top:-100px; left:12px; z-index:1000;
  background: var(--ink); color: var(--paper);
  font-family: var(--font-mono); font-size:13px; padding:12px 18px;
  border-radius: var(--radius); text-decoration:none;
  transition: top .2s var(--ease);
}
.skip-link:focus{ top:12px; }
html{ scroll-behavior: smooth; overflow-x: clip; position:relative; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior: auto; } }

body{
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-mono);
  line-height: 1.4;
  overflow-x: clip;
  /* html and body both need to block horizontal overflow (see the
     earlier double-scrollbar fix note below for why), but
     overflow-x:hidden has a side effect: per spec, when
     overflow-x and overflow-y computed values disagree on
     "visible vs not", the visible one is silently forced to "auto"
     , turning html AND body into two separate scrollable boxes on
     top of the normal window scrollbar (a double-scrollbar bug).
     overflow-x:clip is exempt from that pairing rule, so overflow-y
     stays genuinely visible/normal , single viewport scrollbar,
     horizontal overflow still blocked. */
  transition: background .4s var(--ease), color .4s var(--ease);
  min-height: 100vh; /* fallback for browsers that don't support dvh */
  min-height: 100dvh;
  display:flex;
  flex-direction:column;

  /* ============================================================
     BACKGROUND PATTERN , "blueprint vellum": a literal drafting-
     table reference, always on and fully static (no cursor
     reaction, no animation). Layers, thinnest-drawn-first:

       1. minor grid   , fine 1px lines every --bg-pattern-size
       2. major grid   , heavier lines every --bg-major-every minor
                         lines, like the bold index rulings on real
                         graph paper
       3. corner ticks , small "+" cross-hairs at every major
                         intersection (see body::after below)

     Colour + opacity + spacing come from the --bg-pattern-* and
     --bg-major-* variables above (in :root / [data-theme]) , retune
     the whole site from there without touching this block.

     project.html overrides this with a dot-grid variant instead ,
     see the .bg-dots rules further down.

     The old single-flat-grid version (STYLE 1/2/3 toggle) is kept
     below, commented out, in case you want to drop back to it.
     ============================================================ */

  background-image:
    /* 2. major grid , bold index lines */
    linear-gradient(color-mix(in srgb, var(--bg-major-color) var(--bg-major-opacity-resolved), transparent) 1px, transparent 1px),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-major-color) var(--bg-major-opacity-resolved), transparent) 1px, transparent 1px),
    /* 1. minor grid , fine lines */
    linear-gradient(color-mix(in srgb, var(--bg-pattern-color) var(--bg-pattern-opacity), transparent) 1px, transparent 1px),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-pattern-color) var(--bg-pattern-opacity), transparent) 1px, transparent 1px);
  background-size:
    var(--bg-major-size) var(--bg-major-size),
    var(--bg-major-size) var(--bg-major-size),
    var(--bg-pattern-size) var(--bg-pattern-size),
    var(--bg-pattern-size) var(--bg-pattern-size);
  background-position: 0 0, 0 0, 0 0, 0 0;
}

/* 3. corner ticks , a small "+" cross-hair at every major-grid
   intersection, split across two pseudo-elements (::before for the
   vertical arm, ::after for the horizontal arm).

   Why two elements instead of one: a data-URI SVG mask can't read
   CSS custom properties, so a fixed-geometry SVG can't expose a
   live --tick-size control , the cross has to be built from plain
   gradients instead. Each arm needs to be short on BOTH axes: thin
   (--tick-thickness, the stroke width) and short (--tick-size, the
   arm length). A single linear-gradient only varies along one axis,
   so getting both requires pairing a background-image gradient
   (constrains thickness) with a mask-image gradient (constrains
   length, perpendicular to the first).

   That pairing has to live on its OWN element per arm: CSS unions
   multiple mask-image layers into a single combined mask before
   applying it to the whole background stack, rather than pairing
   mask layers 1:1 with background layers. Putting both arms' masks
   on one element made each arm's mask leak onto the other arm's
   background, which reconstructed full-length grid lines instead of
   short crosses. Two elements , one mask each , avoids that.

   Turned off on project.html , see .bg-dots overrides below.

   position:absolute (not fixed) on both is deliberate: body's own
   background-image scrolls with the page's normal document flow
   (it's not fixed either), so these layers have to match that same
   scrolling behaviour or the ticks drift away from the grid
   intersections as soon as you scroll. html is the positioned
   ancestor (via position:relative above) so these span the full
   scrollable document, not just one viewport-height. */
body::before{
  content:"";
  position:absolute; inset:0; z-index:-1; pointer-events:none;
  /* vertical arm: thin column (--tick-thickness wide) */
  background-image: linear-gradient(
    90deg,
    transparent calc(50% - var(--tick-thickness) / 2),
    color-mix(in srgb, var(--bg-major-color) var(--tick-opacity-resolved), transparent) calc(50% - var(--tick-thickness) / 2),
    color-mix(in srgb, var(--bg-major-color) var(--tick-opacity-resolved), transparent) calc(50% + var(--tick-thickness) / 2),
    transparent calc(50% + var(--tick-thickness) / 2)
  );
  background-size: var(--bg-major-size) var(--bg-major-size);
  background-repeat: repeat;
  background-position: var(--tick-offset-x) var(--tick-offset-y);
  /* mask clips the arm's LENGTH , varies vertically, perpendicular
     to the background gradient's own (horizontal) thickness axis. */
  -webkit-mask-image: linear-gradient(
    transparent calc(50% - var(--tick-size) / 2),
    black calc(50% - var(--tick-size) / 2),
    black calc(50% + var(--tick-size) / 2),
    transparent calc(50% + var(--tick-size) / 2)
  );
  mask-image: linear-gradient(
    transparent calc(50% - var(--tick-size) / 2),
    black calc(50% - var(--tick-size) / 2),
    black calc(50% + var(--tick-size) / 2),
    transparent calc(50% + var(--tick-size) / 2)
  );
  -webkit-mask-size: var(--bg-major-size) var(--bg-major-size);
  mask-size: var(--bg-major-size) var(--bg-major-size);
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-position: var(--tick-offset-x) var(--tick-offset-y);
  mask-position: var(--tick-offset-x) var(--tick-offset-y);
}
body::after{
  content:"";
  position:absolute; inset:0; z-index:-1; pointer-events:none;
  /* horizontal arm: thin row (--tick-thickness tall) */
  background-image: linear-gradient(
    transparent calc(50% - var(--tick-thickness) / 2),
    color-mix(in srgb, var(--bg-major-color) var(--tick-opacity-resolved), transparent) calc(50% - var(--tick-thickness) / 2),
    color-mix(in srgb, var(--bg-major-color) var(--tick-opacity-resolved), transparent) calc(50% + var(--tick-thickness) / 2),
    transparent calc(50% + var(--tick-thickness) / 2)
  );
  background-size: var(--bg-major-size) var(--bg-major-size);
  background-repeat: repeat;
  background-position: var(--tick-offset-x) var(--tick-offset-y);
  /* mask clips the arm's LENGTH , varies horizontally, perpendicular
     to the background gradient's own (vertical) thickness axis. */
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent calc(50% - var(--tick-size) / 2),
    black calc(50% - var(--tick-size) / 2),
    black calc(50% + var(--tick-size) / 2),
    transparent calc(50% + var(--tick-size) / 2)
  );
  mask-image: linear-gradient(
    90deg,
    transparent calc(50% - var(--tick-size) / 2),
    black calc(50% - var(--tick-size) / 2),
    black calc(50% + var(--tick-size) / 2),
    transparent calc(50% + var(--tick-size) / 2)
  );
  -webkit-mask-size: var(--bg-major-size) var(--bg-major-size);
  mask-size: var(--bg-major-size) var(--bg-major-size);
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
  -webkit-mask-position: var(--tick-offset-x) var(--tick-offset-y);
  mask-position: var(--tick-offset-x) var(--tick-offset-y);
}

/* ---- project pages , dot grid instead of blueprint vellum ----
   Applied via <body class="bg-dots"> (see project.html). Swaps the
   line+major+tick background for a single even dot grid, and turns
   off both corner-tick pseudo-elements so it doesn't need
   overriding layer-by-layer. Controlled independently via the
   --dot-* variables in :root , see DOT GRID CONTROL above. */
body.bg-dots{
  background-image: radial-gradient(color-mix(in srgb, var(--dot-color) var(--dot-opacity-resolved), transparent) var(--dot-radius), transparent var(--dot-radius));
  background-size: var(--dot-size) var(--dot-size);
  background-position: 0 0;
}
body.bg-dots::before{ content: none; }
body.bg-dots::after{ content: none; }

/* ---- previous flat-grid implementation (kept for easy revert) ----
body{
  background-image:
    linear-gradient(color-mix(in srgb, var(--bg-pattern-color) var(--bg-pattern-opacity), transparent) 1px, transparent 1px),
    linear-gradient(90deg, color-mix(in srgb, var(--bg-pattern-color) var(--bg-pattern-opacity), transparent) 1px, transparent 1px);
  background-size: var(--bg-pattern-size) var(--bg-pattern-size);
}
STYLE 2 , dot grid
body{
  background-image: radial-gradient(color-mix(in srgb, var(--bg-pattern-color) var(--bg-pattern-opacity), transparent) 1.5px, transparent 1.5px);
  background-size: var(--bg-pattern-size) var(--bg-pattern-size);
}
STYLE 3 , diagonal lines
body{
  background-image: repeating-linear-gradient(45deg, color-mix(in srgb, var(--bg-pattern-color) var(--bg-pattern-opacity), transparent) 0 1px, transparent 1px var(--bg-pattern-size));
}
---------------------------------------------------------------- */

main{ flex:1; }

/* custom cursor , draft0-style: an orange ring + center point,
   echoing the "0" brand mark, with the coordinates readout tagging
   along. Scales up slightly over interactive elements (see
   ".is-hovering" toggled in site.js), and presses down slightly on
   click (".is-clicking") for a bit of tactile feedback.
   The "*{cursor:none}" below is deliberately a blanket override ,
   several elements (buttons, .logo-fold, etc.) set their own
   "cursor:pointer", which has higher specificity than a plain
   "body{cursor:none}" and would otherwise let the native OS hand
   cursor show up alongside our ring. Forcing it everywhere keeps
   the ring the *only* cursor, consistently, on every page. */
@media (pointer:fine){
  *{ cursor: none !important; }
  .cursor, .cursor-coords{ position: fixed; top:0; left:0; pointer-events:none; z-index:9999; }
  .cursor{ width:22px; height:22px; transform: translate(-50%,-50%); color: var(--accent); will-change: transform; }
  .cursor svg{ width:100%; height:100%; overflow:visible; transition: transform .2s var(--ease); }
  .cursor.is-hovering svg{ transform: scale(1.5); }
  .cursor.is-clicking svg{ transform: scale(.8); }
  .cursor.is-hovering.is-clicking svg{ transform: scale(1.15); }
  .cursor-ring{ fill:none; stroke: currentColor; stroke-width:1.5; opacity:.9; }
  .cursor-dot{ fill: currentColor; }
  .cursor-coords{ font-family: var(--font-mono); font-size:10px; color: var(--ink-soft); transform: translate(14px,14px); white-space:nowrap; opacity:0; transition: opacity .2s var(--ease); }
  /* hidden while the pointer is over a cross-origin iframe (YouTube/
     Vimeo/Figma embeds) , see the mouseover/mouseout handling in
     site.js for why: our ring can't track movement happening inside
     someone else's document, so it just disappears instead of
     sitting frozen at the edge looking broken. */
  .cursor.cursor-suppressed, .cursor-coords.cursor-suppressed{ opacity:0 !important; }
}
@media (pointer:coarse), (prefers-reduced-motion: reduce){ .cursor, .cursor-coords{ display:none !important; } }

.container{ max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }

/* ============================================================
   NAV , collapsing dock, anchored LEFT
   ============================================================ */
.nav-dock{
  position: fixed; top: 20px; left: 20px; z-index: 300;
  display:flex; align-items:center; height: 52px;
  background: color-mix(in srgb, var(--paper) 90%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid color-mix(in srgb, var(--ink) 35%, transparent);
  border-radius: var(--radius-dock);
  padding: 0 5px;
  transition: background .4s var(--ease), border-color .4s var(--ease);
  animation: dockInLeft .6s var(--ease) backwards;
  /* NOT "both" , "both" keeps the animation's final keyframe state
     (transform: translateY(0)) applied forever after it finishes.
     Even though that's visually a no-op, ANY non-"none" transform
     on an ancestor makes it the positioning root for position:fixed
     descendants , which silently broke the mobile nav drawer's
     left:20px/right:20px (they were being measured against this
     52px-tall dock instead of the real viewport). "backwards" alone
     still prevents a flash-of-unstyled-content before the animation
     starts, without leaving that transform behind afterwards. */
}
.nav-collapsible{
  display:flex; align-items:center; gap: 2px; max-width: 640px; overflow:hidden;
  white-space:nowrap; opacity:1;
  transition: max-width .5s var(--ease), opacity .3s var(--ease), margin .5s var(--ease);
  margin-left: 6px;
}
/* desktop-only: shrink the dock to just the logo after scrolling
   past the hero, expand again on hover/focus/tap (.force-expand).
   Scoped to min-width so it never fights with the mobile drawer
   treatment below 641px, which uses its own open/close mechanism
   (.nav-dock.mobile-open) instead of hover/scroll state. */
@media (min-width:641px){
  body.scrolled .nav-dock:not(:hover):not(:focus-within):not(.force-expand) .nav-collapsible{
    max-width: 0; opacity: 0; margin-left: 0;
  }
}
.nav-links{ display:flex; list-style:none; gap:2px; }
.nav-links a{
  position:relative; display:flex; align-items:baseline; gap:6px;
  padding: 8px 12px; text-decoration:none; color: var(--ink);
  font-size: 13px; font-weight:500; border-radius: 999px;
  transition: transform .2s var(--ease);
}
@media (hover:hover){ .nav-links a:hover{ transform: translateY(-2px); } }
.nav-links .tag{ font-family: var(--font-mono); font-size:10px; color: var(--ink-soft); }
.nav-links a::after{
  content:''; position:absolute; left:12px; right:12px; bottom:4px; height:1.5px;
  background: var(--accent-text); transform: scaleX(0); transform-origin:left; transition: transform .35s var(--ease);
}
@media (hover:hover){ .nav-links a:hover::after{ transform: scaleX(1); } }
.nav-links a:focus-visible::after{ transform: scaleX(1); }
.nav-links a[aria-current="page"]::after{ transform: scaleX(1); }
.nav-links a[aria-current="page"] .tag{ color: var(--accent-text); }
.logo-tab{
  flex-shrink:0; width:42px; height:42px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background: var(--ink); color: var(--paper); font-weight:700; font-size:15px;
  text-decoration:none; transition: background .4s var(--ease), color .4s var(--ease), transform .25s var(--ease);
}
@media (hover:hover){ .logo-tab:hover{ transform: rotate(-14deg) scale(1.06); } }
/* ---- logo mark: the two SVG files as provided (no recolouring),
   layered on top of each other and swapped with plain CSS based on
   the theme attribute , badge background follows --ink (dark in
   light theme, light in dark theme), so the white file shows in
   light mode and the black file shows in dark mode. ---- */
.logo-tab .logo-mark{ width:16px; height:auto; display:block; }
.logo-tab .logo-mark-dark{ display:none; }
body[data-theme="dark"] .logo-tab .logo-mark-light{ display:none; }
body[data-theme="dark"] .logo-tab .logo-mark-dark{ display:block; }
/* ============================================================
   MOBILE NAV , hamburger + dropdown drawer, replacing the desktop
   hover/scroll-collapse behaviour entirely below 641px. The drawer
   reuses the exact same .nav-collapsible/.nav-links markup as
   desktop (no duplicated link list) , .nav-dock.mobile-open (set by
   the hamburger click handler in site.js) is the only thing that
   opens it. Icon swap (hamburger <-> ×) is done in JS via the
   `hidden` attribute directly on each <svg>, not CSS descendant
   selectors , simpler to reason about and nothing to get wrong.
   ============================================================ */
.nav-hamburger{
  display:none; /* desktop default: hidden. The ONLY place this
    becomes visible is the max-width:640px block below , if it's
    ever showing above that width, something is overriding this
    rule, not the other way around. */
  align-items:center; justify-content:center;
  width:38px; height:38px; margin-left:2px; flex-shrink:0;
  background:transparent; border:none; border-radius:50%; cursor:pointer;
  color:var(--ink); transition: background .2s var(--ease);
}
.nav-hamburger svg{ display:block; pointer-events:none; } /* JS sets each icon's own inline display:none/block directly , see setNavMobileOpen() in site.js */
@media (hover:hover){ .nav-hamburger:hover{ background: rgba(127,127,127,.14); } }

/* Belt-and-suspenders: force it hidden above the breakpoint no
   matter what else loads later in the cascade (e.g. a page-specific
   inline <style> block, which , being later in the document , would
   otherwise be able to override the plain rule above). */
@media (min-width:641px){
  .nav-hamburger{ display:none !important; }
}

@media (max-width: 640px){
  /* backdrop-filter (like transform) makes an element the
     positioning root for any position:fixed descendants , .nav-dock
     has backdrop-filter:blur(8px) permanently (not just during the
     entrance animation), which was ALSO forcing .nav-collapsible's
     fixed positioning to measure against this ~90px pill instead of
     the real viewport, even after the animation fix. Turning it off
     here (mobile only , desktop keeps its blur) removes that
     entirely. The drawer below has its own backdrop-filter anyway,
     so the frosted-glass look isn't lost, just moved. */
  .nav-dock{ backdrop-filter:none; }
  .nav-hamburger{ display:flex; width:44px; height:44px; }
  .nav-collapsible{
    /* position:fixed with explicit left+right (not width:100vw or
       any vw-based calc()) , this is deliberate: 100vw famously
       includes the scrollbar's own width in several browsers, which
       made this drawer a few pixels wider than the true visible
       viewport and forced a horizontal scrollbar site-wide. left+
       right insets are always measured against the real visible
       viewport, so the browser computes a correct width with no vw
       math needed at all. */
    position:fixed; top:82px; left:20px; right:20px;
    width:auto; max-width:none; white-space:normal;
    max-height:0; opacity:0; overflow:hidden; pointer-events:none;
    margin-left:0; flex-direction:column; align-items:stretch;
    background: color-mix(in srgb, var(--paper) 97%, transparent);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border:1px solid var(--ink); border-radius:16px; padding:0 6px;
    transition: max-height .35s var(--ease), opacity .25s var(--ease), padding .35s var(--ease);
  }
  .nav-dock.mobile-open .nav-collapsible{
    max-height:70vh; opacity:1; pointer-events:auto; padding:6px;
  }
  .nav-links{ display:flex; flex-direction:column; gap:2px; width:100%; }
  .nav-links a{ padding:14px 16px; width:100%; border-radius:12px; }
  .nav-links a::after{ display:none; } /* underline indicator reads oddly stacked vertically , current page uses a background fill instead, below */
  .nav-links a[aria-current="page"]{ background: color-mix(in srgb, var(--ink) 8%, transparent); }
}

/* ============================================================
   TOGGLE , standalone dock, anchored RIGHT, always visible
   ============================================================ */
.toggle-dock{
  position: fixed; top: 20px; right: 20px; z-index: 300;
  display:flex; align-items:center; gap:6px; height: 52px;
  background: color-mix(in srgb, var(--paper) 90%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid color-mix(in srgb, var(--ink) 35%, transparent);
  border-radius: var(--radius-dock);
  padding: 5px;
  transition: background .4s var(--ease), border-color .4s var(--ease);
  animation: dockInRight .6s var(--ease) backwards; /* not "both" , see .nav-dock's comment above */
}
@keyframes dockInLeft{ from{ opacity:0; transform: translateY(-14px); } to{ opacity:1; transform: translateY(0); } }
@keyframes dockInRight{ from{ opacity:0; transform: translateY(-14px); } to{ opacity:1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce){ .nav-dock, .toggle-dock{ animation:none !important; } }
.mode-toggle{ display:flex; align-items:center; background: rgba(127,127,127,.14); border-radius: 999px; padding: 4px; gap: 2px; }
.mode-btn{
  display:flex; align-items:center; gap:6px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing:.02em; padding: 8px 14px;
  border:none; background: transparent; color: var(--ink-soft); border-radius: 999px; cursor: pointer;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.mode-btn svg{ display:block; flex-shrink:0; }
.mode-btn.active{ background: var(--ink); color: var(--paper); }
.mute-btn{
  width:34px; height:34px; flex-shrink:0; border-radius:50%; border:none; background:transparent;
  color: var(--ink-soft); display:flex; align-items:center; justify-content:center; cursor:pointer;
  transition: background .2s var(--ease), color .2s var(--ease);
}
@media (hover:hover){ .mute-btn:hover{ background: rgba(127,127,127,.14); color: var(--ink); } }
.mute-btn svg{ display:block; }
.mute-btn .icon-off{ display:none; }
.mute-btn.is-muted .icon-on{ display:none; }
.mute-btn.is-muted .icon-off{ display:block; }


/* ============================================================
   HERO (home)
   ============================================================ */
.hero{
  position:relative; min-height: 92vh; min-height: 92dvh; display:grid;
  grid-template-columns: 1.15fr 0.85fr; align-items:center;
  padding: 120px var(--gutter) 60px; max-width: var(--container); margin: 0 auto; gap: 40px;
}
@media (max-width: 900px){ .hero{ grid-template-columns: 1fr; padding-top: 110px; } }

.hero-eyebrow{ display:inline-flex; align-items:center; gap:8px; font-family: var(--font-mono); font-size:11px; color: var(--ink-soft); margin-bottom: 22px; letter-spacing:.03em; text-transform:uppercase; }
.hero-eyebrow .dot{ width:6px; height:6px; background: var(--accent); border-radius:50%; animation: pulse 2.2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce){ .hero-eyebrow .dot{ animation:none !important; } }
@keyframes pulse{ 0%,100%{opacity:1;transform:scale(1);} 50%{opacity:.4;transform:scale(.7);} }

/* titles/subtitles use the display font; body text (set on body{}
   above) uses the mono/body font. Low specificity here on purpose ,
   anywhere a heading is deliberately styled as a small label instead
   (e.g. .dt-form h3 eyebrows in the tools app) already overrides
   this with a more specific selector, so those are unaffected. */
h1, h2, h3, h4, h5, h6{ font-family: var(--font-display); font-weight:500; }
h1{ font-size: clamp(40px, 6vw, 84px); font-weight:500; letter-spacing:-.03em; line-height:.98; }

.mark-wrap{ position:relative; display:inline-block; }
.mark-wrap svg{ position:absolute; left:-4%; top:-8%; width:108%; height:118%; pointer-events:none; overflow:visible; }
.mark-wrap path{ fill:none; stroke: var(--accent); stroke-width:3; stroke-linecap:round; stroke-dasharray:300; stroke-dashoffset:300; transition: stroke-dashoffset 1s var(--ease); }
.hero.in-view .mark-wrap path{ stroke-dashoffset:0; transition-delay:.5s; }
/* ---- mark-video: full-bleed across the viewport width, matching
   the hero splash canvas's own approach exactly (.splash-canvas
   above) , width:100%/height:auto so the whole frame shows
   uncropped at any screen size (nothing lost to object-fit:cover
   cropping), vertically centered via top:50%/translateY(-50%)
   rather than a fixed height guessed in em.

   Horizontal centering uses left:50% + transform:translateX(-50%)
   instead of the margin-left:-50vw trick , the margin-left version
   assumes 50vw exactly equals half the rendered viewport width,
   which breaks (leaves a gap on one side) whenever a scrollbar or
   other viewport-width quirk throws that math off. translateX(-50%)
   instead centers based on the element's OWN rendered width, so it
   stays accurate regardless. ---- */
.mark-video{
  position:absolute; left:50%; width:100vw; height:auto;
  top:50%; transform:translate(-30%, -50%);
  pointer-events:none; z-index:-1;
}
/* ---- phones don't get the "rough draft" sketch-clip animation
   (mark-video for Chrome/Firefox/Edge, mark-canvas + its two hidden
   source <video> for Safari's compositor path) — just the plain
   text stays, with its underline-draw SVG (.mark-wrap svg/path,
   unaffected, that's a separate decorative effect). The driving
   script in index.html also bails out early on this same breakpoint
   so nothing is fetched or played, not just hidden. ---- */
@media (max-width:640px){
  .mark-video, .mark-canvas, .mark-video-source{ display:none !important; }
}

.hero-sub{ margin-top:28px; max-width:46ch; font-family: var(--font-mono); font-size:15px; color: var(--ink-soft); line-height:1.7; }

.hero-actions{ margin-top:40px; display:flex; gap:14px; align-items:center; flex-wrap:wrap; }
.btn-primary{
  font-family: var(--font-mono); font-size:13px; font-weight:500; color: var(--accent-ink); background: var(--accent);
  padding:14px 24px; border-radius: var(--radius); border:1px solid var(--accent); text-decoration:none;
  transition: transform .15s var(--ease), box-shadow .15s var(--ease); display:inline-flex; align-items:center; gap:8px;
  cursor:pointer;
}
@media (hover:hover){ .btn-primary:hover{ transform: translateY(-2px); box-shadow: 3px 4px 0 var(--ink); } }
.btn-primary:active{ transform: translateY(0); box-shadow:none; }
.btn-ghost{
  font-family: var(--font-mono); font-size:13px; color: var(--ink); text-decoration:none;
  border:none; border-bottom:1px solid var(--ink); padding-bottom:2px; display:inline-block;
  transition: transform .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
  /* .btn-ghost is used almost exclusively on <a> tags sitewide, which
     have no default box styling to fight , but on the rare occasion
     it lands on a real <button> (see .nda-popup-dismiss), native
     browser button chrome (its own border/background/padding) would
     otherwise show through underneath this and look like a boxed
     button instead of the plain underlined link every other CTA is.
     "border:none" then "border-bottom:..." resets all four sides
     before re-adding just the one we want; these are no-ops on <a>,
     so safe to apply always. */
  appearance:none; -webkit-appearance:none; background:none; padding-left:0; padding-right:0; padding-top:0; cursor:pointer;
}
@media (hover:hover){ .btn-ghost:hover{ transform: translateX(4px); color: var(--accent-text); border-color: var(--accent-text); } }

.hero-panel{
  position:relative; aspect-ratio: 4/5; border: 1px solid var(--ink); border-radius: var(--radius);
  background: color-mix(in srgb, var(--paper) 88%, var(--ink) 12%); overflow:hidden;
  transition: border-color .4s var(--ease), background .4s var(--ease), transform .15s ease-out;
}
.hero-panel .fig-label{ position:absolute; top:12px; left:12px; font-family: var(--font-mono); font-size:10px; color: var(--ink-soft); letter-spacing:.04em; }
.callout{
  position:absolute; border:1px dashed var(--accent); padding:10px 12px; background: var(--paper);
  font-family: var(--font-mono); font-size:11px; color: var(--ink); max-width:180px; opacity:0;
  transform: translateY(10px) rotate(-1.5deg); transition: opacity .6s var(--ease), transform .6s var(--ease);
}
.hero.in-view .callout{ opacity:1; transform: translateY(0) rotate(-1.5deg); }
.callout-1{ top:22%; left:8%; transition-delay:.9s; }
.callout-2{ bottom:16%; right:10%; transition-delay:1.1s; transform: translateY(10px) rotate(1deg); }
.hero.in-view .callout-2{ transform: translateY(0) rotate(1deg); }
.callout-tag{ color: var(--accent-text); font-weight:500; }
.panel-crosshair{ position:absolute; width:26px; height:26px; top:50%; left:50%; transform: translate(-50%,-50%); opacity:.35; }
.panel-crosshair::before, .panel-crosshair::after{ content:''; position:absolute; background: var(--ink); }
.panel-crosshair::before{ top:50%; left:0; right:0; height:1px; }
.panel-crosshair::after{ left:50%; top:0; bottom:0; width:1px; }

/* ---- scroll-cue: vertical marquee. SCROLL travels upward through
   a small clipped window on a loop — enters fast, slows through the
   legible middle stretch, exits fast — rather than the old
   horizontal underline shimmer. The tick mark (.line) rotates to
   vertical to read as a small up-arrow/indicator beside it. ---- */
.scroll-cue{ position:absolute; bottom:28px; left: var(--gutter); display:flex; align-items:center; gap:10px; font-family: var(--font-mono); font-size:10px; color: var(--ink-soft); letter-spacing:.05em; }
.scroll-cue .line{ width:1px; height:20px; background: var(--ink-soft); position:relative; overflow:hidden; flex-shrink:0; }
.scroll-cue .line::after{ content:''; position:absolute; inset:0; background: var(--accent); transform: translateY(100%); animation: scrollcue-tick 1.8s var(--ease) infinite; }
.scroll-cue .marquee{ position:relative; height:1.6em; width:5.5em; overflow:hidden; }
.scroll-cue .marquee-track{ position:absolute; left:0; width:100%; text-align:left; animation: scrollcue-marquee 2.6s cubic-bezier(.7,0,.3,1) infinite; }
@media (prefers-reduced-motion: reduce){
  .scroll-cue .line::after{ animation:none !important; }
  .scroll-cue .marquee-track{ animation:none !important; top:0; }
}
/* tick mark: quick fast-in/fast-out pulse along the vertical line, echoing the marquee's own rhythm */
@keyframes scrollcue-tick{ 0%{transform:translateY(100%);} 45%,55%{transform:translateY(0);} 100%{transform:translateY(-100%);} }
/* marquee: enters fast (0→20%), holds/settles through the readable
   middle (20→75%), exits fast (75→100%) — the tight keyframe
   spacing at the ends vs. the wide flat stretch in the middle is
   what produces the fast/slow/fast feel on a single linear-ish
   easing curve. */
@keyframes scrollcue-marquee{
  0%   { top: 1.6em; }
  20%  { top: 0; }
  75%  { top: 0; }
  100% { top: -1.6em; }
}

/* ---- logo fold: full-width edge-to-edge wordmark, topmost fold.
   Uses 100dvh (not 100vh) so mobile browser chrome (address bar
   showing/hiding) doesn't leave a gap or force extra scroll , same
   convention as body/.hero below. ---- */
.logo-fold{ min-height: 100vh; min-height: 100dvh; position:relative; overflow:hidden; cursor:pointer; }
.logo-fold .scroll-cue{ position:absolute; z-index:2; bottom:36px; left:50%; transform:translateX(-50%); }
/* splash-stage: mount point for splash-sequence.js's ordered-file
   player. It creates/swaps a single <video> or <img> inside this on
   its own (see that file's mountCurrent()); this rule just sizes and
   positions whichever one is currently mounted. Left/right edges
   match the screen edges (width:100%); height scales proportionally
   (height:auto) so the whole frame always shows, at any screen
   size, with nothing cropped , same convention the old splash
   system used. */
.splash-stage{ position:absolute; inset:0; overflow:hidden; }
.splash-seq-media{
  position:absolute; top:50%; left:0; width:100%; height:auto;
  transform:translateY(-50%);
}
.logo-fold-hit{ position:absolute; inset:0; z-index:1; }

.intro-line{ max-width: var(--container); margin: 0 auto; padding: 30px var(--gutter) 60px; border-top: 1px solid color-mix(in srgb, var(--ink) 12%, transparent); }
.intro-line p{ font-size: clamp(15px, 2.3vw, 26px); font-weight: 500; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.intro-line .lede{ color: var(--ink-soft); font-weight:400; margin-bottom:6px; font-family: var(--font-mono); font-size:13px; text-transform:uppercase; letter-spacing:.05em; }

/* ---- mini-services: compact echo of the /services cards, living
   under the homepage's typing line. Card chrome (border/padding) is
   deliberately quiet, matching .service-card's restraint, so this
   sits right before the hero and shouldn't compete with it , four
   across on desktop, 2x2 on tablet, one column on phone. Discipline
   name/description text is sized up so the labels themselves stay
   legible and evident even though the card box is compact. The
   highlight state (.is-active) is toggled by runTyping() in site.js
   in step with the word currently typing above; stroke-only (just
   the border turns orange, no background fill) so it reads as a
   crisp outline rather than a colored block competing with the
   accent-colored typing word itself. */
.mini-services{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px; margin-top:26px; }
.mini-service-card{
  background: var(--paper);
  border:1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  border-radius: var(--radius);
  padding:20px 22px;
  min-width:0;
  transition: border-color .3s var(--ease), box-shadow .3s var(--ease);
}
.mini-service-card h4{ font-size:22px; margin:0 0 10px; overflow-wrap:break-word; }
.mini-service-card p{ font-family:var(--font-mono); font-size:14px; color:var(--ink-soft); line-height:1.6; margin:0; }
.mini-service-card.is-active{
  border-color: var(--accent-text);
  box-shadow: 0 0 0 1px var(--accent-text);
}
@media (prefers-reduced-motion: reduce){ .mini-service-card{ transition:none; } }
@media (max-width:760px){ .mini-services{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width:420px){ .mini-services{ grid-template-columns:1fr; } }
.type-word{ color: var(--accent-text); display:inline-block; min-width: 1ch; }
.type-cursor{ display:inline-block; width:2px; height:0.9em; background: var(--accent); margin-left:2px; vertical-align:-0.1em; animation: blink 1s step-end infinite; }
@media (prefers-reduced-motion: reduce){ .type-cursor{ animation:none !important; opacity: .6; } }
@keyframes blink{ 0%,100%{opacity:1;} 50%{opacity:0;} }

/* ---- clients strip: single scrolling line, seamless loop.
   Names are muted/grayscale by default; hovering any one name
   pauses the whole line and brings that name into full color
   while the rest keep drifting past behind it. ---- */
.clients-strip{ overflow:hidden; padding: 28px 0; border-top:1px solid color-mix(in srgb, var(--ink) 15%, transparent); border-bottom:1px solid color-mix(in srgb, var(--ink) 15%, transparent); }
.clients-row{ display:flex; width:max-content; gap:64px; align-items:center; will-change: transform; animation: clientsA 26s linear infinite; }
@media (hover:hover){ .clients-row:hover{ animation-play-state: paused; } }
@keyframes clientsA{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
@media (prefers-reduced-motion: reduce){ .clients-row{ animation:none; } }
.clients-row span, .clients-row a{
  font-family: var(--font-mono); font-size: clamp(16px, 2.2vw, 26px); letter-spacing:.01em;
  white-space:nowrap; color: var(--ink-soft); opacity:.45; filter: grayscale(1);
  transition: opacity .3s var(--ease), color .3s var(--ease), transform .3s var(--ease), filter .3s var(--ease);
  cursor: default; text-decoration:none; display:inline-block;
}
@media (hover:hover){ .clients-row span:hover, .clients-row a:hover{ opacity:1; color: var(--accent-text); transform: scale(1.1); filter: grayscale(0); } }
.clients-row a{ cursor: pointer; }
/* ---- client logos , same greyed-out/hover-to-colour treatment as
   the plain-text names above, just applied to an <img> instead of
   relying on `color` (which does nothing to a raster image). ---- */
.clients-row img{
  height: clamp(64px, 2.6vw, 32px); width:auto; max-width: 160px; object-fit: contain;
  display:block; vertical-align:middle;
}
/* ---- optional per-client dark-mode logo variant: same swap
   pattern as .logo-tab's logo-mark-light/dark. A client with no
   dark variant only renders one <img> (no .client-logo-dark in the
   markup at all), so these rules simply have nothing to match and
   that single image stays visible in both themes. ---- */
body[data-theme="dark"] .clients-row img.client-logo-light{ display:none; }
.clients-row img.client-logo-dark{ display:none; }
body[data-theme="dark"] .clients-row img.client-logo-dark{ display:block; }

/* dark mode: same grayscale treatment as light mode, just brightened
   a touch , plain grayscale(1) on a dark background reads dim/muddy,
   this keeps it desaturated but easier to make out at rest. Hover
   still reveals the logo's real colors, unchanged. */
body[data-theme="dark"] .clients-row img{
  filter: grayscale(1) brightness(2);
}
@media (hover:hover){
  body[data-theme="dark"] .clients-row a:hover img,
  body[data-theme="dark"] .clients-row span:hover img{
    filter: none;
  }
}

/* ---- scroll reveal ---- */
.reveal{
  opacity: 0; transform: translateY(24px);
  transition: opacity .6s var(--ease), transform .6s var(--ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.reveal.in-view{ opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce){ .reveal{ opacity:1 !important; transform:none !important; transition:none !important; } }

/* ---- enter: above-the-fold, plays once on page load (no
   scroll needed) , used for hero/page-hero content ---- */
@keyframes enterUp{ from{ opacity:0; transform: translateY(18px); } to{ opacity:1; transform: translateY(0); } }
.enter{ animation: enterUp .7s var(--ease) both; }
.enter-1{ animation-delay: .05s; }
.enter-2{ animation-delay: .16s; }
.enter-3{ animation-delay: .27s; }
.enter-4{ animation-delay: .38s; }
@media (prefers-reduced-motion: reduce){ .enter{ animation:none !important; opacity:1 !important; transform:none !important; } }
/* opacity-only entrance for elements whose transform is driven by JS
   (e.g. hero-panel's pointer-tilt) , a transform-animating .enter
   would permanently override that inline style once fill-mode holds */
@keyframes panelFade{ from{ opacity:0; } to{ opacity:1; } }
.panel-enter{ animation: panelFade .8s var(--ease) .22s both; }
@media (prefers-reduced-motion: reduce){ .panel-enter{ animation:none !important; opacity:1 !important; } }

/* ============================================================
   SUB-PAGE HERO
   ============================================================ */
.page-hero{ max-width: var(--container); margin:0 auto; padding:140px var(--gutter) 50px; }
@media (max-width:640px){ .page-hero{ padding-top:96px; padding-bottom:36px; } }

/* ============================================================
   MOBILE , touch targets + iOS input zoom fix
   ============================================================ */
@media (max-width:640px){
  /* iOS Safari auto-zooms the whole page when focusing any input
     under 16px , this is the standard fix, scoped tightly to mobile
     only so desktop sizing (14px/13.5px depending on the field) is
     untouched. */
  input, select, textarea{ font-size:16px !important; }

  /* icon-only buttons in the fixed docks are comfortably above the
     ~44px touch-target minimum on desktop-oriented sizing already,
     but bump them slightly further for easier tapping */
  .mute-btn{ width:40px; height:40px; }

  /* mode toggle: icon-only on phones , "Light"/"Dark" text hidden,
     button shrinks to a small square matching .mute-btn's compact
     treatment right next to it. Desktop keeps the text label
     untouched (this whole block only applies below 641px). */
  .mode-btn-label{ display:none; }
  .mode-btn{ padding:0; width:36px; height:36px; justify-content:center; }
}
.page-hero h1{ font-size: clamp(34px,5vw,64px); max-width: 20ch; }
.page-hero .lede{ margin-top:20px; max-width:60ch; font-family:var(--font-mono); font-size:15px; color:var(--ink-soft); line-height:1.7; }

.section{ max-width:var(--container); margin:0 auto; padding: 20px var(--gutter) 100px; }
/* ---- section-tight: for a .section that's immediately followed by
   another visual block it should sit close to (e.g. the "Studios and
   companies we've worked with" label right above .clients-strip) ,
   drops the normal 100px bottom padding meant to separate a section
   from the NEXT section down to a small gap instead. ---- */
.section.section-tight{ padding-bottom:12px; }
.section-label{ font-family:var(--font-mono); font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--ink-soft); margin-bottom:20px; }
.back-link{ display:inline-flex; align-items:center; gap:6px; font-family:var(--font-mono); font-size:12px; color:var(--ink-soft); text-decoration:none; margin-bottom:24px; transition: color .2s var(--ease), gap .2s var(--ease); }
@media (hover:hover){ .back-link:hover{ color:var(--accent-text); gap:10px; } }

/* ---- work: filter bar + grid ---- */
.filter-bar{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:32px; }
.filter-btn{
  font-family: var(--font-mono); font-size:11.5px; letter-spacing:.02em; padding:9px 16px;
  border:1px solid var(--ink); background:transparent; color:var(--ink); border-radius:999px; cursor:pointer;
  transition: background .2s var(--ease), color .2s var(--ease), transform .2s var(--ease);
}
.filter-select{ display:none; } /* mobile-only , shown below 641px, see @media block */
@media (max-width:640px){
  .filter-btn{ display:none; }
  .filter-select{
    display:block; flex:1; min-width:0; order:-1; /* takes remaining space, first in the row */
    background:transparent; border:1px solid var(--ink); border-radius:var(--radius);
    padding:12px 14px; font-family:var(--font-mono); font-size:16px; color:var(--ink);
  }
  .filter-bar a.btn-ghost{ margin-left:0 !important; flex-shrink:0; font-size:11.5px; }
}
@media (hover:hover){ .filter-btn:hover{ transform: translateY(-2px); } }
.filter-btn.active{ background: var(--ink); color: var(--paper); }

/* ---- bento grid , shared component: homepage "recent work" and
   the work page filtered grid both use this exact markup/CSS.
   Every tile is the SAME HEIGHT, but column width cycles through 3
   patterns every 9 items (3 rows): wide-narrow-narrow, then
   narrow-wide-narrow, then narrow-narrow-wide, then repeats. This
   works as a 4-equal-column base grid where the "wide" position in
   each group of 3 spans 2 of those columns instead of 1 , same
   underlying math as a plain 2fr/1fr/1fr row, just varied by
   position instead of fixed to column 1 every time. ---- */
.bento-grid{
  display:grid; grid-template-columns:repeat(4,1fr); grid-auto-rows:420px;
  gap:12px;
}
.section-cta{ display:flex; justify-content:center; margin-top:48px; }
.bento-card:nth-child(9n+1){ grid-column: span 2; }
.bento-card:nth-child(9n+2){ grid-column: span 1; }
.bento-card:nth-child(9n+3){ grid-column: span 1; }
.bento-card:nth-child(9n+4){ grid-column: span 1; }
.bento-card:nth-child(9n+5){ grid-column: span 2; }
.bento-card:nth-child(9n+6){ grid-column: span 1; }
.bento-card:nth-child(9n+7){ grid-column: span 1; }
.bento-card:nth-child(9n+8){ grid-column: span 1; }
.bento-card:nth-child(9n+9){ grid-column: span 2; }
.bento-card{
  position:relative; overflow:hidden; display:block; text-decoration:none; color:inherit;
  border:1px solid color-mix(in srgb, var(--ink) 35%, transparent); border-radius:var(--radius);
  transition: filter .5s var(--ease), opacity .5s var(--ease);
}
.bento-card .bento-img{
  position:absolute; inset:0;
  background: color-mix(in srgb, var(--paper) 88%, var(--ink) 12%);
  transition: transform .5s var(--ease);
}
.bento-card .bento-img img{ width:100%; height:100%; object-fit:cover; display:block; }
.bento-card .bento-placeholder{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  text-align:center; padding:18px; font-family:var(--font-mono); font-size:11px; color:var(--ink-soft);
}
@media (hover:hover){ .bento-card:hover .bento-img{ transform: scale(1.04); } }
.bento-card:focus-visible .bento-img{ transform: scale(1.04); }
.bento-card .bento-overlay{
  position:absolute; inset:0; z-index:2; display:flex; flex-direction:column; justify-content:flex-end; padding:20px;
  background: linear-gradient(to top, rgba(10,9,6,.86) 0%, rgba(10,9,6,.25) 55%, transparent 100%);
  opacity:0; transform: translateY(8px);
  transition: opacity .3s var(--ease), transform .3s var(--ease);
}
@media (hover:hover){ .bento-card:hover .bento-overlay{ opacity:1; transform:translateY(0); } }
.bento-card:focus-visible .bento-overlay{ opacity:1; transform:translateY(0); }
.bento-card .bento-tag{ font-family:var(--font-mono); font-size:10px; letter-spacing:.05em; text-transform:uppercase; color:#FF8A5E; margin-bottom:6px; }
.bento-card .bento-overlay h4{ font-size:18px; color:#F5F4EE; letter-spacing:-.01em; }
.bento-card .bento-overlay p{ font-size:12px; color:#D8D6C8; font-family:var(--font-mono); line-height:1.5; margin-top:4px; }
.bento-card .bento-cta{ font-family:var(--font-mono); font-size:11px; color:#F5F4EE; margin-top:10px; display:inline-block; }
/* filter states , cards stay in the grid, never removed */
.bento-card.is-filtered{ filter: blur(4px); opacity:.32; pointer-events:none; }
@media (prefers-reduced-motion: reduce){ .bento-card{ transition: opacity .3s, filter .3s; } }
@media (max-width:900px){
  .bento-grid{ grid-template-columns:repeat(2,1fr); grid-auto-rows:220px; }
}
@media (max-width:640px){
  .bento-grid{ grid-template-columns:1fr; grid-auto-rows:240px; }
  /* the bento-style span-2 rules above are for the desktop mosaic
     layout , on a single-column grid, a "span 2" card would ask CSS
     Grid to create an extra IMPLICIT column just for itself (grid
     item spans can't just clamp to the explicit grid, they force
     the track to exist), making that one card wider/differently
     placed than the rest instead of clamping to full width like
     you'd expect. Pinning every card to span 1 here is what
     actually guarantees "same size, one per row" on mobile. */
  .bento-card:nth-child(9n+1), .bento-card:nth-child(9n+2), .bento-card:nth-child(9n+3),
  .bento-card:nth-child(9n+4), .bento-card:nth-child(9n+5), .bento-card:nth-child(9n+6),
  .bento-card:nth-child(9n+7), .bento-card:nth-child(9n+8), .bento-card:nth-child(9n+9){
    grid-column: span 1;
  }
}

/* ---- services ---- */
.service-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:20px; }
.service-card{ border:1px solid var(--ink); border-radius:var(--radius); padding:32px; transition: transform .3s var(--ease), box-shadow .3s var(--ease); }
@media (hover:hover){ .service-card:hover{ transform: translateY(-4px); box-shadow: 4px 6px 0 color-mix(in srgb, var(--ink) 15%, transparent); } }
.service-card .num{ font-family:var(--font-mono); font-size:12px; color:var(--accent-text); }
.service-card h3{ font-size:26px; margin:14px 0 10px; }
.service-card p{ color:var(--ink-soft); font-size:14px; line-height:1.7; max-width:40ch; }
@media (max-width:760px){ .service-grid{ grid-template-columns:1fr; } }

/* ---- about: process + team ---- */
.process-list{ display:flex; flex-direction:column; border-top:1px solid var(--ink); }
.process-step{ display:grid; grid-template-columns:60px 1fr; gap:20px; padding:22px 0; border-bottom:1px solid color-mix(in srgb, var(--ink) 15%, transparent); transition: padding-left .3s var(--ease); }
@media (hover:hover){ .process-step:hover{ padding-left:10px; } }
.process-step .step-num{ font-family:var(--font-mono); color:var(--accent-text); font-size:14px; }
.process-step h4{ font-size:18px; margin-bottom:6px; }
.process-step p{ color:var(--ink-soft); font-size:14px; max-width:50ch; }

/* interactive variant , click to expand a detail line (homepage teaser) */
.process-step.interactive{ cursor:pointer; user-select:none; }
.process-step.interactive h4{ display:flex; justify-content:space-between; align-items:center; }
.process-step.interactive h4::after{ content:'+'; font-family:var(--font-mono); font-size:16px; color:var(--accent-text); transition: transform .3s var(--ease); }
.process-step.interactive.is-open h4::after{ content:'\2212'; }
.process-step.interactive .step-detail{
  max-height:0; overflow:hidden; opacity:0;
  font-family:var(--font-mono); font-size:12.5px; color:var(--ink-soft); line-height:1.6;
  transition: max-height .4s var(--ease), opacity .3s var(--ease), margin-top .4s var(--ease);
}
.process-step.interactive.is-open .step-detail{ max-height:140px; opacity:1; margin-top:10px; }

/* ---- homepage: stats strip ---- */
.stats-strip{ display:grid; grid-template-columns:repeat(var(--stat-count, 4), 1fr); gap:1px; background:color-mix(in srgb, var(--ink) 35%, transparent); border:1px solid color-mix(in srgb, var(--ink) 35%, transparent); }
.stat-block{ background:var(--paper); padding:36px 20px; text-align:center; }
.stat-number{ font-size:clamp(30px,4vw,52px); font-weight:700; letter-spacing:-.02em; }
.stat-number .suffix{ color:var(--accent-text); }
.stat-label{ font-family:var(--font-mono); font-size:10.5px; color:var(--ink-soft); text-transform:uppercase; letter-spacing:.05em; margin-top:8px; }
@media (max-width:640px){ .stats-strip{ grid-template-columns:repeat(2,1fr); } }

/* ---- homepage: about teaser ---- */
.about-teaser-grid{ display:grid; grid-template-columns:1fr 1fr; gap:60px; align-items:start; }
.about-teaser-title{ font-size:clamp(26px,3.4vw,40px); letter-spacing:-.01em; margin:14px 0 18px; max-width:18ch; }
.about-teaser-body{ color:var(--ink-soft); font-size:15px; line-height:1.8; max-width:48ch; margin-bottom:22px; }
@media (max-width:800px){ .about-teaser-grid{ grid-template-columns:1fr; } }

.team-grid{ display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:24px; margin-top:12px; }
.team-card{ border:1px dashed var(--ink-soft); border-radius:var(--radius); padding:20px; text-align:center; transition: transform .3s var(--ease), border-style .2s, border-color .2s; }
@media (hover:hover){ .team-card:hover{ transform: translateY(-4px); border-style: solid; border-color: var(--accent-text); } }
.team-card .avatar{ position:relative; width:100%; aspect-ratio:1; background:color-mix(in srgb, var(--ink) 8%, transparent); border-radius:var(--radius); margin-bottom:14px; display:flex;align-items:center;justify-content:center; font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); overflow:hidden; }
.team-card .avatar img{ width:100%; height:100%; object-fit:cover; display:block; }
.team-card h4{ font-size:15px; }
.team-card span{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); }
/* small filled-icon LinkedIn button — sits on the bottom-right
   corner of the photo itself, icon only, no label. */
.team-linkedin{
  display:flex; align-items:center; justify-content:center;
  width:26px; height:26px; border-radius:50%;
  background:var(--paper); color:var(--ink);
  box-shadow: 0 1px 4px color-mix(in srgb, var(--ink) 25%, transparent);
  transition: background .2s var(--ease), color .2s var(--ease), transform .2s var(--ease);
}
.team-linkedin:hover{ background:var(--accent-text); color:var(--paper); transform:scale(1.08); }
.team-card .avatar .team-linkedin{ position:absolute; right:8px; bottom:8px; z-index:1; }

/* ---- studio: content that isn't a plain member card ---- */
.team-content{ display:flex; flex-direction:column; gap:24px; }
.team-group-photo{ border-radius:var(--radius); overflow:hidden; }
.team-group-photo img{ width:100%; display:block; aspect-ratio:16/9; object-fit:cover; }
/* group-photo's own caption stays the short mono/soft photo-credit
   style (e.g. "The whole studio, Mumbai — 2026") , this is a
   caption under a photo, not a person's bio or the studio's own
   introductory copy, so it's deliberately NOT .team-caption's
   body-text treatment below. */
.team-group-photo figcaption{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); margin-top:10px; text-align:center; }
/* member/founder-pair captions ("a couple of lines about their
   experience") read as normal body copy , same tone/size/line-height
   as a project page's Text block (.text-block-body), just centered
   under a photo instead of running full-width in its own section. */
.team-caption{ display:block; max-width:46ch; margin-left:auto; margin-right:auto; font-size:15px; line-height:1.7; color: var(--ink); margin-top:10px; text-align:center; }
.team-card .team-caption{ margin-top:8px; }

/* plain-text team entry (no photo) , same look as a project page's
   Text block, just living in the About page's studio section. */
.team-text-block{ max-width:60ch; margin-left:auto; margin-right:auto; text-align:center; color: var(--ink); }

/* founder pair: one shared photo, two names/roles stacked
   underneath (each centered over its half of the image), with each
   person's LinkedIn button pinned to the corner of the photo
   nearest them — left corner for the left/first person, right
   corner for the right/second person. */
.team-founder-pair{ border:1px dashed var(--ink-soft); border-radius:var(--radius); padding:20px; transition: transform .3s var(--ease), border-style .2s, border-color .2s; }
@media (hover:hover){ .team-founder-pair:hover{ transform: translateY(-4px); border-style: solid; border-color: var(--accent-text); } }
.team-founder-pair .avatar{ position:relative; width:100%; aspect-ratio:16/9; background:color-mix(in srgb, var(--ink) 8%, transparent); border-radius:var(--radius); margin-bottom:16px; overflow:hidden; }
.team-founder-pair .avatar img{ width:100%; height:100%; object-fit:cover; display:block; }
.team-founder-pair .team-linkedin-slot{ position:absolute; bottom:8px; z-index:1; }
.team-founder-pair .team-linkedin-slot.left{ left:8px; }
.team-founder-pair .team-linkedin-slot.right{ right:8px; }
.team-founder-pair-people{ display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media (max-width:520px){ .team-founder-pair-people{ grid-template-columns:1fr; } }
.team-founder-pair-people > div{ text-align:center; }
.team-founder-pair-people h4{ font-size:15px; }
.team-founder-pair-people span{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); display:block; }
.team-founder-pair > .team-caption{ margin-top:16px; padding-top:16px; border-top:1px solid color-mix(in srgb, var(--ink) 12%, transparent); }

/* ---- careers ---- */
.role-list{ border-top:1px solid var(--ink); }
.role-row{
  display:flex; justify-content:space-between; align-items:center; padding:22px 0;
  border:none; border-bottom:1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  flex-wrap:wrap; gap:10px; position:relative; transition: padding-left .3s var(--ease);
  width:100%; background:none; font:inherit; color:inherit; text-align:left;
}
.role-row::before{ content:''; position:absolute; left:-14px; top:0; bottom:0; width:3px; background:var(--accent); transform:scaleY(0); transition: transform .3s var(--ease); }
@media (hover:hover){ .role-row:hover{ padding-left:14px; } }
@media (hover:hover){ .role-row:hover::before{ transform:scaleY(1); } }
.role-row .role-info h4{ font-size:17px; }
.role-row .role-meta{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); margin-top:4px; }
.role-row .btn-ghost{ font-size:13px; white-space:nowrap; }
@media (max-width:640px){
  /* more breathing room between listings */
  .role-row{ padding:28px 0; row-gap:12px; }
  /* "Apply" on the same line as the title, meta text on its own
     line below , the default flex layout above wraps "Apply" below
     the WHOLE title+meta block once it runs out of room, not
     specifically next to the title. Grid + display:contents on
     .role-info (which un-boxes it without changing the markup ,
     its children become direct grid items of .role-row) lets the
     title and "Apply" share a row while the meta text spans its
     own row underneath. */
  .role-row{
    display:grid; grid-template-columns:1fr auto;
    grid-template-areas: "title apply" "meta meta";
    align-items:center;
  }
  .role-row .role-info{ display:contents; }
  .role-row .role-info h4{ grid-area:title; }
  .role-row .role-meta{ grid-area:meta; margin-top:0; }
  .role-row .btn-ghost{ grid-area:apply; justify-self:end; }
}

/* ---- contact ---- */
.contact-grid{ display:grid; grid-template-columns:1fr 1fr; gap:60px; }
.contact-form{ display:flex; flex-direction:column; gap:16px; }
.field label{ display:block; font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--ink-soft); margin-bottom:6px; }
.field input, .field select, .field textarea{ width:100%; background:transparent; border:1px solid var(--ink); border-radius:var(--radius); padding:12px 14px; font-family:var(--font-mono); font-size:14px; color:var(--ink); }
.field textarea{ resize:vertical; min-height:110px; }
.field input:focus, .field select:focus, .field textarea:focus{ outline:2px solid var(--accent); outline-offset:1px; }
.contact-info dt{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); text-transform:uppercase; margin-top:22px; }
.contact-info dt:first-child{ margin-top:0; }
.contact-info dd{ font-size:18px; margin-top:4px; }
.form-success{ display:none; border:1px solid var(--accent); padding:16px; border-radius:var(--radius); font-family:var(--font-mono); font-size:13px; margin-top:16px; }
.form-success.visible{ display:block; }
@media (max-width:760px){ .contact-grid{ grid-template-columns:1fr; } }

/* ---- project detail pages ---- */
.project-meta{ display:flex; flex-wrap:wrap; gap:32px; margin-top:28px; padding-top:24px; }
.project-meta dt{ font-family:var(--font-mono); font-size:10px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-soft); }
.project-meta dd{ font-size:15px; margin-top:4px; }

.img-placeholder{
  border:1px solid var(--ink); border-radius:var(--radius); aspect-ratio: 16/10;
  display:flex; align-items:center; justify-content:center; text-align:center; padding:20px;
  font-family:var(--font-mono); font-size:11px; color:var(--ink-soft);
  background: color-mix(in srgb, var(--paper) 90%, var(--ink) 10%);
  overflow:hidden; transition: transform .3s var(--ease);
}
@media (hover:hover){ .img-placeholder:hover{ transform: scale(1.015); } }
.img-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:16px; }
.img-grid.cols-4{ grid-template-columns:repeat(4,1fr); }
.img-grid .wide{ grid-column: 1 / -1; aspect-ratio: 21/9; }
@media (max-width:760px){ .img-grid, .img-grid.cols-4{ grid-template-columns:repeat(2,1fr); } }

.spec-block{ border-top:1px solid var(--ink); }
.spec-block .spec-row{ display:flex; justify-content:space-between; align-items:baseline; gap:16px; padding:16px 0; border-bottom:1px solid color-mix(in srgb, var(--ink) 15%, transparent); font-family:var(--font-mono); font-size:13px; transition: padding-left .25s var(--ease); }
@media (hover:hover){ .spec-block .spec-row:hover{ padding-left:8px; } }
.spec-block .spec-row:last-child{ border-bottom:none; }
.spec-block .spec-label{ color:var(--ink-soft); text-transform:uppercase; letter-spacing:.05em; font-size:11px; white-space:nowrap; }
.spec-block .spec-value{ text-align:right; }

/* ---- palette / materials row: scales HORIZONTALLY only, never
   wraps to a second line or grows the block taller. Bounded to the
   same content width as the page's images (it's inside the same
   .section wrapper, see project-template.js's section() helper) —
   with up to 8 swatches (enforced in the tools-site editor) it stays
   within that width uncropped; beyond a handful the row scrolls
   sideways within its own bounds rather than bleeding past the page
   edge or spilling onto a new row. ---- */
/* ---- palette / materials row: responsive like the image-strip
   block , reflows into however many columns fit the available
   width rather than a fixed column count or a horizontal-scrolling
   strip. auto-fill/minmax is the same pattern .gallery-grid and
   .team-grid already use elsewhere for this: each swatch keeps its
   natural ~120px width, wraps onto a new row once the container runs
   out of room, and never needs its own breakpoint override , up to
   8 swatches (enforced in the tools-site editor) reflow naturally at
   any screen size, phone included. ---- */
.palette-row{ display:grid; grid-template-columns:repeat(auto-fill, minmax(100px, 120px)); gap:14px; }
.swatch{ width:auto; }
.swatch .chip{ height:80px; border-radius:var(--radius); border:1px solid color-mix(in srgb, var(--ink) 15%, transparent); transition: transform .25s var(--ease); }
@media (hover:hover){ .swatch:hover .chip{ transform: scale(1.05); } }
.swatch .chip-label{ font-family:var(--font-mono); font-size:10px; color:var(--ink-soft); margin-top:8px; }

.type-specimen{ border:1px solid var(--ink); border-radius:var(--radius); padding:32px; }
.type-specimen .spec-tag{ font-family:var(--font-mono); font-size:11px; color:var(--accent-text); margin-bottom:10px; display:block; }
.type-specimen .big{ font-size:clamp(32px,5vw,56px); font-weight:700; letter-spacing:-.02em; }
.type-specimen .sample-body{ margin-top:16px; max-width:60ch; color:var(--ink-soft); font-size:14px; line-height:1.7; }

.feature-list{ display:grid; grid-template-columns:repeat(2,1fr); gap:16px; }
.feature-item{ border:1px solid var(--ink); border-radius:var(--radius); padding:20px; }
.feature-item h4{ font-size:15px; margin-bottom:6px; }
.feature-item p{ font-size:13px; color:var(--ink-soft); }
@media (max-width:760px){ .feature-list{ grid-template-columns:1fr; } }

.screen-row{ display:flex; gap:20px; flex-wrap:wrap; justify-content:center; padding:20px 0; }
.screen-frame{
  width:200px; aspect-ratio:9/19; border:2px solid var(--ink); border-radius:20px;
  display:flex; align-items:center; justify-content:center; text-align:center; padding:16px;
  font-family:var(--font-mono); font-size:10px; color:var(--ink-soft);
  background: color-mix(in srgb, var(--paper) 90%, var(--ink) 10%);
  transition: transform .3s var(--ease);
}
@media (hover:hover){ .screen-frame:nth-child(odd):hover{ transform: rotate(-2deg) translateY(-4px); } }
@media (hover:hover){ .screen-frame:nth-child(even):hover{ transform: rotate(2deg) translateY(-4px); } }
.tech-tags{ display:flex; flex-wrap:wrap; gap:8px; }
.tech-tag{ font-family:var(--font-mono); font-size:11px; padding:7px 13px; border:1px solid var(--ink); border-radius:999px; color:var(--ink-soft); display:inline-block; transition: background .2s var(--ease), color .2s var(--ease), transform .2s var(--ease); }
@media (hover:hover){ .tech-tag:hover{ background:var(--ink); color:var(--paper); transform: translateY(-2px); } }

.pull-quote{ font-size:clamp(24px,3.4vw,38px); font-weight:500; letter-spacing:-.01em; line-height:1.3; max-width:22ch; margin:8px 0 28px; border-left:3px solid var(--accent); padding-left:24px; }
.editorial p{ max-width:62ch; margin-bottom:18px; line-height:1.85; color:var(--ink-soft); font-size:15px; }
.fun-note{ border:1px dashed var(--accent); padding:16px 18px; font-family:var(--font-mono); font-size:12px; max-width:44ch; margin-top:8px; }
.fun-note:not(.reveal){ transform: rotate(-1deg); }
.fun-note.reveal{ transform: translateY(24px) rotate(-1deg); }
.fun-note.reveal.in-view{ transform: translateY(0) rotate(-1deg); }

/* ---- footer ---- */
.site-footer{ border-top:1px solid var(--ink); margin-top:auto; }
.footer-inner{ max-width:var(--container); margin:0 auto; padding:32px var(--gutter); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:20px; }
.footer-inner .f-logo{ font-weight:700; font-size:15px; }
.footer-inner .f-links{ display:flex; gap:18px; list-style:none; flex-wrap:wrap; }
.footer-inner .f-links a{ font-family:var(--font-mono); font-size:12px; color:var(--ink-soft); text-decoration:none; }
@media (hover:hover){ .footer-inner .f-links a:hover{ color:var(--accent-text); } }
.footer-inner .f-meta{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); }
/* ---- social buttons: shared between the footer and the Contact
   page's "Elsewhere" block (see .contact-social there) — same
   circular icon-only treatment in both places, driven by the same
   SOCIAL_LINKS list in assets/js/footer.js. ---- */
.f-social{ display:flex; gap:10px; }
.f-social-btn{
  display:flex; align-items:center; justify-content:center; width:34px; height:34px;
  border-radius:50%; border:1px solid color-mix(in srgb, var(--ink) 20%, transparent);
  color: var(--ink-soft); transition: color .2s var(--ease), border-color .2s var(--ease), transform .2s var(--ease);
}
@media (hover:hover){ .f-social-btn:hover{ color: var(--accent-text); border-color: var(--accent-text); transform: translateY(-2px); } }

/* ============================================================
   PROJECT TEMPLATE , new block types used by project.html (the
   generic Template case-study page, see assets/js/project-template.js).
   Existing components this template already reuses as-is:
   .page-hero, .section-label, .project-meta, .feature-list/.feature-item,
   .screen-row/.screen-frame, .tech-tags/.tech-tag, .back-link, .btn-ghost,
   .pull-quote (for the "quote" block , see .pull-quote-block below).
   Everything below is new, added only for the block types those
   don't already cover.
   ============================================================ */

/* ---- --project-gap: the ONE spacing value used everywhere on this
   page that means "gap between distinct things" , between the hero
   and whatever comes after it, between every block/section, AND
   inside every multi-image grid (gallery, image-pair, image-strip,
   image-strip, screens, process). Previously these were
   all different ad-hoc numbers (16px/72px section padding, 40px
   hero gap, 8px/16px/28px/56px grid gaps) , now they're all this
   one variable, so the whole page reads as one consistent rhythm
   and there's a single place to retune it. Deliberately NOT applied
   to small chip-style UI (tags, colour swatches, spec-sheet rows) ,
   those are tiny elements where this gap would look broken, not
   "uniform." ---- */
.project-template-page{ --project-gap: 40px; }

/* ---- text block size options , the "medium" default is
   deliberately just the plain inherited paragraph size (no extra
   class, no change from before this field existed); sm/lg are
   explicit overrides written by the block's Text size select. ---- */
.text-size-sm{ font-size:14px; line-height:1.7; }
.text-size-lg{ font-size:19px; line-height:1.6; }

/* ---- block text alignment , the "Text align" field on text-bearing
   block types (text, features, quote, tags, spec-sheet; see
   block-types.js/renderBlock's section() helper, which puts the
   align-center/align-right class on the wrapping <section>). Each
   block type needs different handling to actually shift, not just
   the text within an unmoved box: a free-flowing paragraph needs its
   own margin re-centered/re-anchored (it has a max-width), while the
   flex/grid-based blocks (features, tags) need their items'
   justification changed instead. text-align alone is set on every
   type as a baseline/fallback for anything not explicitly handled
   below (e.g. spec-sheet's row label/value pairs, quote's
   attribution). ---- */
.section.align-center{ text-align:center; }
.section.align-right{ text-align:right; }
/* the Text block's body copy is the page's actual written content
   (a case study's prose, not a hero/intro dek), so it reads at the
   normal --ink tone matching the rest of the site's body copy,
   rather than the dimmer --ink-soft .lede is everywhere else uses at
   by default. Scoped to this class rather than touching .lede
   itself, which would also recolor every other (deliberately dimmer)
   use of it sitewide. */
.text-block-body{ color: var(--ink); }
.section.align-center .text-block-body{ margin-left:auto; margin-right:auto; }
.section.align-right .text-block-body{ margin-left:auto; margin-right:0; }
.section.align-center .tech-tags{ justify-content:center; }
.section.align-right .tech-tags{ justify-content:flex-end; }
.section.align-center .section-label{ margin-left:auto; margin-right:auto; }
.section.align-right .section-label{ margin-left:auto; margin-right:0; }

/* ---- hero: full-bleed, title overlaid on top of the media (only
   when the project has hero media set , see project-template.js's
   hasHeroMedia check; a project with no hero falls back to the
   plain .page-hero layout below, unchanged). ---- */
.hero-cover{
  position:relative; width:100vw; margin:0 calc(50% - 50vw);
  height:88vh; min-height:460px; max-height:980px; overflow:hidden;
  display:flex; align-items:flex-end;
}
@media (max-width:640px){ .hero-cover{ height:70vh; min-height:420px; } }
.hero-cover-media{ position:absolute; inset:0; }
.hero-cover-media img{ width:100%; height:100%; object-fit:cover; display:block; }
.hero-cover-media .video-embed{ position:absolute; inset:0; width:100%; height:100%; aspect-ratio:auto; }
.hero-cover-media .video-embed.video-native video{ width:100%; height:100%; object-fit:cover; }
/* Vimeo's own background=1 param (see renderVideoTag in
   project-template.js) already cover-crops to fill any container
   size , no extra CSS needed. YouTube has no equivalent, so a
   YouTube-embedded hero may letterbox instead of covering; that's a
   platform limitation, not something CSS can fix for an iframe. */
.hero-cover-media .video-embed iframe{ width:100%; height:100%; }
.hero-cover-scrim{
  position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(to top, color-mix(in srgb, black 70%, transparent), transparent 60%);
}
.hero-cover-content{
  position:relative; z-index:1; width:100%;
  max-width:var(--container); margin:0 auto; padding:0 var(--gutter) 48px;
}
/* text sits on a photo/video, not the page background , always
   light, regardless of the site's light/dark theme toggle. */
.hero-cover-content .back-link{ color:rgba(255,255,255,.8); }
@media (hover:hover){ .hero-cover-content .back-link:hover{ color:#fff; } }
.hero-cover-content .hero-eyebrow{ color:rgba(255,255,255,.75); }
.hero-cover-content h1{ font-size:clamp(34px,5vw,64px); max-width:20ch; color:#fff; margin:0; }
.hero-cover-content .project-meta{ margin-top:20px; padding-top:0; border-top:none; }
.hero-cover-content .project-meta dt{ color:rgba(255,255,255,.6); }
.hero-cover-content .project-meta dd{ color:#fff; }
.page-hero-below{ padding-top:var(--project-gap); }
@media (max-width:640px){ .page-hero-below{ padding-top:calc(var(--project-gap) * 0.6); } }
/* .page-hero's base rule (used standalone by the no-hero-media
   fallback layout too) has its own padding-bottom:50px baked into
   its shorthand , left alone here would stack with the next
   .section's new margin-top below, breaking the "every gap is
   exactly --project-gap" goal. Zeroing it and letting the following
   .section's margin-top provide 100% of that gap instead keeps it
   uniform in both the hero-image and no-hero-image layouts. */
.project-template-page .page-hero{ padding-bottom:0; }

/* ---- project.html specifically: images take up more of the page,
   less dead air between sections , scoped to this one page (via the
   body class set in project.html) rather than changing .section
   globally, since that class is shared by every page on the site. ----
   Bigger, more editorial pass (reference: layerdesign.com) , fewer,
   larger images per row and more breathing room between blocks. ---- */
.project-template-page .section{ padding: 0 var(--gutter); margin-top: var(--project-gap); margin-bottom: var(--project-gap); }
.project-template-page .gallery-grid{ grid-template-columns:repeat(auto-fill, minmax(420px,1fr)); gap:var(--project-gap); }
.project-template-page .screen-row{ gap:var(--project-gap); padding:32px 0; }
.project-template-page .screen-frame{ width:240px; }
.project-template-page .screen-frame.device-frame-desktop{ width:520px; }
.project-template-page .screen-frame.device-frame-phone{ width:220px; }

/* screen-frame now also has to hold a real <img> (uploaded screens),
   not just placeholder text , clip it to the frame's rounded corners */
.screen-frame img{ width:100%; height:100%; object-fit:cover; border-radius:18px; }
.screen-frame.device-frame-desktop{ width:340px; aspect-ratio:16/10; border-radius:10px; padding:0; }
.screen-frame.device-frame-desktop img{ border-radius:8px; }
.screen-frame.device-frame-phone{ width:180px; }

.gallery-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(220px,1fr)); gap:16px; }
.gallery-grid figure{ margin:0; }
.gallery-grid img{ width:100%; aspect-ratio:var(--ar, 3/2); object-fit:cover; border-radius:var(--radius); }
.gallery-grid figcaption{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); margin-top:8px; }

.video-embed{ position:relative; width:100%; aspect-ratio:16/9; border-radius:var(--radius); overflow:hidden; border:1px solid var(--ink); }
.video-embed iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; }
.video-embed.video-native{ aspect-ratio:auto; }
.video-embed.video-native video{ display:block; width:100%; height:auto; }

.video-mute-btn{
  position:absolute; right:14px; bottom:14px; z-index:2;
  width:36px; height:36px; border-radius:50%; border:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  background: color-mix(in srgb, black 55%, transparent); color:#fff;
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  transition: background .2s var(--ease), transform .2s var(--ease);
}
@media (hover:hover){ .video-mute-btn:hover{ background: color-mix(in srgb, black 75%, transparent); transform: scale(1.06); } }

.prototype-embed{ width:100%; aspect-ratio:16/10; border-radius:var(--radius); overflow:hidden; border:1px solid var(--ink); }
.prototype-embed iframe{ width:100%; height:100%; border:0; }

/* ---- before/after: draggable comparison slider (contained to the
   normal page-width container, not full-bleed), not a
   static side-by-side pair , the "after" image sits underneath at
   full size, the "before" image sits on top clipped to a
   JS-controlled width%, and a handle in between lets a visitor drag
   (mouse), swipe (touch, via Pointer Events , one code path covers
   both) or use arrow keys (keyboard-focusable, role="slider") to
   reveal more of either side. See wireBeforeAfterSliders() in
   project-template.js for the interaction; this block is purely the
   visual shell it wires up. Both <img> tags are deliberately given
   the SAME width (100%, the slider's own full width) so the two
   photos stay pixel-aligned as the clip width changes , sizing the
   "before" image to its own (shrinking) container instead would
   visibly rescale it while dragging. ---- */
.before-after-slider{
  position:relative; width:100%; aspect-ratio:var(--ar, 16/9); overflow:hidden; cursor:ew-resize; touch-action:none;
  border-radius:var(--radius);
  background:var(--ink-soft);
  -webkit-user-select:none; user-select:none; /* dragging must move the divider, not select page text/the images */
}
.baf-after, .baf-before{ position:absolute; inset:0; overflow:hidden; }
.baf-before{ width:50%; } /* JS sets this width% on drag */
.baf-after img, .baf-before img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
  -webkit-user-drag:none; user-drag:none; pointer-events:none; /* stops the browser's native "ghost image" drag/select , the slider div itself (not the <img>) owns all pointer handling */
}
.baf-handle{
  position:absolute; top:0; bottom:0; left:50%; width:2px;
  background:var(--paper); transform:translateX(-50%); pointer-events:none;
}
.baf-handle-grip{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:44px; height:44px; border-radius:50%;
  background:var(--paper); border:1px solid var(--ink); color:var(--ink);
  display:flex; align-items:center; justify-content:center; gap:1px;
  pointer-events:auto; cursor:ew-resize;
}
.baf-label{
  position:absolute; top:16px; z-index:1;
  font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:.05em;
  padding:6px 12px; background:color-mix(in srgb, var(--paper) 85%, transparent);
  border:1px solid var(--ink); border-radius:999px; pointer-events:none;
  transition:opacity .2s var(--ease);
}
.baf-label-before{ left:16px; }
.baf-label-after{ right:16px; }

/* spec sheet now reuses the real .spec-block component (see
   further up in this file, originally designed for the industrial
   case-study example) instead of the ad-hoc two-column .spec-sheet/dl
   grid this used to be , same design language everywhere a spec
   sheet appears. */

/* ---- process: was a cramped horizontal row of ~220px thumbnails;
   rebuilt as a large, vertically-stacked sequence , same numbered
   data, much more visual weight. Uses .process-sequence/.process-block
   (NOT .process-step) specifically to avoid colliding with the
   pre-existing, unrelated .process-step component used on about.html
   and index.html (the "how we work" accordion steps). ---- */
.process-sequence{ display:flex; flex-direction:column; gap:var(--project-gap); }
.process-block-num{ font-family:var(--font-mono); font-size:13px; color:var(--ink-soft); margin-bottom:14px; }
.process-block-figure{ margin:0; }
.process-block-figure img{ width:100%; max-height:82vh; object-fit:cover; border-radius:var(--radius); cursor:zoom-in; }
.process-block-figure figcaption{ margin-top:10px; font-family:var(--font-mono); font-size:12px; color:var(--ink-soft); }

/* ---- image pair: two images side by side, contained to the normal
   page-width container (not full-bleed) , sized as large as that
   container allows. ---- */
.image-pair-grid{ display:grid; grid-template-columns:1fr 1fr; gap:var(--project-gap); }
.image-pair-grid img{ width:100%; aspect-ratio:var(--ar, auto); max-height:80vh; object-fit:cover; display:block; cursor:zoom-in; border-radius:var(--radius); }
.image-pair figcaption{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); text-align:center; padding-top:10px; }
@media (max-width:640px){ .image-pair-grid{ grid-template-columns:1fr; gap:calc(var(--project-gap) * 0.6); } }

/* ---- image strip: 1-5 images side by side, contained (not
   full-bleed) , --count (set inline per-block, from the actual
   number of images) drives equal-width columns, so fewer images
   means each one renders bigger, up to a single image filling the
   whole row. ---- */
.image-strip{ display:grid; grid-template-columns:repeat(var(--count, 3), 1fr); gap:var(--project-gap); }
.image-strip-item{ margin:0; }
.image-strip-item img{ width:100%; aspect-ratio:var(--ar, auto); max-height:82vh; object-fit:cover; display:block; cursor:zoom-in; border-radius:var(--radius); }
.image-strip-item figcaption{ margin-top:8px; font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); }
@media (max-width:760px){ .image-strip{ grid-template-columns:1fr; gap:calc(var(--project-gap) * 0.6); } }

/* ---- pull quote: reuses the site's existing .pull-quote component
   (already used elsewhere, e.g. about.html) for visual consistency,
   but widened/centred here since a project page's quote is a
   standalone statement rather than an aside next to body copy. ---- */
.pull-quote-block{ max-width:720px; margin:16px auto 0; text-align:center; }
.pull-quote-block .pull-quote{ max-width:none; border-left:none; padding-left:0; margin-bottom:0; }
/* left/right explicitly override the centered default above (see
   the "Text align" field's quote-specific handling in
   project-template.js) , left restores the quote's own natural
   border-left treatment since that only reads correctly against a
   left edge, not floating mid-line under centered text. */
.pull-quote-block.align-left{ margin-left:0; margin-right:auto; text-align:left; }
.pull-quote-block.align-left .pull-quote{ border-left:3px solid var(--accent); padding-left:24px; }
.pull-quote-block.align-right{ margin-left:auto; margin-right:0; text-align:right; }
.pull-quote-block figcaption{ margin-top:18px; font-family:var(--font-mono); font-size:12px; color:var(--ink-soft); }

/* the "materials" block now renders through the shared
   .palette-row/.swatch/.chip components defined earlier in this
   file (the same style Bridge and Bond, since retired, used for its
   own colour palette section), instead of the bespoke .material-chip
   style this used to be. */

/* ---- full-width image block: contained to the normal page-width
   container (not full-bleed) , sized as large as that allows. ---- */
.contained-figure{ margin:0; }
.contained-figure img{ width:100%; max-height:80vh; object-fit:cover; display:block; cursor:zoom-in; border-radius:var(--radius); }
.contained-figure figcaption{ font-family:var(--font-mono); font-size:11px; color:var(--ink-soft); text-align:center; padding-top:10px; }

/* ---- bottom-right icon buttons (website link / Draft0 section /
   back-to-case-study) , fixed to the viewport, sitting in a row.
   Icon-only, with a custom tooltip on hover AND keyboard focus
   (not the native title="" tooltip , see the comment above
   buildIconCtaRow() in project-template.js for why). Reuses the
   same circular-icon-button visual language as .mute-btn elsewhere
   on the site, just larger and fixed-positioned. ---- */
.icon-cta-row{ position:fixed; right:var(--gutter); bottom:24px; z-index:50; display:flex; gap:12px; }
.icon-cta{
  position:relative; width:44px; height:44px; border-radius:50%; flex-shrink:0;
  background:var(--ink); color:var(--paper); display:flex; align-items:center; justify-content:center;
  text-decoration:none; box-shadow:0 10px 30px color-mix(in srgb, var(--ink) 25%, transparent);
  transition: transform .2s var(--ease), box-shadow .2s var(--ease);
}
@media (hover:hover){ .icon-cta:hover{ transform:translateY(-2px); box-shadow:0 14px 36px color-mix(in srgb, var(--ink) 30%, transparent); } }
.icon-cta-tip{
  position:absolute; bottom:calc(100% + 10px); right:0; transform:translateY(4px);
  background:var(--ink); color:var(--paper); font-family:var(--font-mono); font-size:11px; white-space:nowrap;
  padding:6px 10px; border-radius:999px; opacity:0; pointer-events:none;
  transition:opacity .18s var(--ease), transform .18s var(--ease);
}
.icon-cta:hover .icon-cta-tip, .icon-cta:focus-visible .icon-cta-tip{ opacity:1; transform:translateY(0); }

/* ---- click-to-enlarge lightbox , one shared overlay, reused for
   every project image (gallery, full-width, image-pair, screens,
   process). NOT before/after , that's drag-to-compare instead. ---- */
.lightbox-trigger{ cursor:zoom-in; }
.lightbox-overlay{
  display:none; position:fixed; inset:0; z-index:200; background:rgba(10,9,6,.92);
  align-items:center; justify-content:center; padding:40px; cursor:zoom-out;
}
.lightbox-overlay.open{ display:flex; }
.lightbox-overlay img{ max-width:100%; max-height:100%; object-fit:contain; border-radius:8px; }

/* ---- end-of-page project navigation: next project in the same
   discipline, or back to the full grid ---- */
.project-nav-section{ padding-top:36px; }
.project-nav-buttons{ display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:16px; }

/* ---- CMS-load error state , shown in place of a dynamic section
   when the fetch to tools.draft0.in fails entirely. See site.js's
   renderLoadError(). Deliberately plain/quiet , this isn't meant to
   read as alarming, just honestly "this didn't load." */
.load-error{
  font-family:var(--font-mono); font-size:12.5px; color:var(--ink-soft);
  text-align:center; padding:32px 20px; width:100%;
}

/* ---- bento thumbnail video , same treatment as a thumbnail image,
   just a <video> instead of an <img> ---- */
.bento-card .bento-img video{ width:100%; height:100%; object-fit:cover; display:block; }

/* ---- NDA , a card marked isNda still shows in the grid (with this
   badge, always visible, not just on hover) but clicking it doesn't
   navigate , see the delegated click handler in site.js. ---- */
.bento-nda-badge{
  position:absolute; top:14px; left:16px; z-index:2;
  font-family:var(--font-mono); font-size:10px; letter-spacing:.05em; text-transform:uppercase;
  color:#F5F4EE; background: color-mix(in srgb, black 55%, transparent);
  padding:5px 10px; border-radius:999px; -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}
.nda-popup-overlay{
  display:none; position:fixed; inset:0; z-index:200; background:rgba(10,9,6,.82);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  align-items:center; justify-content:center; padding:24px;
}
.nda-popup-overlay.open{ display:flex; }
.nda-popup{
  max-width:420px; background:var(--paper); border:1px solid var(--ink); border-radius:var(--radius);
  padding:32px; text-align:center;
}
/* same accent-dot + uppercase-mono motif as .hero-eyebrow on every
   page's title section , reused here so the popup reads as part of
   the site rather than a generic browser-style alert box. */
.nda-popup-eyebrow{
  display:inline-flex; align-items:center; gap:8px; justify-content:center;
  font-family:var(--font-mono); font-size:11px; letter-spacing:.05em; text-transform:uppercase;
  color:var(--ink-soft); margin-bottom:16px; width:100%;
}
.nda-popup-eyebrow .dot{ width:6px; height:6px; background:var(--accent); border-radius:50%; animation: pulse 2.2s ease-in-out infinite; }
.nda-popup p{ font-family:var(--font-mono); font-size:13.5px; color:var(--ink-soft); line-height:1.65; margin-bottom:22px; }
.nda-popup .nda-popup-dismiss{ font-size:13px; }

</style>
