/* ============================================================
   APEX MARCOM - stylesheet
   One file, every page.

   The look is a deliberate blend of two earlier directions:
   the Colour Board (sand paper, ink type, nine saturated service
   tiles) supplies the identity and the structure; the Trivision
   mockup supplies the material craft -
   ambient light, film grain, glass chrome, inset hairline
   highlights, deep soft shadows, gradient display type.

   The rule that keeps the mix from turning into a costume:
   LIGHT SURFACES STAY EDITORIAL, DARK SURFACES GO PREMIUM.
   Sand pages read like good print - generous measure, hairline
   rules, no glow. The dark and coloured blocks (hero, band,
   service headers, footer) are where the glass, the grain and
   the light pools are allowed to do their work. Applying the
   dark treatment to a sand page is what makes this kind of
   design look cheap, so don't.
   ============================================================ */

/* ---- typefaces ----
   Self-hosted, not loaded from fonts.googleapis.com. The block below is
   REGENERATED by tools/fetch-fonts.py - edit that script, never this, or
   the next run overwrites you. The binaries live in assets/fonts/ and are
   served immutable for a year (see _headers), which is only safe because
   their filenames are stable and the font versions are frozen. */
/* @@FONTS-START - generated by tools/fetch-fonts.py, do not hand-edit */
@font-face{
  font-family:"Archivo";
  font-style:normal;
  font-weight:400 900;
  font-stretch:100%;
  font-display:swap;
  src:url("fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"Archivo";
  font-style:normal;
  font-weight:400 900;
  font-stretch:100%;
  font-display:swap;
  src:url("fonts/archivo-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"DM Mono";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("fonts/dm-mono-400-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"DM Mono";
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url("fonts/dm-mono-400-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"DM Mono";
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("fonts/dm-mono-500-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face{
  font-family:"DM Mono";
  font-style:normal;
  font-weight:500;
  font-display:swap;
  src:url("fonts/dm-mono-500-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* @@FONTS-END */

:root{
  /* ---- ink family ---- */
  --ink:#141318;          /* body type, ink surfaces                */
  --void:#0A090D;         /* deepest - hero and footer grounds      */
  --panel:#1B1A21;        /* raised surface on a void ground        */

  /* ---- paper family ---- */
  --sand:#EDEBE4;
  --sand-2:#F8F7F2;   /* raised card: brighter paper on the sand page. Was #E1DFD6,
                         a darker sand that read as a smudge, not a surface. */
  --sand-3:#E2DFD5;

  /* ---- the palette is two colours ----
     The logo is red and near-black, so the site is red and near-black. This
     replaced a six-colour system (orange/blue/yellow/green/violet/pink) that
     was invented before the real mark existed and read as decoration rather
     than as identity.

     --red is sampled straight out of "Apex Marcom Logo.png".
     --deep is the second step, for rhythm across nine tiles without
       introducing a third hue.
     --red-ink is the red darkened until it clears 4.5:1 on --sand (the logo
       red itself manages only 3.7:1). Small text and thin rules ON PAPER
       use this; fills, tiles and gradients use the true red.
     --red-lit is the red lightened until it clears 4.5:1 on --void (5.9:1).
       Small text ON INK uses this. Using --red there gives 4.5:1 at best.

     Sand is the paper, not a third colour. */
  --red:#EC1E24;
  --deep:#A9131A;
  --red-ink:#C4141A;
  --red-lit:#F5555A;

  /* ---- the four tile colours ----
     The nine service tiles cycle through these plus ink and sand. They are
     NOT brand colours and nothing outside a tile uses them: the red above is
     still what says Apex Marcom, and it is what buttons, links, the header
     rule and the gradient are painted with. These exist so nine tiles in a
     row read as nine different things rather than as one long red block.

     Chosen dark enough to carry white text at 4.5:1 or better, EXCEPT amber,
     which is a light tone and takes ink - see the .t-* block below, and the
     note there about what breaks if that is forgotten.

       teal    5.1:1 with white      amber   8.7:1 with ink
       indigo  9.3:1 with white      clay    5.1:1 with white  */
  --teal:#0C6E6D;
  --amber:#E8A33D;
  --indigo:#33408A;
  --clay:#94472E;          /* deepened from #A8553A: white on it 5.2 -> 6.6:1 */

  /* Semantic states, deliberately outside the brand palette. A success
     message has to read as success; painting it red because the palette is
     red would be a design idea beating a usability one. */
  --ok:#1F8A5F;
  /* form error text. Deliberately a touch browner than the brand red so an
     invalid field does not read as a brand accent. 4.9:1 on --sand. */
  --err:#B82D18;

  /* ---- the accents ----
     Indigo leads and teal answers it. These are what buttons, links, rules,
     bullets and the light pools on every dark block are painted with; the
     four tile colours above are the same family, so the page holds together
     without any of it being red.

     --accent      fills and backgrounds
     --accent-ink  small text and thin rules ON PAPER. Flips in dark, exactly
                   like --red-ink did: indigo on a dark ground is 2.9:1 and
                   unreadable, so the dark theme swaps in the light indigo.
     --accent-lit  small text on blocks that are dark in BOTH themes - the
                   hero, the band, the CTA, the footer. Never flips. */
  --accent:#33408A;
  --accent-ink:#2B377A;
  --accent-lit:#93A0E8;

  /* One gradient, indigo into teal. It ran red into deep red, and before
     that red -> orange -> yellow, which was three hues pretending to be one.
     Two hues that belong to the tile family is the middle position. */
  --grad-accent:linear-gradient(102deg,#3F4FB0,#33408A 46%,#0F7A75);

  /* ---- lines. Two weights on paper, two on ink. ---- */
  --line:rgba(20,19,24,.17);
  --line-2:rgba(20,19,24,.32);
  --edge:rgba(237,235,228,.11);
  --edge-lit:rgba(237,235,228,.26);

  /* ---- secondary type colours, both grounds ---- */
  --muted:#3F3E48;        /* 8.8:1 on --sand                        */
  --muted-2:#55545F;      /* 6.2:1 on --sand - captions, mono only.
                             Was #6B6A75, which measured 4.46:1 - under
                             AA for small text despite what this said. */
  --dim:rgba(237,235,228,.84);
  --dim-2:rgba(237,235,228,.68);

  /* ---- elevation. Soft and deep, never a hard offset block. ---- */
  --sh-1:0 1px 2px rgba(20,19,24,.05),0 10px 24px -18px rgba(20,19,24,.40);
  --sh-2:0 2px 4px rgba(20,19,24,.05),0 30px 60px -34px rgba(20,19,24,.55);
  --sh-3:0 40px 90px -50px rgba(20,19,24,.70);
  --sh-dark:0 40px 90px -52px #000;
  /* the 1px inner top highlight is the single most load-bearing detail in
     the premium half of this design - it is what makes a panel read as a
     lit surface rather than a filled rectangle. */
  --lip:inset 0 1px 0 rgba(237,235,228,.16);

  /* ---- tokens the theme switch depends on ----
     --sand and --ink flip between themes, and that is fine for the page
     ground and for body text. It is NOT fine in two places:

     1. Blocks that are dark in BOTH themes (hero, band, CTA, footer). Their
        foreground must stay light, so it is --on-dark, which never changes.
        Writing color:var(--sand) there would turn the hero's type near-black
        the moment the page goes dark.
     2. Blocks that are LIGHT in both themes - the amber tile. Its foreground
        must stay dark, so it is --on-light, which never changes either.
        Writing color:var(--ink) there is the same mistake mirrored: the
        amber tile keeps its colour when the page goes dark while --ink turns
        pale, and the tile ends up light type on a light ground.
     3. Tints. var(--fill) is a faint dark wash on paper and an
        invisible smudge on ink, so every one of them is --fill instead. */
  --on-dark:#EDEBE4;
  --on-light:#141318;
  /* WAS `--fill:var(--fill)` - a reference to itself, which is invalid, so
     every tint built on --fill rendered as nothing in the light theme. */
  --fill:rgba(20,19,24,.06);
  --fill-2:rgba(20,19,24,.12);
  --prose:#24232B;
  --field-bg:rgba(255,255,255,.55);
  --field-bg-focus:#fff;
  --grid:rgba(20,19,24,.15);
  --chrome:rgba(237,235,228,.86);
  --chrome-float:rgba(237,235,228,.7);
  --chrome-fade:rgba(237,235,228,.85);
  --ok-text:#156946;
  --lip-light:rgba(255,255,255,.5);
  --grain-blend:multiply;
  --grain-opacity:.42;
  /* the ground behind every page: three colour pools and a blueprint grid.
     Strong enough to read as a surface, not as a flat sheet of paper. */
  --pool-a:rgba(51,64,138,.17);     /* indigo, top left   */
  --pool-b:rgba(232,163,61,.16);    /* amber, top right   */
  --pool-c:rgba(12,110,109,.13);    /* teal, low          */
  --gridline:rgba(20,19,24,.055);

  --disp:"Archivo", system-ui, sans-serif;
  --mono:"DM Mono", ui-monospace, monospace;
  --gut:clamp(18px,4vw,56px);
  --r:18px;
  --r-sm:12px;
  --ease:cubic-bezier(.22,1,.36,1);
}

/* NATIVE INTEGRATION.
   color-scheme tells the browser to render its own furniture - scrollbars,
   form controls, the date picker, spellcheck underlines, the tap highlight -
   in the matching light or dark style. Without it a dark page keeps white
   scrollbars and a white select dropdown.
   accent-color paints checkboxes, radios, range sliders and progress bars in
   the brand red while keeping each platform's NATIVE control. That is the
   whole trick: the OS draws the control, we only say what colour it is. */
:root{color-scheme:light;accent-color:var(--accent)}
:root[data-theme="dark"]{color-scheme:dark}

/* ---- dark theme ----
   Three states, and all three have to be handled or the toggle only works
   one way. data-theme is stamped on <html> by assets/theme.js BEFORE first
   paint; with no stored choice nothing is stamped and the OS preference
   decides, which is why the media query is guarded against an explicit
   light choice.

   --void is not black here. The always-dark blocks have to stay visible
   against the page, so on a #100F13 ground they lift to #1A1922 rather than
   sinking into it. */
:root[data-theme="dark"],
:root:not([data-theme="light"]) {}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]){
    --sand:#100F13;
    --sand-2:#2B2A35;   /* raised surface - MUST stay clear of --void below,
                           or .t-sand and .t-ink become the same tile */
    --sand-3:#36353F;
    --ink:#EDEBE4;
    --void:#1A1922;
    --panel:#2B2A35;
    --muted:#C3C2CD;
    --muted-2:#A8A7B3;
    --prose:#D9D8E0;
    --line:rgba(237,235,228,.18);
    --line-2:rgba(237,235,228,.34);
    --fill:rgba(237,235,228,.08);
    --fill-2:rgba(237,235,228,.14);
    --field-bg:rgba(237,235,228,.05);
    --field-bg-focus:rgba(237,235,228,.09);
    --grid:rgba(237,235,228,.11);
    --chrome:rgba(16,15,19,.86);
    --chrome-float:rgba(16,15,19,.72);
    --chrome-fade:rgba(16,15,19,.85);
    --red-ink:#F5555A;
  /* Lifted off the dark ground. Same roles, same contrast targets: white on
     teal/indigo/clay, ink on amber. */
  --teal:#0F7A75;
  --indigo:#4A57A8;
  --clay:#9C4C31;
  --accent:#4A57A8;
  --accent-ink:#A9B4F0;
    --err:#F2726A;
    --ok-text:#5FCFA0;
    --lip-light:rgba(237,235,228,.10);
  --grain-blend:overlay;
    --grain-opacity:.26;
    --pool-a:rgba(99,114,214,.2);
    --pool-b:rgba(232,163,61,.09);
    --pool-c:rgba(15,122,117,.16);
    --gridline:rgba(237,235,228,.045);
    --sh-1:0 1px 2px rgba(0,0,0,.4),0 10px 24px -18px rgba(0,0,0,.8);
    --sh-2:0 2px 4px rgba(0,0,0,.4),0 30px 60px -34px rgba(0,0,0,.9);
    --sh-3:0 40px 90px -50px rgba(0,0,0,.95);
    color-scheme:dark;
  }
}
:root[data-theme="dark"]{
  --sand:#100F13;
  --sand-2:#2B2A35;   /* raised surface - MUST stay clear of --void below,
                         or .t-sand and .t-ink become the same tile */
  --sand-3:#36353F;
  --ink:#EDEBE4;
  --void:#1A1922;
  --panel:#2B2A35;
  --muted:#C3C2CD;
  --muted-2:#A8A7B3;
  --prose:#D9D8E0;
  --line:rgba(237,235,228,.18);
  --line-2:rgba(237,235,228,.34);
  --fill:rgba(237,235,228,.08);
  --fill-2:rgba(237,235,228,.14);
  --field-bg:rgba(237,235,228,.05);
  --field-bg-focus:rgba(237,235,228,.09);
  --grid:rgba(237,235,228,.11);
  --chrome:rgba(16,15,19,.86);
  --chrome-float:rgba(16,15,19,.72);
  --chrome-fade:rgba(16,15,19,.85);
  --red-ink:#F5555A;
  /* Lifted off the dark ground. Same roles, same contrast targets: white on
     teal/indigo/clay, ink on amber. */
  --teal:#0F7A75;
  --indigo:#4A57A8;
  --clay:#9C4C31;
  --accent:#4A57A8;
  --accent-ink:#A9B4F0;
  --err:#F2726A;
  --ok-text:#5FCFA0;
  --lip-light:rgba(237,235,228,.10);
  --grain-blend:overlay;
  --grain-opacity:.26;
  --pool-a:rgba(99,114,214,.2);
  --pool-b:rgba(232,163,61,.09);
  --pool-c:rgba(15,122,117,.16);
  --gridline:rgba(237,235,228,.045);
  --sh-1:0 1px 2px rgba(0,0,0,.4),0 10px 24px -18px rgba(0,0,0,.8);
  --sh-2:0 2px 4px rgba(0,0,0,.4),0 30px 60px -34px rgba(0,0,0,.9);
  --sh-3:0 40px 90px -50px rgba(0,0,0,.95);
}

/* ---- cross-document view transitions ----
   One line turns eighteen separate page loads into one continuous surface:
   the browser snapshots the old page, snapshots the new one, and animates
   between them. No JavaScript, no router, no single-page rewrite - it works
   precisely because the site is static multipage.

   Browsers without support ignore the at-rule and navigate normally, so
   there is nothing to fall back to and nothing to feature-detect. */
@view-transition{ navigation: auto }

/* The header and footer are identical on every page, so they are named and
   therefore MORPH instead of cross-fading. Without this the chrome fades out
   and back in on every navigation, which reads as a flicker rather than as a
   transition. Names must be unique per page - these two elements appear once. */
header{view-transition-name:site-header}
footer{view-transition-name:site-footer}

::view-transition-old(root),
::view-transition-new(root){
  animation-duration:.28s;
  animation-timing-function:cubic-bezier(.22,1,.36,1);
}
::view-transition-group(site-header),
::view-transition-group(site-footer){
  animation-duration:.3s;
  animation-timing-function:cubic-bezier(.22,1,.36,1);
}

*{box-sizing:border-box;margin:0;padding:0}
/* `hidden` must win. Any rule that sets display (flex, grid) on a class
   silently beats the browser's own [hidden]{display:none}, and a hidden
   element reappears: the planner showed all nine service cards and the site
   sheet kept its arrows on single-photo sites until this was added. */
[hidden]{display:none!important}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%;overflow-x:clip}
body{
  background:var(--sand);color:var(--ink);
  font-family:var(--disp);font-size:16px;line-height:1.55;
  overflow-x:hidden;
  font-optical-sizing:auto;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
}
a{color:inherit;text-decoration:none}
img,svg{max-width:100%}
:focus-visible{outline:3px solid var(--ink);outline-offset:3px;border-radius:2px}
.wrap{max-width:1400px;margin:0 auto;padding-inline:var(--gut)}

/* ---- ambient + grain ----
   Both are drawn on <body>'s pseudo-elements rather than on markup, because
   the shell is copy-pasted into every page and adding two divs would be a
   seventeen-file change for decoration. Fixed, non-interactive, and sitting
   below the header (z 60) but above content.

   On paper these are near-invisible on purpose: a wash of warm and cool at
   ~7% is the difference between "off-white" and "a sheet of paper under a
   window". Push either much further and the page starts to look tinted. */
/* The ground. Fixed, so the pools stay put while content moves over them -
   the same relationship as a wallpaper under icons. They used to be at 5-9%
   alpha, which on sand read as no colour at all and left every page header
   looking like an empty sheet. Static on purpose: a drifting background is
   exactly the full-viewport motion reduced-motion users ask to be spared. */
body::before{
  content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(58vw 50vw at 0% -8%, var(--pool-a), transparent 64%),
    radial-gradient(50vw 44vw at 100% 4%, var(--pool-b), transparent 62%),
    radial-gradient(56vw 46vw at 70% 104%, var(--pool-c), transparent 64%),
    radial-gradient(40vw 36vw at 12% 92%, var(--pool-b), transparent 60%);
}
/* A blueprint grid behind the top of each page - the part that is mostly
   heading and air. It scrolls with the page and fades out within the first
   screen, so it gives the header a surface to sit on without ever sitting
   behind body copy. One element, CSS only: no markup in seventeen files. */
main::before{
  content:"";position:absolute;left:0;right:0;top:0;height:min(1100px,130vh);
  z-index:-1;pointer-events:none;
  background-image:
    linear-gradient(var(--gridline) 1px, transparent 1px),
    linear-gradient(90deg, var(--gridline) 1px, transparent 1px);
  background-size:56px 56px;background-position:center -1px;
  -webkit-mask-image:radial-gradient(120% 90% at 50% 0%, #000 30%, transparent 78%);
  mask-image:radial-gradient(120% 90% at 50% 0%, #000 30%, transparent 78%);
}
body::after{
  content:"";position:fixed;inset:0;z-index:3;pointer-events:none;
  opacity:var(--grain-opacity);mix-blend-mode:var(--grain-blend);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.13'/%3E%3C/svg%3E");
}
header,main,footer,.navpanel,.navscrim,.skip{position:relative;z-index:4}

/* ============================================================
   CHROME
   ============================================================ */

header{
  position:sticky;top:0;z-index:60;
  background:var(--chrome);
  backdrop-filter:blur(14px) saturate(150%);
  -webkit-backdrop-filter:blur(14px) saturate(150%);
  transition:background 240ms ease,box-shadow 240ms ease,transform 340ms var(--ease);
}
header.floating{
  background:var(--chrome-float);
  backdrop-filter:blur(22px) saturate(180%);
  -webkit-backdrop-filter:blur(22px) saturate(180%);
}
/* a soft fade where content passes beneath the chrome, instead of a 1px
   rule that sits there even with nothing under it */
header::after{
  content:"";position:absolute;left:0;right:0;top:100%;height:24px;
  pointer-events:none;opacity:0;transition:opacity .35s ease;
  background:linear-gradient(180deg,var(--chrome-fade),transparent);
}
header.floating::after{opacity:1}
/* app.js adds .hidden on scroll-down and clears it the instant they scroll
   up. It never applies under reduced motion or with the nav sheet open. */
header.hidden{transform:translateY(-102%)}

.hd{display:flex;align-items:center;justify-content:space-between;gap:18px;padding-block:15px}
.logo{
  font-weight:900;font-size:clamp(18px,2vw,23px);letter-spacing:-.025em;
  display:inline-flex;align-items:center;gap:9px;
}
/* The header carries the real lockup as an image, so the name is the artwork
   rather than Archivo pretending to be it. width/height are on the tag to
   reserve the space before it loads; the height here is what actually sizes
   it. The footer uses the variant with the wordmark in sand. */
.logo img{
  height:34px;width:auto;display:block;
  transition:opacity .25s ease;
}
.logo:hover img{opacity:.8}
/* The lockup's M is near-black, so on a dark header it vanishes and leaves a
   floating red A. content:url() re-points the rendered image without touching
   the <img> in eighteen copies of the shell; the alt text is unaffected. The
   footer is dark in both themes and already carries the light asset. */
:root[data-theme="dark"] header .logo img{content:url("logo-header-light.png")}
@media(prefers-color-scheme:dark){
  :root:not([data-theme="light"]) header .logo img{content:url("logo-header-light.png")}
}
@media(max-width:420px){.logo img{height:28px}}

nav{display:flex;align-items:center;gap:clamp(12px,1.8vw,26px)}
nav a{font-family:var(--mono);font-size:12.5px;letter-spacing:.02em}
nav a:hover{text-decoration:underline;text-underline-offset:5px}
/* The site's primary call to action, so it carries the accent gradient like
   .btn.solid rather than sitting black in the corner - the two are the same
   button in two places and they should not disagree. */
nav .pill{
  background:var(--grad-accent);background-size:180% 100%;color:#fff;
  padding:11px 21px;border-radius:100px;
  font-weight:600;font-family:var(--disp);font-size:14px;letter-spacing:-.01em;
  box-shadow:var(--sh-1),inset 0 1px 0 rgba(237,235,228,.14);
  transition:background .22s ease,box-shadow .28s ease,transform 120ms var(--ease);
}
nav .pill:hover{
  background-position:100% 0;text-decoration:none;
  box-shadow:0 14px 34px -16px rgba(51,64,138,.75),inset 0 1px 0 rgba(255,255,255,.24);
}

/* ---- theme toggle ----
   First in the nav so it survives every breakpoint: the nav links hide below
   820px and the CTA pill below 520px, but this stays.

   The icon shown is the theme you would switch TO, not the one you are in -
   a moon on a light page, a sun on a dark one. The opposite convention
   ("show what you're in") reads as a status light and people do not click it.

   The swap is pure CSS keyed off the same three states as the palette, so
   the correct icon is painted before any script runs. */
.themetoggle{
  width:38px;height:38px;flex:none;
  border:none;background:transparent;color:inherit;cursor:pointer;
  border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  transition:background .2s ease,transform 120ms var(--ease);
}
.themetoggle:hover{background:var(--fill-2)}
.themetoggle:active{transform:scale(.92)}
.themetoggle svg{width:19px;height:19px;display:block}
.themetoggle .i-sun{display:none}
:root[data-theme="dark"] .themetoggle .i-sun{display:block}
:root[data-theme="dark"] .themetoggle .i-moon{display:none}
@media(prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .themetoggle .i-sun{display:block}
  :root:not([data-theme="light"]) .themetoggle .i-moon{display:none}
}

/* ---- share ----
   The button is created by app.js only where navigator.share actually
   exists, so there is never a dead control on a browser that cannot use it. */
.sdactions{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.btn.share{cursor:pointer;font-family:var(--disp)}
.btn.share svg{width:16px;height:16px}

/* ---- character counter ----
   Created by app.js on the brief textarea, revealed only past 80% of the
   limit. Uses --muted rather than an alarm colour until it is genuinely
   nearly full: a counter that looks like an error from the moment it
   appears reads as a telling-off. */
.charcount{
  margin-top:6px;
  font-family:var(--mono);
  font-size:.72rem;letter-spacing:.04em;
  color:var(--muted);
  text-align:right;
}
.charcount.low{color:var(--err)}

/* Turnstile sits between the brief and the send button. The widget draws
   itself at a fixed 300x65 and cannot be restyled, so this only gives it
   room and stops it stretching the form's rhythm. */
.cf-turnstile{margin:4px 0 -4px}

/* mobile nav - hamburger, full-width sheet under the sticky header.
   --headh is measured in JS so the sheet always clears the real header. */
.navtoggle{
  display:none;width:42px;height:42px;margin-right:-8px;
  border:none;background:transparent;color:inherit;cursor:pointer;
  flex-direction:column;align-items:center;justify-content:center;gap:5px;
  border-radius:12px;
}
.navtoggle span{
  display:block;width:20px;height:2px;background:var(--ink);border-radius:2px;
  transition:transform .28s var(--ease),opacity .16s ease;
}
.navtoggle[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}
.navtoggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.navtoggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}

.navscrim{
  position:fixed;inset:0;z-index:50;background:rgba(10,9,13,.46);
  backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);
  opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s;
}
.navscrim.open{opacity:1;visibility:visible}
.navpanel{
  position:fixed;left:0;right:0;top:0;z-index:55;
  background:var(--sand);border-radius:0 0 var(--r) var(--r);
  padding:calc(var(--headh,76px) + 14px) var(--gut) 30px;
  transform:translateY(-101%);visibility:hidden;
  transition:transform .44s var(--ease),visibility .44s;
  box-shadow:var(--sh-3);
  max-height:100dvh;overflow-y:auto;
}
.navpanel.open{transform:none;visibility:visible}
.navpanel a{display:block;border-bottom:1px solid var(--line)}
.navpanel .big{
  font-weight:800;font-size:clamp(27px,7.5vw,40px);letter-spacing:-.035em;line-height:1.08;
  padding-block:18px;display:flex;align-items:baseline;justify-content:space-between;gap:14px;
}
.navpanel .big .n{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;opacity:.45;font-weight:400}
.navpanel .big:last-of-type{color:var(--accent-ink)}
.navmeta{display:grid;gap:2px;margin-top:24px}
.navmeta a{
  border-bottom:none;background:var(--fill);border-radius:var(--r-sm);padding:13px 18px;
  display:flex;justify-content:space-between;gap:14px;align-items:baseline;flex-wrap:wrap;
}
.navmeta a:hover{background:var(--ink);color:var(--sand)}
.navmeta .k{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;opacity:.6}
.navmeta .v{font-weight:700;font-size:15.5px}
body.navopen{overflow:hidden}

@media(max-width:820px){
  nav .d{display:none}
  .navtoggle{display:flex}
}
@media(max-width:520px){
  nav .pill{display:none}
}
@media(min-width:821px){
  .navpanel,.navscrim{display:none}
}

/* skip link - first tab stop, otherwise the rail swallows the focus */
.skip{
  position:absolute;left:var(--gut);top:8px;z-index:100;
  transform:translateY(-160%);
  background:var(--ink);color:var(--sand);
  padding:12px 20px;border-radius:100px;
  font-family:var(--mono);font-size:12.5px;letter-spacing:.02em;
  transition:transform .2s var(--ease);
}
.skip:focus-visible{transform:none}

/* current page marker in the nav */
nav a[aria-current="page"]{text-decoration:underline;text-underline-offset:5px;opacity:.62}
/* the pill inherits nav a's current-page underline, which on a filled pill
   reads as a strikethrough rather than as a marker */
nav .pill[aria-current="page"]{opacity:1;background:var(--accent);text-decoration:none}
.navpanel a[aria-current="page"] .n{opacity:1;color:var(--accent-ink)}

/* ============================================================
   BUTTONS
   ============================================================ */

.btn{
  border-radius:100px;padding:14px 26px;
  font-weight:600;font-size:14.5px;letter-spacing:-.005em;
  display:inline-flex;align-items:center;gap:9px;
  transition:background .22s ease,color .22s ease,border-color .22s ease,
             box-shadow .3s ease,transform 120ms var(--ease);
}
/* gradient fill rather than flat orange - the fill is the one place the
   brand is allowed to look lit. Background-position shifts on hover so the
   gradient itself moves, which costs nothing and reads as a material. */
.btn.solid{
  color:#fff;background:var(--grad-accent);background-size:180% 100%;
  box-shadow:0 10px 26px -16px rgba(51,64,138,.8),inset 0 1px 0 rgba(255,255,255,.26);
  transition:background-position .5s var(--ease),box-shadow .3s ease,transform 120ms var(--ease);
}
.btn.solid:hover{background-position:100% 0;box-shadow:0 18px 44px -18px rgba(51,64,138,.85),inset 0 1px 0 rgba(255,255,255,.32)}
.btn.out{
  border:1.5px solid var(--edge-lit);color:var(--on-dark);
  background:linear-gradient(180deg,rgba(237,235,228,.09),rgba(237,235,228,.02));
}
.btn.out:hover{border-color:var(--on-dark);background:var(--on-dark);color:var(--void)}
/* .btn.out placed on paper rather than on a dark block */
.sec .btn.out,.phead .btn.out,.legal .btn.out,.nflinks .btn.out{
  border-color:var(--line-2);color:var(--ink);
  background:linear-gradient(180deg,var(--fill),transparent);
}
.sec .btn.out:hover,.phead .btn.out:hover,.legal .btn.out:hover,.nflinks .btn.out:hover{
  background:var(--ink);color:var(--sand);border-color:var(--ink);
}

/* ============================================================
   HERO
   ============================================================ */

.hero{padding-block:clamp(34px,4.5vw,64px) clamp(20px,3vw,36px)}
.hgrid{display:grid;gap:16px}
@media(min-width:1000px){.hgrid{grid-template-columns:1.55fr 1fr;align-items:stretch}}

/* The hero is a SOLID colour block, not black with light pools - the client
   asked for solid colour, and the black read as the old red-on-black site.
   --hero and --hero-hi are literals, not var(--indigo)/var(--amber): those
   flip in dark mode, and this block must look the same in both themes.
     --hero     indigo ground. --on-dark on it is 7.9:1.
     --hero-hi  light amber for the "brand" word and the eyebrow: 5.5:1.
                Brand amber (#E8A33D) itself only reaches 4.3:1 here, which
                fails for the 11.5px eyebrow - so it is used as a FILL (the
                button, dark type on it at 8.6:1), never as small text. */
.hmain{
  --hero:#33408A;--hero-hi:#F2BE6A;
  position:relative;overflow:hidden;isolation:isolate;
  background:var(--hero);color:var(--on-dark);
  border-radius:var(--r);
  padding:clamp(28px,4vw,60px);
  display:flex;flex-direction:column;justify-content:space-between;gap:36px;
  min-height:clamp(340px,46vw,520px);
  box-shadow:var(--sh-dark),var(--lip);
}
/* no light pools: they are what made the block read as black-with-a-glow.
   The grain below stays - it is texture, not colour. */
.hmain::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  opacity:.5;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
}

/* mono eyebrow with a gradient hairline running into it */
.tagline,.kick{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;
  display:flex;align-items:center;gap:11px;
}
.tagline{color:var(--accent-lit)}
.kick{color:var(--accent-ink)}
.tagline::before,.kick::before{
  content:"";width:28px;height:1px;flex:none;
  background:linear-gradient(90deg,currentColor,transparent);
}

h1{font-weight:900;font-size:clamp(40px,6.6vw,92px);line-height:.94;letter-spacing:-.04em}
/* gradient display type, with the solid colour left underneath as the
   fallback for engines without background-clip:text */
h1 .o{color:var(--accent)}
/* .y sits on the dark hero, where a second red would fight the first. It
   stays paper-coloured and lets the gradient on .o carry the accent. */
h1 .y{color:var(--accent)}
.hmain h1 .y{color:var(--on-dark)}
.hmain h1 .o{color:var(--hero-hi)}
.hmain .tagline{color:var(--hero-hi)}
/* the page's indigo-gradient button would vanish on an indigo ground */
.hmain .btn.solid{
  background:#E8A33D;color:var(--on-light);
  box-shadow:0 10px 26px -16px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.35);
}
.hmain .btn.solid:hover{background:#F2BE6A;box-shadow:0 18px 40px -18px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.4)}
.hmain .btn.out:hover{color:var(--hero)}
.hsub{max-width:46ch;font-size:clamp(15px,1.2vw,17.5px);color:var(--dim)}
.hcta{display:flex;flex-wrap:wrap;gap:10px}

.hside{display:grid;grid-template-rows:1fr 1fr;gap:16px}
.mini{
  position:relative;overflow:hidden;
  border-radius:var(--r);padding:clamp(20px,2.4vw,30px);
  display:flex;flex-direction:column;justify-content:space-between;gap:20px;
  min-height:150px;box-shadow:var(--sh-2),var(--lip);
}
.mini.a{background:var(--indigo);color:#fff}
.mini.b{background:var(--teal);color:#fff}
.mini .lab{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;opacity:.82}
.mini .big{font-weight:800;font-size:clamp(20px,2.2vw,28px);line-height:1.08;letter-spacing:-.025em}

/* ============================================================
   SECTIONS
   ============================================================ */

.sec{padding-block:clamp(44px,6vw,90px)}
.shead{display:grid;gap:14px;margin-bottom:clamp(24px,3vw,40px)}
@media(min-width:920px){.shead{grid-template-columns:1fr 1fr;gap:60px;align-items:end}}
h2{font-weight:900;font-size:clamp(30px,4.6vw,62px);line-height:.99;letter-spacing:-.038em;margin-top:12px}
.snote{font-size:16.5px;color:var(--muted);max-width:54ch}

/* ============================================================
   THE RAIL  (index.html)
   SIGNATURE: 1:1 tracking, rubber-band edges, momentum projection
   on flick. Drag it, throw it, grab it mid-flight.
   ============================================================ */

.rail{
  position:relative;overflow:hidden;
  margin-inline:calc(var(--gut) * -1);padding-inline:var(--gut);
  cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none;
}
.rail.dragging{cursor:grabbing}
.rail.dragging .tile{transition:none}
.rail.dragging .tile:hover{transform:none;translate:none;box-shadow:var(--sh-2),var(--lip)}
.board{display:flex;gap:14px;will-change:transform;padding-block:4px}
.railbar{height:3px;background:var(--line);border-radius:99px;margin-top:22px;overflow:hidden}
.railbar i{display:block;height:100%;background:var(--grad-accent);border-radius:99px;transform-origin:left center;will-change:transform}
.railhint{
  display:flex;align-items:center;gap:10px;margin-top:12px;
  font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2);
}
.railhint .arrow{display:inline-block;animation:nudge 2.4s cubic-bezier(.4,0,.2,1) infinite}
@keyframes nudge{0%,60%,100%{transform:translateX(0)}75%{transform:translateX(6px)}}

.tile{
  position:relative;overflow:hidden;
  border-radius:var(--r);
  padding:clamp(20px,2.2vw,28px);
  min-height:clamp(190px,20vw,250px);
  display:flex;flex-direction:column;justify-content:space-between;gap:18px;
  color:#fff;
  flex:0 0 clamp(228px,25vw,300px);
  box-shadow:var(--sh-2),var(--lip);
  transition:transform .24s var(--ease),box-shadow .3s ease;
}
.tile.wide{flex-basis:clamp(280px,36vw,430px)}
/* the transform here is the fallback; supporting engines get `translate`
   instead - see "independent transforms" below. */
.tile:hover{transform:translateY(-6px);box-shadow:var(--sh-3),var(--lip)}
.tile .n{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;opacity:.78}
.tile h3{font-weight:800;font-size:clamp(20px,2vw,27px);line-height:1.05;letter-spacing:-.03em}
.tile p{font-size:13.6px;line-height:1.5;opacity:.86}
.tile .n,.tile > div,.tile .go{position:relative;z-index:1}
.tile > div{max-width:88%}
/* the arrow is the affordance that says a tile is a link, not a swatch */
.tile .go{
  font-size:19px;line-height:1;align-self:flex-start;
  opacity:.55;transition:opacity .24s ease,transform .28s var(--ease);
}
.tile:hover .go{opacity:1;transform:translateX(5px)}

/* a specular wash that lifts on hover. CSS-only - nothing tracks the
   pointer, because that would mean another JS listener per tile. */
.tile::after,.vcard::after,.sdhero::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(148deg,rgba(255,255,255,.16),rgba(255,255,255,0) 42%,rgba(0,0,0,.14));
  opacity:.75;transition:opacity .3s ease;
}
.tile:hover::after{opacity:1}

/* Six grounds: four colours, plus paper and ink. The nine verticals cycle
   through them so no two adjacent tiles share a ground.

   TWO OF THESE ARE LIGHT TONES - .t-amber and .t-sand - and that is the one
   thing to remember here. Any component that sets color:#fff and is declared
   AFTER this block has to hand the ink back for both of them, or it renders
   white type on a light card. .vcard, .sdhero and .wplate all do exactly
   that; the rules are beside each of them. Add a fifth colour and this is the
   list to check.

   .t-red and .t-deep are kept because red is still the brand, and a red tile
   is occasionally the right answer - the CTA band, a campaign page. The nine
   verticals no longer use them. */
.t-red{background:var(--red)}
.t-deep{background:var(--deep)}
.t-teal{background:var(--teal)}
.t-indigo{background:var(--indigo)}
.t-clay{background:var(--clay)}
.t-amber{background:var(--amber);color:var(--on-light)}
.t-ink{background:var(--void)}
.t-sand{background:var(--sand-2);color:var(--ink)}
.tile.t-sand,.tile.t-amber{box-shadow:var(--sh-2),inset 0 1px 0 var(--lip-light)}

/* ---- tile artwork ----
   Line motifs drawn in currentColor, so a tile with ink text (amber,
   sand) gets ink artwork and a dark tile gets light artwork for free. */
.tile .art{
  position:absolute;right:-14px;bottom:-16px;
  width:132px;height:132px;
  opacity:.26;pointer-events:none;
  transition:transform .5s var(--ease),opacity .3s ease;
}
.tile .art svg,.vcard .art svg,.wplate .art svg,.sdhero .art svg{width:100%;height:100%;display:block}
.tile:hover .art{transform:translate(-4px,-6px) rotate(-3deg);opacity:.4}

/* ---- photo tiles ----
   The nine verticals carry a real photograph - the client's own sites and
   portfolio - instead of a line motif. The photo fills the tile and a scrim
   fades it into the tile's OWN tone at the bottom, where the type sits, so
   the rail still reads as nine colours rather than nine photos. The tone is
   darkened first (--mix of it into near-black): white type on raw amber or
   paper would fail, and every tile's type is white here. Light tones mix in
   less. Without color-mix the scrim is plain dark, which is still legible.
   The tone class stays on the tile as the ground a failed image falls to. */
.tile.photo{color:#fff}
.tile.photo .art{display:none}
.photo.t-indigo{--tone:var(--indigo)}
.photo.t-teal{--tone:var(--teal)}
.photo.t-clay{--tone:var(--clay)}
.photo.t-amber{--tone:var(--amber)}
.photo.t-ink{--tone:var(--void)}
.photo.t-sand{--tone:var(--sand-2)}
.tile.photo{--mix:48%}
.tile.photo.t-amber{--mix:40%}
.tile.photo.t-sand{--mix:18%}
.tphoto{
  position:absolute;inset:0;z-index:0;width:100%;height:100%;object-fit:cover;
  -webkit-user-drag:none;user-select:none;
  transition:scale .8s var(--ease);
}
/* The type is pinned to the bottom (the number stays top), so the scrim can
   be strong exactly where the text is and leave the top of the photo clear:
   a photograph that fades down into the tile's colour, like the service cards. */
.tile.photo{justify-content:flex-end}
.tile.photo .n{margin-bottom:auto}
.tile.photo h3{text-shadow:0 1px 14px rgba(0,0,0,.28)}
.tile.photo::before{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(180deg,rgba(10,9,13,.42) 0%,rgba(10,9,13,.04) 22%,rgba(10,9,13,.62) 46%,rgba(10,9,13,.92) 66%,rgba(10,9,13,.95) 100%);
}
@supports (color:color-mix(in srgb,red 50%,blue)){
  .tile.photo{--shade:color-mix(in srgb,var(--tone) var(--mix),#0A090D)}
  .tile.photo::before{
    background:linear-gradient(180deg,
      rgba(10,9,13,.42) 0%,rgba(10,9,13,.04) 22%,
      color-mix(in srgb,var(--shade) 66%,transparent) 46%,
      color-mix(in srgb,var(--shade) 94%,transparent) 66%,
      color-mix(in srgb,var(--shade) 97%,transparent) 100%);
  }
}
.tile.photo p{opacity:.94}
/* the photo leans in on hover, slower than the tile lifts - depth, not a jump */
.tile.photo:hover .tphoto{scale:1.05}
.rail.dragging .tile.photo:hover .tphoto{scale:1}
/* focus points for crops: the board sits high in the site photos */
.pos-t{object-position:center 18%}
.pos-u{object-position:center 32%}
.pos-m{object-position:center 50%}
.pos-l{object-position:center 62%}

/* ============================================================
   SPLIT BAND
   ============================================================ */

.band{
  position:relative;overflow:hidden;isolation:isolate;
  background:var(--void);color:var(--on-dark);
  border-radius:var(--r);padding:clamp(28px,4vw,64px);
  box-shadow:var(--sh-dark),var(--lip);
}
.band::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(58% 70% at 92% 4%, rgba(15,122,117,.30), transparent 64%),
    radial-gradient(58% 70% at 2% 98%, rgba(51,64,138,.34), transparent 66%);
}
.band h2{color:var(--on-dark)}
.band .kick{color:var(--accent-lit)}
.band .snote{color:var(--dim)}
.two{display:grid;gap:14px;margin-top:clamp(24px,3vw,40px)}
@media(min-width:860px){.two{grid-template-columns:1fr 1fr}}
/* glass, not a flat tint - blur plus the hairline lip */
.pane{
  background:linear-gradient(165deg,rgba(237,235,228,.085),rgba(237,235,228,.02));
  border:1px solid var(--edge);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  box-shadow:var(--lip);
  border-radius:14px;padding:clamp(22px,2.6vw,34px);
}
.pane h3{font-weight:800;font-size:clamp(19px,2vw,26px);letter-spacing:-.025em;margin-bottom:16px}
.pane ul{list-style:none;display:grid;gap:9px}
.pane li{font-size:14.8px;color:var(--dim);display:flex;gap:10px}
.pane li::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--accent-lit);flex:none;transform:translateY(6px)}
.pane.on li::before{background:var(--sand)}

/* ============================================================
   CTA BLOCK
   ============================================================ */

/* Was a large yellow panel - the loudest thing on the site and the furthest
   from the brand. It is now the same ink ground as the hero and the band, so
   the red button on it is the only saturated thing in view. */
.cta-block{
  position:relative;overflow:hidden;isolation:isolate;
  background:var(--void);color:var(--on-dark);border-radius:var(--r);
  padding:clamp(30px,4.5vw,70px);display:grid;gap:28px;
  box-shadow:var(--sh-dark),var(--lip);
}
.cta-block::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(56% 70% at 4% 4%, rgba(168,85,58,.34), transparent 66%),
    radial-gradient(56% 70% at 96% 96%, rgba(51,64,138,.30), transparent 66%);
}
.cta-block .kick{color:var(--accent-lit)}
.cta-block .snote{color:var(--dim)}
@media(min-width:900px){.cta-block{grid-template-columns:1.15fr 1fr;align-items:center}}
.cta-block h2{color:var(--on-dark)}
.clist{display:grid;gap:2px}
.clist > a,.clist > span{
  background:rgba(237,235,228,.07);border:1px solid var(--edge);
  border-radius:var(--r-sm);padding:15px 20px;
  display:flex;justify-content:space-between;gap:16px;align-items:baseline;flex-wrap:wrap;
}
.clist > a{transition:background .2s ease,color .2s ease,transform 120ms var(--ease)}
.clist > a:hover{background:var(--accent);border-color:var(--accent);color:#fff}
.clist .k{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;opacity:.65}
.clist .v{font-weight:700;font-size:16.5px}

/* ============================================================
   FOOTER
   ============================================================ */

footer{
  position:relative;overflow:hidden;isolation:isolate;
  background:var(--void);color:var(--on-dark);
  margin-top:clamp(40px,5vw,80px);
  padding-block:clamp(44px,5vw,72px) 30px;
}
footer::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(46% 80% at 8% 0%, rgba(51,64,138,.24), transparent 66%),
    radial-gradient(46% 80% at 92% 100%, rgba(12,110,109,.22), transparent 66%);
}
.fgrid{display:grid;gap:clamp(28px,3.5vw,48px)}
@media(min-width:720px){.fgrid{grid-template-columns:1fr 1fr}}
@media(min-width:1040px){.fgrid{grid-template-columns:1.5fr 1fr 1fr 1fr;gap:clamp(24px,3vw,56px)}}
.fbrand .logo{color:var(--on-dark)}
.fbrand p{margin-top:16px;max-width:34ch;font-size:14.6px;color:var(--dim-2);line-height:1.62}
.fcontact{display:grid;gap:6px;margin-top:20px}
.fcontact a{
  font-weight:700;font-size:clamp(15.5px,1.5vw,18px);letter-spacing:-.02em;
  width:fit-content;transition:color .2s ease;
}
.fcontact a:hover{color:var(--accent-lit)}
.fcol{display:flex;flex-direction:column;gap:9px;align-items:flex-start}
.fcol h2{
  font-family:var(--mono);font-weight:400;
  font-size:11px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--dim-2);margin:0 0 6px;line-height:1.4;
}
.fcol a{
  font-size:14.4px;color:var(--dim);
  transition:color .2s ease,transform .2s var(--ease);
}
.fcol a:hover{color:var(--on-dark);transform:translateX(3px)}
.fcol a[aria-current="page"]{color:var(--on-dark);text-decoration:underline;text-underline-offset:4px}
.fb{
  display:flex;flex-wrap:wrap;gap:14px;justify-content:space-between;
  font-family:var(--mono);font-size:11.5px;letter-spacing:.06em;color:var(--dim-2);
  border-top:1px solid var(--edge);margin-top:clamp(34px,4vw,56px);padding-top:22px;
}
.fnav{display:flex;flex-wrap:wrap;gap:18px}
.fnav a:hover{text-decoration:underline;text-underline-offset:4px}

/* ============================================================
   MOTION LAYER
   ============================================================ */

/* will-change here is a HINT FOR THE PENDING ANIMATION ONLY. It promotes a
   compositor layer per revealed element (nine of them on services.html), so
   app.js removes it on `transitionend` once the element has landed. Keep the
   declaration, keep that cleanup - deleting the cleanup as a "simplification"
   leaves every revealed element on its own layer for the life of the page. */
.rv{opacity:0;transform:translateY(20px);will-change:transform,opacity}
.rv.in{
  opacity:1;transform:none;
  transition:opacity .58s var(--ease),transform .74s var(--ease),translate .74s var(--ease);
  transition-delay:calc(var(--i,0)*65ms);
}
[data-press]{transition:transform 110ms cubic-bezier(.2,.9,.3,1),scale 110ms cubic-bezier(.2,.9,.3,1)}
[data-press].pressed{transform:scale(.97)}

/* ---- independent transforms ----
   Reveal (translateY), hover lift (translateY) and press feedback (scale) all
   used to be written to the single `transform` property, so whichever rule won
   the cascade erased the others: a pressed tile lost its hover lift, and a
   press landing mid-reveal cancelled the reveal outright.
   `translate` / `scale` / `rotate` are separate animatable properties that
   compose (translate, then rotate, then scale, then transform), which is the
   native equivalent of Motion's independent transforms. The `transform`
   declarations above stay as the fallback for engines without them; inside this
   @supports block transform is zeroed first, because otherwise the two would
   ADD rather than override and every offset would double.
   Order matters here: .tile:hover has the same specificity as .rv.in, so it has
   to come after it or a revealed tile would refuse to lift. */
@supports (translate:0) and (scale:1){
  .rv{transform:none;translate:0 20px}
  .rv.in{transform:none;translate:none}
  .tile{transition:translate .24s var(--ease),box-shadow .3s ease}
  .tile:hover{transform:none;translate:0 -6px}
  .wcard:hover{transform:none;translate:0 -5px}
  .vcard:hover{transform:none;translate:0 -4px}
  .snav a:hover{transform:none;translate:0 -3px}
  .tile:hover .go{transform:none;translate:5px 0}
  .fcol a:hover{transform:none;translate:3px 0}
  .rail.dragging .tile:hover{translate:none}
  [data-press].pressed{transform:none;scale:.97}
}

/* ============================================================
   PAGE HEADER (subpages)
   ============================================================ */

.phead{padding-block:clamp(30px,5vw,72px) clamp(22px,3vw,40px)}
.crumb{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.11em;text-transform:uppercase;
  color:var(--muted-2);margin-bottom:16px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;
}
.crumb a:hover{text-decoration:underline;text-underline-offset:4px;color:var(--ink)}
.crumb > span{opacity:.55}
.phead h1{font-size:clamp(38px,6.4vw,84px);line-height:.95;letter-spacing:-.04em;font-weight:900}
.phead .lede{max-width:58ch;margin-top:22px;font-size:clamp(16px,1.35vw,19px);color:var(--muted);line-height:1.6}
.updated{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2);margin-top:18px}

/* ============================================================
   SERVICES INDEX
   ============================================================ */

.vgrid{display:grid;gap:14px}
@media(min-width:760px){.vgrid{grid-template-columns:1fr 1fr}}
.vcard{
  border-radius:var(--r);padding:var(--vp);
  color:#fff;position:relative;overflow:hidden;
  display:flex;flex-direction:column;gap:20px;scroll-margin-top:120px;
  box-shadow:var(--sh-2),var(--lip);
  transition:transform .24s var(--ease),box-shadow .3s ease;
}
.vcard:hover{transform:translateY(-4px);box-shadow:var(--sh-3),var(--lip)}
.vcard.t-sand,.vcard.t-amber{box-shadow:var(--sh-2),inset 0 1px 0 var(--lip-light)}
/* .vcard / .sdhero / .wplate all set color:#fff, and all three are declared
   after the .t-* palette, so the two light tones need their ink handed back
   explicitly or they render white type on a light card. */
.vcard.t-sand{color:var(--ink)}
.vcard.t-amber{color:var(--on-light)}
.vcard .art{position:absolute;right:-18px;top:-18px;width:150px;height:150px;opacity:.2;pointer-events:none}
/* On the services page the photo is a banner across the top of the card,
   not behind it: a card carries eight bullet points, and no scrim keeps
   that much small type legible over a photograph. The banner bleeds to the
   card's edges (the negative margin undoes the padding) and fades into the
   card's own ground, so the card below is unchanged, amber and paper included. */
.vcard{--vp:clamp(24px,2.8vw,38px)}
.vcard.photo .art{display:none}
.vphoto{
  position:relative;display:block;overflow:hidden;
  margin:calc(var(--vp) * -1) calc(var(--vp) * -1) 0;
  height:clamp(150px,17vw,220px);
}
.vphoto img{display:block;width:100%;height:100%;object-fit:cover;transition:scale .8s var(--ease)}
.vphoto::after{
  content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(10,9,13,.12) 0%,transparent 40%,var(--tone) 100%);
}
.vcard.photo:hover .vphoto img{scale:1.04}
.vcard > *{position:relative;z-index:1}
.vcard .n{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;opacity:.78}
.vcard h2{font-size:clamp(24px,2.6vw,36px);line-height:1.03;letter-spacing:-.035em;margin-top:0}
.vcard ul{list-style:none;display:grid;gap:8px}
.vcard li{font-size:14.6px;display:flex;gap:10px;opacity:.92}
.vcard li::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none;transform:translateY(7px);opacity:.6}
/* the read-more line at the foot of each vertical card */
.vmore{
  margin-top:auto;
  font-family:var(--mono);font-size:12.5px;letter-spacing:.02em;
  display:inline-flex;align-items:center;gap:8px;
  border-bottom:1px solid currentColor;width:fit-content;
  padding-bottom:3px;opacity:.85;
  transition:opacity .2s ease,gap .24s var(--ease);
}
.vmore:hover{opacity:1;gap:13px}

/* ============================================================
   SERVICE DETAIL PAGES
   ============================================================ */

.sdhero{
  position:relative;overflow:hidden;
  border-radius:var(--r);
  padding:clamp(30px,4.6vw,68px);
  color:#fff;
  display:flex;flex-direction:column;align-items:flex-start;gap:20px;
  box-shadow:var(--sh-2),var(--lip);
}
.sdhero.t-sand,.sdhero.t-amber{box-shadow:var(--sh-2),inset 0 1px 0 var(--lip-light)}
.sdhero.t-sand{color:var(--ink)}
.sdhero.t-amber{color:var(--on-light)}
.sdhero > *{position:relative;z-index:1}
.sdhero .art{position:absolute;right:-26px;top:-30px;width:min(280px,42vw);height:min(280px,42vw);opacity:.18;pointer-events:none;z-index:0}
.sdno{font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;opacity:.8}
.sdhero h1{font-size:clamp(36px,5.6vw,74px);line-height:.95;letter-spacing:-.04em;font-weight:900;max-width:16ch}
.sdhero .lede{max-width:60ch;font-size:clamp(15.5px,1.3vw,18.5px);line-height:1.6;opacity:.9}
.sdhero .btn.solid{
  background:rgba(20,19,24,.9);color:#fff;background-image:none;
  box-shadow:0 12px 30px -18px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.18);
}
.sdhero .btn.solid:hover{background:var(--ink)}
/* on the dark grounds an ink button disappears into the hero, so those two
   tones keep the gradient fill instead */
.sdhero.t-ink .btn.solid,.sdhero.t-deep .btn.solid{
  background:var(--grad-accent);background-size:180% 100%;
  box-shadow:0 10px 26px -16px rgba(51,64,138,.8),inset 0 1px 0 rgba(255,255,255,.26);
}
.sdhero.t-ink .btn.solid:hover,.sdhero.t-deep .btn.solid:hover{background-position:100% 0}
.sdhero.t-sand .lede{opacity:.82}

/* two columns: the argument, and a sidebar that stays with it */
.sdbody .wrap{display:grid;gap:clamp(28px,4vw,64px)}
@media(min-width:1000px){.sdbody .wrap{grid-template-columns:minmax(0,1fr) clamp(280px,28vw,360px);align-items:start}}
.sdmain{max-width:70ch}
.sdmain h2{font-size:clamp(23px,2.5vw,34px);line-height:1.08;letter-spacing:-.03em;margin-top:clamp(30px,3.4vw,50px)}
.sdmain h2:first-child{margin-top:0}
.sdmain p{margin-top:16px;font-size:16.5px;line-height:1.68;color:var(--prose)}
.sdmain ol{margin-top:20px;padding-left:0;list-style:none;counter-reset:step}
.sdmain ol li{
  counter-increment:step;position:relative;
  padding-left:46px;padding-block:13px;
  border-top:1px solid var(--line);
  font-size:15.6px;line-height:1.6;color:var(--prose);
}
.sdmain ol li:last-child{border-bottom:1px solid var(--line)}
.sdmain ol li::before{
  content:counter(step,decimal-leading-zero);
  position:absolute;left:0;top:15px;
  font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;
  color:var(--accent-ink);
}
.sdmain ol strong{font-weight:800;letter-spacing:-.015em}
/* the client's own service list - set as chips, not bullets, because it is
   a menu of deliverables rather than a sequence */
.slist{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin-top:20px}
.slist li{
  background:var(--fill);border:1px solid var(--line);
  border-radius:100px;padding:9px 17px;
  font-size:14.2px;letter-spacing:-.005em;
}

.sdaside{
  position:relative;
  border-radius:var(--r);padding:clamp(24px,2.6vw,34px);
  background:var(--sand-2);border:1px solid var(--line);
  box-shadow:var(--sh-1);
  display:flex;flex-direction:column;gap:16px;
}
@media(min-width:1000px){.sdaside{position:sticky;top:calc(var(--headh,76px) + 20px)}}
.sdaside h3{font-weight:800;font-size:clamp(18px,1.8vw,22px);letter-spacing:-.025em;line-height:1.15}
.sdaside p{font-size:14.8px;line-height:1.62;color:var(--muted)}
.sdaside ul{list-style:none;display:grid;gap:8px}
.sdaside li{font-size:14.4px;color:var(--muted);display:flex;gap:10px;line-height:1.5}
.sdaside li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none;transform:translateY(7px)}
.sdaside .btn{align-self:flex-start;margin-top:4px}
/* the sidebar's closing line reuses .mini, which elsewhere is a filled hero
   card. Strip it back to plain text here. */
.sdaside .mini{
  min-height:0;padding:0;background:none;box-shadow:none;border-radius:0;
  display:block;font-family:var(--mono);font-size:12px;letter-spacing:.02em;color:var(--muted-2);
}
.sdaside .mini a{border-bottom:1px solid var(--line-2);padding-bottom:2px}
.sdaside .mini a:hover{color:var(--ink);border-color:var(--ink)}

/* ---- site photographs (outdoor-advertising.html) ----
   Real photographs from the client's media decks, built by
   tools/make-site-photos.py. Sand-page rules apply: a hairline and a
   caption, no glass or glow - the photograph is the decoration.
   aspect-ratio matches the 3:2 the script crops to, so a 696px master
   and a 1080px one occupy the same box. */
.sites{padding-top:0}
.sites h2{font-size:clamp(26px,3.2vw,44px);letter-spacing:-.035em}
.sitesnote{margin-top:12px;max-width:66ch;font-size:15.8px;line-height:1.68;color:var(--muted)}
.sitegrid{
  display:grid;gap:clamp(14px,1.8vw,24px);margin-top:clamp(22px,2.6vw,36px);
  grid-template-columns:repeat(auto-fill,minmax(min(100%,340px),1fr));
}
.sitegrid figure{margin:0}
.sitegrid img{
  display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;
  border-radius:var(--r-sm);background:var(--fill);
}
.sitegrid figcaption{
  display:flex;flex-direction:column;gap:3px;
  padding-top:12px;margin-top:12px;border-top:1px solid var(--line-2);
}
.sitegrid figcaption strong{font-size:16px;letter-spacing:-.015em}
.sitegrid figcaption span{font-family:var(--mono);font-size:12px;letter-spacing:.02em;color:var(--muted)}
/* The retail gallery's captions carry a sentence, not a spec line, so the
   description is set as text; the credit stays small and mono. These are
   STOCK photographs, and the credit line is also what says so under each. */
.retail{padding-top:clamp(8px,1.6vw,24px)}
.sitegrid figcaption .rdesc{font-family:var(--disp);font-size:14.5px;letter-spacing:0;line-height:1.5;color:var(--muted)}
.sitegrid figcaption .credit{font-size:10.5px;color:var(--muted-2);margin-top:4px}

/* ---- portfolio (work.html, and a four-card strip on four service pages) ----
   Stills from the client's own reels and concert photographs, built by
   tools/make-work-media.py. 4:5 portrait, because every source is a 9:16
   reel; two columns on a phone rather than one tall card per screen.
   Same sand-page rules as the site photographs: hairline and caption, the
   picture is the decoration. [data-clip] marks the cards that get a video
   loop once the R2 bucket is attached (PORTFOLIO.md) - nothing reads it yet. */
.foliotabs{display:flex;flex-wrap:wrap;gap:8px}
#foliofilter{margin:clamp(18px,2vw,26px) 0 0}
.foliotabs a{
  font-family:var(--mono);font-size:12px;letter-spacing:.02em;
  padding:9px 14px;border:1px solid var(--line-2);border-radius:999px;color:var(--ink);
}
.foliotabs a:hover{border-color:var(--ink)}
.foliogrp{margin-top:clamp(34px,4vw,56px);scroll-margin-top:calc(var(--headh,72px) + 16px)}
.foliogrp h3{font-size:clamp(20px,2.1vw,27px);letter-spacing:-.028em}
.fdesc{margin-top:8px;max-width:62ch;font-size:15.5px;line-height:1.62;color:var(--muted)}
.fstrip h2{font-size:clamp(26px,3.2vw,44px);letter-spacing:-.035em}
.foliogrid{
  display:grid;gap:clamp(12px,1.6vw,20px);margin-top:clamp(18px,2vw,26px);
  grid-template-columns:repeat(2,minmax(0,1fr));
}
@media (min-width:700px){.foliogrid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}
/* the home page shows exactly one picture per group: four across, never a gap */
@media (min-width:900px){.fhome .foliogrid{grid-template-columns:repeat(4,minmax(0,1fr))}}
.foliogrid figure{margin:0}
/* The card is a link to the full-size still, so it works with no script;
   app.js turns the click into the viewer sheet. Press, lean and zoom are
   the same three responses the site cards give, so a picture that opens
   something feels the same everywhere on the site. */
.folink{
  position:relative;display:block;overflow:hidden;border-radius:var(--r-sm);
  background:var(--fill);color:inherit;
}
.folink:focus-visible{outline:3px solid var(--accent-ink);outline-offset:4px}
.foliogrid img{
  display:block;width:100%;height:auto;aspect-ratio:4/5;object-fit:cover;
  transition:transform .6s var(--ease);
}
@media(hover:hover){.folink:hover img{transform:scale(1.035)}}
.foliogrid figcaption{
  display:flex;flex-direction:column;gap:3px;
  padding-top:10px;margin-top:10px;border-top:1px solid var(--line-2);
}
.foliogrid figcaption strong{font-size:15px;letter-spacing:-.015em;line-height:1.3}
.foliogrid figcaption span{font-family:var(--mono);font-size:11.5px;letter-spacing:.02em;line-height:1.45;color:var(--muted)}
.foliogrid figcaption .credit{font-size:10.5px;color:var(--muted-2);margin-top:2px}

/* ============================================================
   SITES PAGE (sites.html)
   Every position in the client's two media decks. The page is sand, so it
   stays editorial - hairlines, generous measure, the photograph doing the
   work. The only material on it is the small chrome that floats OVER a
   photograph (photo count, gallery arrows, close), and that is exactly where
   a translucent layer earns its place: it has to read against a picture it
   does not control.
   ============================================================ */
.sitestats{margin-top:clamp(22px,3vw,34px)}
/* Services and Work have no photographs to show, so their header's right
   half gets the verticals' own tones and motifs as a small tilted mosaic -
   tiles on a table, seen from above. Same 1000px cut-off as the photo stack.
   Each tile lifts under the pointer; the tilt is static, not animated. */
.phead-deco .wrap{position:relative}
.phmosaic{display:none}
@media(min-width:1000px){
  .phmosaic{
    display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(10px,1vw,16px);
    position:absolute;right:calc(var(--gut) + 4vw);top:clamp(0px,1vw,20px);
    width:clamp(250px,24vw,360px);
    transform:perspective(1400px) rotateX(38deg) rotateZ(-24deg);
    transform-origin:60% 40%;
  }
  .phmosaic span{
    aspect-ratio:1;border-radius:clamp(12px,1.2vw,18px);color:#fff;
    display:grid;place-items:center;
    box-shadow:0 1px 0 rgba(255,255,255,.25) inset,0 2px 0 rgba(20,19,24,.18),0 22px 34px -18px rgba(20,19,24,.55);
    transition:transform .45s var(--ease),box-shadow .45s var(--ease);
  }
  .phmosaic span.t-amber{color:var(--on-light)}
  .phmosaic span.t-sand{color:var(--ink)}
  .phmosaic svg{width:46%;height:46%;opacity:.9}
  .phmosaic span:hover{transform:translateZ(0) translateY(-8px);box-shadow:0 1px 0 rgba(255,255,255,.25) inset,0 2px 0 rgba(20,19,24,.18),0 34px 44px -18px rgba(20,19,24,.5)}
  .phmosaic.m6{width:clamp(240px,23vw,340px)}
  .phead-deco h1,.phead-deco .lede{max-width:min(58ch,54vw)}
}

/* The header's empty right half gets three real photographs, fanned. They
   are prints, so they get a print's white border and a shadow with depth,
   and they fan a little further under the pointer. Hidden below 1000px:
   on a phone the heading already fills the width. */
.phead-sites .wrap{position:relative}
.phstack{display:none}
@media(min-width:1000px){
  .phstack{
    display:block;position:absolute;right:var(--gut);top:clamp(10px,2vw,40px);
    width:clamp(300px,31vw,440px);aspect-ratio:3/2.35;
  }
  .phstack img{
    position:absolute;left:0;top:0;width:86%;height:auto;aspect-ratio:3/2;object-fit:cover;
    border:7px solid #FBFAF6;border-radius:6px;
    box-shadow:0 1px 2px rgba(20,19,24,.12),0 24px 50px -22px rgba(20,19,24,.55);
    transition:transform .6s var(--ease);
  }
  .phstack img:nth-child(1){transform:translate(22%,-4%) rotate(8deg)}
  .phstack img:nth-child(2){transform:translate(-8%,8%) rotate(-7deg)}
  .phstack img:nth-child(3){transform:translate(8%,30%) rotate(1.5deg)}
  .phead-sites:hover .phstack img:nth-child(1){transform:translate(27%,-8%) rotate(10deg)}
  .phead-sites:hover .phstack img:nth-child(2){transform:translate(-13%,6%) rotate(-9deg)}
  .phead-sites:hover .phstack img:nth-child(3){transform:translate(8%,33%) rotate(1deg)}
  .phead-sites h1,.phead-sites .lede,.phead-sites .sitestats{max-width:min(58ch,52vw)}
}

/* Segmented control. Built by app.js - with no script every corridor is
   simply listed, and there is nothing to filter. The thumb is ONE element
   moved by a spring, not a background on the active button, so switching
   reads as the same object travelling rather than one pill vanishing and
   another appearing. */
.segwrap{margin-bottom:clamp(26px,3.4vw,44px)}
.seg{
  position:relative;display:inline-flex;gap:2px;padding:4px;
  border-radius:100px;background:var(--fill);
  box-shadow:inset 0 0 0 1px var(--line);
  max-width:100%;overflow-x:auto;scrollbar-width:none;
}
.seg::-webkit-scrollbar{display:none}
.segthumb{
  position:absolute;top:4px;bottom:4px;left:0;width:0;border-radius:100px;
  background:var(--ink);pointer-events:none;
  box-shadow:0 6px 16px -8px rgba(20,19,24,.5);
}
.seg button{
  position:relative;z-index:1;flex:none;
  font:inherit;font-weight:600;font-size:14px;letter-spacing:-.005em;
  padding:10px 18px;border:0;border-radius:100px;background:none;color:var(--muted);
  cursor:pointer;transition:color .2s ease;
}
.seg button .n{font-family:var(--mono);font-size:11px;margin-left:6px;opacity:.7}
.seg button[aria-pressed="true"]{color:var(--sand)}
.seg button:focus-visible{outline:2px solid var(--accent-ink);outline-offset:2px}
/* A segmented control does not wrap and should not scroll. On a phone it
   takes the full width and the segments share it; the counts stay, smaller. */
@media(max-width:560px){
  .seg{display:flex;width:100%}
  .seg button{flex:1 1 auto;padding:9px 8px;font-size:13.5px}
  .seg button .n{margin-left:4px;font-size:10px}
  /* the portfolio has five segments, not four: the counts are what give */
  #foliofilter .seg button{padding:9px 6px}
  #foliofilter .seg button .n{display:none}
}
@media(max-width:359px){#foliofilter .seg button{padding:8px 4px;font-size:12px;letter-spacing:-.02em}}

.sitegroup + .sitegroup{margin-top:clamp(44px,6vw,84px)}
.sghead{
  display:grid;gap:8px 28px;align-items:end;
  padding-bottom:clamp(16px,2vw,24px);margin-bottom:clamp(18px,2.4vw,30px);
  border-bottom:1px solid var(--line-2);
}
@media(min-width:820px){.sghead{grid-template-columns:auto 1fr auto}}
.sghead h2{font-size:clamp(24px,2.8vw,38px);letter-spacing:-.035em;line-height:1}
.sghead p{max-width:62ch;font-size:15px;line-height:1.6;color:var(--muted)}
.sgcount{font-family:var(--mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-2)}

.sitelist{
  display:grid;gap:clamp(22px,2.4vw,34px) clamp(14px,1.8vw,24px);
  grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));
}
.sitecard{display:flex;flex-direction:column;gap:14px;color:inherit;border-radius:var(--r-sm)}
.sitecard:focus-visible{outline:3px solid var(--accent-ink);outline-offset:4px}
.sitethumb{
  position:relative;display:block;overflow:hidden;border-radius:var(--r-sm);
  background:var(--fill);aspect-ratio:3/2;
}
.sitethumb img{
  display:block;width:100%;height:100%;object-fit:cover;
  transition:transform .6s var(--ease);
}
@media(hover:hover){.sitecard:hover .sitethumb img{transform:scale(1.035)}}
.sitecount{
  position:absolute;left:10px;bottom:10px;
  font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  padding:6px 10px;border-radius:100px;color:#fff;
  background:rgba(20,19,24,.42);
  -webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.18);
}
.sitetxt{display:flex;flex-direction:column;gap:4px}
/* The corridor is already the heading above the grid, so the card does not
   repeat it. It stays in the markup because the sheet reads it from here. */
.sitekick{display:none}
.sitesx{padding-top:clamp(6px,1.4vw,18px)}
.siteplace{font-size:clamp(17px,1.5vw,20px);font-weight:800;letter-spacing:-.022em;line-height:1.2}
.sitemeta{font-family:var(--mono);font-size:12.5px;letter-spacing:.01em;color:var(--muted)}
.sitesfoot{margin-top:clamp(40px,5vw,64px);max-width:66ch;font-size:14px;line-height:1.6;color:var(--muted-2)}
.sitesmore{margin-top:clamp(22px,2.6vw,34px)}

/* ---- campaign planner (index.html) ----
   One question at a time, the way a setup assistant asks: fewer choices on
   screen, and each answer visibly narrows the next. Steps slide in from the
   side they are going (forward from the right, back from the left) on a
   spring in app.js; the progress pips fill as you go. */
.planner{
  display:grid;gap:clamp(22px,3vw,48px);
  padding:clamp(24px,3.4vw,52px);border-radius:var(--r);
  background:var(--sand-2);border:1px solid var(--line);box-shadow:var(--sh-2);
}
@media(min-width:960px){.planner{grid-template-columns:minmax(0,.8fr) minmax(0,1.4fr)}}
.plhead h2{font-size:clamp(28px,3.4vw,48px);letter-spacing:-.04em;line-height:1;margin-top:12px}
.plsub{margin-top:14px;max-width:40ch;font-size:15px;line-height:1.6;color:var(--muted)}
.plprog{display:flex;gap:6px;list-style:none;margin:22px 0 0;padding:0}
.plprog li{width:34px;height:5px;border-radius:5px;background:var(--line-2);transition:background .3s ease,width .4s var(--ease)}
.plprog li.on{background:var(--accent-ink);width:52px}
.plbody{position:relative;min-height:320px;display:flex;flex-direction:column}
.plstep h3{font-size:clamp(20px,2vw,26px);letter-spacing:-.025em;display:flex;flex-direction:column;gap:6px;outline:none}
.pln{font-family:var(--mono);font-size:11.5px;font-weight:400;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2)}
.plopts{display:grid;gap:10px;margin-top:18px}
@media(min-width:560px){.plopts{grid-template-columns:1fr 1fr}}
.plopt{
  font:inherit;font-size:15.5px;font-weight:600;text-align:left;cursor:pointer;
  padding:16px 18px;border-radius:14px;border:1.5px solid var(--line-2);
  background:var(--sand);color:var(--ink);
  transition:border-color .2s ease,background .2s ease,translate .25s var(--ease);
}
.plopt:hover{border-color:var(--accent-ink)}
@media(hover:hover){.plopt:hover{translate:0 -2px}}
.plopt:focus-visible{outline:2px solid var(--accent-ink);outline-offset:2px}
.plopt[aria-pressed="true"]{background:var(--ink);color:var(--sand);border-color:var(--ink)}
.plsum{margin-top:8px;font-size:14.5px;color:var(--muted);line-height:1.55}
.plres-list{display:grid;gap:10px;margin-top:16px}
@media(min-width:560px){.plres-list{grid-template-columns:1fr 1fr}}
.plres{
  display:flex;flex-direction:column;gap:6px;padding:16px 18px;border-radius:14px;color:#fff;
  box-shadow:var(--sh-1);transition:translate .25s var(--ease),box-shadow .3s ease;
}
.plres.t-amber{color:var(--on-light)}
.plres.t-sand{color:var(--ink);border:1px solid var(--line)}
@media(hover:hover){.plres:hover{translate:0 -3px;box-shadow:var(--sh-2)}}
.plres:focus-visible{outline:2px solid var(--accent-ink);outline-offset:3px}
.plres .n{font-family:var(--mono);font-size:11px;letter-spacing:.1em;opacity:.8}
.plres strong{font-size:17px;letter-spacing:-.02em}
.plres .d{font-size:13.5px;line-height:1.45;opacity:.9}
.plnote{margin-top:14px;padding:12px 14px;border-radius:12px;background:var(--fill);font-size:14px;line-height:1.55;color:var(--muted)}
.plnote a{color:var(--accent-ink);font-weight:700;border-bottom:1.5px solid currentColor}
.plcta{margin-top:18px}
.plnav{display:flex;gap:16px;margin-top:auto;padding-top:18px}
.plback,.plagain{font:inherit;font-size:14px;font-weight:600;background:none;border:0;padding:6px 0;cursor:pointer;color:var(--muted);border-bottom:1.5px solid var(--line-2)}
.plback:hover,.plagain:hover{color:var(--ink);border-color:var(--ink)}

/* ---- your ad on a real site (sites.html) ----
   The photo is the stage; .adface is a flat rectangle at the board's real
   ratio, 1000px wide, warped onto the board's four measured corners by a
   matrix3d from app.js. It is laid out at 1000px so its type is set at poster
   scale and simply scaled down by the warp - sizes below are in that space. */
.adjump{margin-top:14px}
.adjump a{font-weight:700;color:var(--accent-ink);border-bottom:1.5px solid currentColor;padding-bottom:2px}
.adsec{padding-top:clamp(20px,3vw,44px)}
.adhead h2{font-size:clamp(26px,3.2vw,44px);letter-spacing:-.035em}
.adhead p{margin-top:10px;max-width:64ch;font-size:15px;line-height:1.6;color:var(--muted)}
.adgrid{display:grid;gap:clamp(18px,2.4vw,32px);margin-top:clamp(20px,2.6vw,34px);align-items:start}
@media(min-width:1000px){.adgrid{grid-template-columns:minmax(0,1.6fr) minmax(300px,1fr)}}
.adstage{
  position:relative;overflow:hidden;border-radius:var(--r);aspect-ratio:3/2;
  background:#15141A;box-shadow:var(--sh-2);
}
.adphoto{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:opacity .25s ease}
.adstage.swap .adphoto,.adstage.swap .adface{opacity:0}
.adface{
  position:absolute;left:0;top:0;width:1000px;overflow:hidden;
  transform-origin:0 0;background:#33408A;color:#fff;
  box-shadow:inset 0 0 0 6px rgba(0,0,0,.35);
  transition:opacity .25s ease;
  /* the photographs are hazy daylight; a crisp screen-bright ad would read
     as pasted on. Knock it back to sit in the same light. */
  filter:saturate(.88) brightness(.93) contrast(.95);
}
.aduser{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#fff}
.aduser.contain{object-fit:contain}
.adplace{
  position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;
  padding:0 60px;gap:18px;
  background:linear-gradient(120deg,#3F4FB0,#33408A 55%,#0B6360);
}
.adbrand{font-weight:900;font-size:128px;line-height:.95;letter-spacing:-.04em;overflow-wrap:anywhere}
.adsub{font-family:var(--mono);font-size:30px;letter-spacing:.12em;text-transform:uppercase;opacity:.85}
.adface.tall .adbrand{font-size:150px}
.adface.tall .adplace{padding:80px 60px}
.adface.has-img .adplace{display:none}
.adgloss{position:absolute;inset:0;pointer-events:none;background:linear-gradient(115deg,rgba(255,255,255,.14),rgba(255,255,255,0) 40%,rgba(0,0,0,.18))}

.adctl{display:flex;flex-direction:column;gap:12px}
.adlabel{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2);margin-top:4px}
.adsites{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}
.adsite{
  display:flex;gap:10px;align-items:center;text-align:left;cursor:pointer;
  padding:6px;border-radius:12px;border:1.5px solid var(--line);background:var(--fill);color:inherit;font:inherit;
  transition:border-color .2s ease,background .2s ease;
}
.adsite img{width:52px;height:36px;object-fit:cover;border-radius:6px;flex:none}
.adsite span{display:flex;flex-direction:column;font-family:var(--mono);font-size:10.5px;color:var(--muted);line-height:1.3}
.adsite strong{font-family:var(--disp);font-size:13px;color:var(--ink);letter-spacing:-.01em}
.adsite:hover{border-color:var(--line-2)}
.adsite[aria-pressed="true"]{border-color:var(--accent-ink);background:color-mix(in srgb,var(--accent) 10%,transparent)}
.adsite:focus-visible,.adfit button:focus-visible{outline:2px solid var(--accent-ink);outline-offset:2px}
.adinputs{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.adfile{cursor:pointer;font-family:var(--disp)}
.adfile:focus-within{outline:2px solid var(--accent-ink);outline-offset:3px}
.adtext{flex:1 1 180px}
.adtext input{
  width:100%;font:inherit;font-size:15px;padding:13px 16px;border-radius:100px;
  border:1.5px solid var(--line-2);background:var(--fill);color:var(--ink);
}
.adtext input:focus{outline:2px solid var(--accent-ink);outline-offset:1px;border-color:transparent}
.adfit{display:flex;flex-wrap:wrap;gap:6px}
.adfit button{
  font:inherit;font-size:13px;font-weight:600;padding:8px 14px;border-radius:100px;cursor:pointer;
  border:1.5px solid var(--line-2);background:none;color:var(--muted);
}
.adfit button[aria-pressed="true"]{background:var(--ink);color:var(--sand);border-color:var(--ink)}
.adfit .adclear{color:var(--err,#B3261E);border-color:transparent;text-decoration:underline;text-underline-offset:3px}
.admsg{min-height:1.2em;font-size:13.5px;color:var(--muted)}
.admsg.bad{color:var(--err,#B3261E)}
.adask{align-self:flex-start;border-color:var(--line-2);color:var(--ink)}

/* ---- content-admin output: case studies and availability ----
   Only ever rendered by worker/render.js; the static files carry none of it
   (the Work page's case section ships empty and hidden). */
.sitefree{
  position:absolute;right:10px;top:10px;
  font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  padding:6px 10px;border-radius:100px;color:var(--on-light);background:#F2BE6A;
}
.cases{padding-top:0}
.casegrid{display:grid;gap:clamp(14px,1.8vw,24px);margin-top:clamp(22px,2.6vw,34px)}
@media(min-width:760px){.casegrid{grid-template-columns:1fr 1fr}}
.casecard{border-radius:var(--r);overflow:hidden;background:var(--sand-2);border:1px solid var(--line);box-shadow:var(--sh-1)}
.casehit{display:flex;flex-direction:column;color:inherit;height:100%}
.casehit .sitethumb{border-radius:0}
.casehit:focus-visible{outline:3px solid var(--accent-ink);outline-offset:3px}
.casetxt{display:flex;flex-direction:column;gap:6px;padding:clamp(18px,2vw,26px)}
.casetxt strong{font-size:clamp(19px,1.8vw,24px);letter-spacing:-.025em;line-height:1.15}
.casesum{font-size:14.6px;line-height:1.55;color:var(--muted)}
.casemeta{margin-top:14px;font-family:var(--mono);font-size:12.5px;letter-spacing:.04em;color:var(--muted)}
.casestats{margin-top:clamp(20px,2.6vw,32px)}
.casephotos{margin-top:0}
.casebody{max-width:68ch;margin-top:clamp(28px,3.4vw,48px)}
.casebody p{font-size:clamp(16px,1.25vw,18px);line-height:1.7;color:var(--prose,var(--muted))}
.casebody p + p{margin-top:1em}
.casecta{display:flex;flex-wrap:wrap;gap:10px;margin-top:clamp(28px,3.4vw,44px)}

/* ---- the corridor map (sites.html) ----
   A diagram, drawn in CSS: each corridor is a road with its sites as stops.
   Deliberately NOT a tile map - that would be a third-party request, a new
   paragraph in privacy.html, and coordinates the decks do not give us. Each
   stop is a real link to #site-<id> (app.js opens the sheet from it; without
   script the browser jumps to the card), and the preview is pure CSS, so it
   works on hover and on keyboard focus alike. On a phone the road turns
   vertical and the preview is dropped: there is no hover to show it on. */
.rmapsec{padding-top:clamp(16px,2vw,32px);padding-bottom:clamp(8px,1.6vw,20px)}
.rmhead{display:grid;gap:14px 40px;align-items:end;margin-bottom:clamp(20px,2.6vw,34px)}
@media(min-width:900px){.rmhead{grid-template-columns:1fr auto}}
.rmhead h2{font-size:clamp(26px,3.2vw,44px);letter-spacing:-.035em}
.rmhead p{margin-top:10px;max-width:64ch;font-size:15px;line-height:1.6;color:var(--muted)}
.rkey{display:flex;gap:18px;list-style:none;margin:0;padding:0;font-family:var(--mono);font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.rkey li{display:flex;align-items:center;gap:8px}
.rkey li::before{content:"";width:12px;height:12px;border-radius:50%;background:var(--rc);box-shadow:0 0 0 3px color-mix(in srgb,var(--rc) 22%,transparent)}
.rkey .lit,.rstop.lit{--rc:#E8A33D}
.rkey .nonlit,.rstop.nonlit{--rc:#4A57A8}
.rkey .na,.rstop.na{--rc:#8E8D99}

.rmap{
  display:grid;gap:clamp(14px,1.6vw,20px);
  padding:clamp(18px,2.4vw,32px);border-radius:var(--r);
  background:var(--sand-2);border:1px solid var(--line);box-shadow:var(--sh-1);
}
.rline{display:grid;gap:10px}
.rline + .rline{padding-top:clamp(14px,1.6vw,20px);border-top:1px dashed var(--line-2)}
.rname{font-size:15px;font-weight:800;letter-spacing:-.015em;display:flex;gap:10px;align-items:baseline}
.rname span{font-family:var(--mono);font-size:11px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-2)}

.rstops{
  --pin:20px;
  position:relative;list-style:none;margin:0;padding:6px 0 0;
  display:flex;justify-content:space-around;gap:6px;
}
/* the road: asphalt with a dashed centre line, drawn through the pins */
.rstops::before,.rstops::after{
  content:"";position:absolute;left:0;right:0;pointer-events:none;
  top:calc(6px + var(--pin) / 2);transform-origin:left center;
}
.rstops::before{height:12px;margin-top:-6px;border-radius:6px;background:#2A2931}
.rstops::after{height:2px;margin-top:-1px;background:repeating-linear-gradient(90deg,rgba(255,255,255,.55) 0 14px,transparent 14px 26px)}
.rstop{position:relative;flex:1 1 0;min-width:0;display:flex;justify-content:center}
.rdot{display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;color:inherit;border-radius:12px;padding:0 4px 4px}
.rdot:focus-visible{outline:2px solid var(--accent-ink);outline-offset:4px}
.rpin{
  position:relative;z-index:1;width:var(--pin);height:var(--pin);border-radius:50%;
  background:var(--rc);border:3px solid var(--sand-2);
  box-shadow:0 0 0 2px var(--rc),0 4px 10px rgba(20,19,24,.35);
  transition:scale .3s var(--ease),box-shadow .3s ease;
}
.rstop.lit .rpin{box-shadow:0 0 0 2px var(--rc),0 0 16px 4px rgba(232,163,61,.55)}
.rdot:hover .rpin,.rdot:focus-visible .rpin{scale:1.35}
.rlab{display:flex;flex-direction:column;gap:2px;line-height:1.2}
.rlab strong{font-size:13px;font-weight:700;letter-spacing:-.01em}
.rlab span{font-family:var(--mono);font-size:11px;color:var(--muted)}

/* The preview grows out of the stop it belongs to (transform-origin at its
   foot), so it is plainly that stop's photograph, and leaves the same way. */
.rpop{
  position:absolute;z-index:5;bottom:calc(100% - 4px);left:50%;
  width:220px;translate:-50% 0;scale:.86;opacity:0;visibility:hidden;
  transform-origin:50% 100%;pointer-events:none;
  background:var(--panel);color:var(--on-dark);border-radius:14px;overflow:hidden;
  box-shadow:0 24px 48px -18px rgba(0,0,0,.6);
  transition:opacity .2s ease,scale .3s var(--ease),visibility 0s linear .3s;
}
.rpop img{display:block;width:100%;height:auto;aspect-ratio:3/2;object-fit:cover}
.rpop > span{display:flex;flex-direction:column;gap:3px;padding:10px 12px 12px;font-family:var(--mono);font-size:11px;color:var(--dim)}
.rpop strong{font-family:var(--disp);font-size:13.5px;color:var(--on-dark);letter-spacing:-.01em}
@media(hover:hover){
  .rstop:hover .rpop,.rdot:focus-visible + .rpop{opacity:1;scale:1;visibility:visible;transition:opacity .2s ease,scale .3s var(--ease),visibility 0s}
}
/* keep the end previews on screen */
.rstop:first-child .rpop{left:0;translate:0 0;transform-origin:15% 100%}
.rstop:last-child .rpop{left:auto;right:0;translate:0 0;transform-origin:85% 100%}

/* the road draws itself in, then the stops drop onto it */
.rline.rv .rstops::before,.rline.rv .rstops::after{scale:0 1}
.rline.rv .rpin{scale:0}
.rline.rv.in .rstops::before,.rline.rv.in .rstops::after{scale:1 1;transition:scale .9s var(--ease)}
.rline.rv.in .rpin{scale:1;transition:scale .5s var(--ease) calc(.35s + var(--k,0) * 70ms)}
.rline.rv.in .rdot:hover .rpin,.rline.rv.in .rdot:focus-visible .rpin{scale:1.35;transition-delay:0s}
.rstop:nth-child(2){--k:1}.rstop:nth-child(3){--k:2}.rstop:nth-child(4){--k:3}
.rstop:nth-child(5){--k:4}.rstop:nth-child(6){--k:5}.rstop:nth-child(7){--k:6}.rstop:nth-child(8){--k:7}

@media(max-width:759px){
  .rstops{flex-direction:column;justify-content:flex-start;gap:14px;padding:0 0 0 2px}
  .rstops::before{top:0;bottom:0;left:calc(var(--pin) / 2 + 2px);right:auto;width:12px;height:auto;margin:0 0 0 -6px;transform-origin:center top}
  .rstops::after{top:0;bottom:0;left:calc(var(--pin) / 2 + 2px);right:auto;width:2px;height:auto;margin:0 0 0 -1px;background:repeating-linear-gradient(180deg,rgba(255,255,255,.55) 0 14px,transparent 14px 26px);transform-origin:center top}
  .rline.rv .rstops::before,.rline.rv .rstops::after{scale:1 0}
  .rline.rv.in .rstops::before,.rline.rv.in .rstops::after{scale:1 1}
  .rstop{justify-content:flex-start}
  .rdot{flex-direction:row;text-align:left;gap:14px;padding:2px 4px}
  .rpop{display:none}
}

/* ---- the site sheet ----
   A native <dialog>, opened with showModal() so focus is trapped, the page
   behind is inert and Escape arrives as a 'cancel' event - all for free.
   The dialog itself is a transparent full-viewport box; the scrim and the
   panel inside it are driven by ONE spring in app.js (the panel's offset,
   and the scrim's opacity derived from it), which is what lets the sheet
   be grabbed mid-flight and thrown away or back.
   It always enters from, and leaves to, the bottom edge: same path both
   ways, on every screen size. */
.sheet{
  position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;
  margin:0;padding:0;border:0;background:transparent;color:var(--ink);
  /* clip, not hidden. showModal() focuses a control inside the panel while
     the panel is still parked below the viewport, and a box with
     overflow:hidden is still scrollable by script - so the browser scrolled
     the dialog to reveal it and the scrim and panel opened hundreds of
     pixels out of place. clip cannot be scrolled at all. */
  overflow:hidden;overflow:clip;
}
.sheet::backdrop{background:transparent}
html.sheetopen,html.sheetopen body{overflow:hidden}
.sheetscrim{position:absolute;inset:0;background:rgba(10,9,13,.58);opacity:var(--p,0)}
.sheetpanel{
  position:absolute;left:0;right:0;bottom:0;margin-inline:auto;
  width:min(100%,1040px);max-height:calc(100dvh - 18px);
  display:flex;flex-direction:column;
  background:var(--sand);border-radius:24px 24px 0 0;overflow:hidden;
  box-shadow:0 -30px 80px -30px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);
  transform:translate3d(0,var(--y,110%),0);will-change:transform;
}
@media(min-width:900px){
  .sheetpanel{
    bottom:24px;border-radius:24px;max-height:calc(100dvh - 48px);
    display:grid;grid-template-columns:minmax(0,1.55fr) minmax(300px,1fr);
  }
}
.sheetgrab{position:absolute;top:0;left:0;right:0;z-index:3;height:22px;display:flex;justify-content:center;align-items:center;pointer-events:none}
.sheetgrab span{width:40px;height:5px;border-radius:5px;background:rgba(255,255,255,.75);box-shadow:0 1px 4px rgba(0,0,0,.3)}
@media(min-width:900px){.sheetgrab{display:none}}

/* The gallery owns its gestures in both axes - sideways pages the photos,
   downwards drags the whole sheet - so the browser is told to do nothing
   with touches that start here. The text column below scrolls natively. */
.sheetgal{position:relative;overflow:hidden;background:#15141A;aspect-ratio:3/2;flex:none;touch-action:none;cursor:grab}
.sheetgal.dragging{cursor:grabbing}
@media(min-width:900px){.sheetgal{aspect-ratio:auto;min-height:420px}}
.sheettrack{display:flex;height:100%;will-change:transform}
.sheettrack figure{flex:0 0 100%;margin:0;height:100%}
.sheettrack img{display:block;width:100%;height:100%;object-fit:cover;-webkit-user-drag:none;user-select:none}
.sheetnav,.sheetx{
  position:absolute;z-index:2;width:40px;height:40px;border-radius:50%;border:0;
  display:grid;place-items:center;color:#fff;cursor:pointer;
  background:rgba(20,19,24,.4);
  -webkit-backdrop-filter:blur(16px) saturate(170%);backdrop-filter:blur(16px) saturate(170%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.2);
  transition:background .2s ease,opacity .2s ease;
}
.sheetnav svg,.sheetx svg{width:18px;height:18px}
.sheetnav:hover,.sheetx:hover{background:rgba(20,19,24,.62)}
.sheetnav:focus-visible,.sheetx:focus-visible{outline:2px solid #fff;outline-offset:2px}
.sheetnav{top:50%;margin-top:-20px}
.sheetnav.prev{left:12px}
.sheetnav.next{right:12px}
.sheetnav[disabled]{opacity:0;pointer-events:none}
.sheetx{top:12px;right:12px}
@media(min-width:900px){.sheetx{background:var(--fill);color:var(--ink);-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:inset 0 0 0 1px var(--line)}.sheetx:hover{background:var(--line)}}
.sheetdots{position:absolute;left:0;right:0;bottom:12px;z-index:2;display:flex;justify-content:center;gap:6px}
.sheetdots i{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.5);transition:background .2s ease,transform .2s ease}
.sheetdots i.on{background:#fff;transform:scale(1.25)}

.sheetbody{
  padding:clamp(22px,3vw,40px);padding-bottom:max(22px,env(safe-area-inset-bottom));
  overflow-y:auto;overscroll-behavior:contain;
  display:flex;flex-direction:column;gap:14px;
}
@media(min-width:900px){.sheetbody{padding-top:64px}}
.sheetkick{font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--accent-ink)}
.sheetbody h2{font-size:clamp(24px,2.6vw,34px);font-weight:900;letter-spacing:-.035em;line-height:1.02}
.sheetfacts dl{margin:6px 0 0}
.sheetfacts dl div{display:grid;grid-template-columns:8.5em 1fr;gap:12px;padding:11px 0;border-top:1px solid var(--line-2)}
.sheetfacts dl div:last-child{border-bottom:1px solid var(--line-2)}
.sheetfacts dt{font-family:var(--mono);font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-2);padding-top:2px}
.sheetfacts dd{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em}
.sheetcta{display:flex;flex-wrap:wrap;gap:10px;margin-top:auto;padding-top:10px}
.sheetcta .btn.out{border-color:var(--line-2);color:var(--ink);background:linear-gradient(180deg,var(--fill),transparent)}
.sheetcta .btn.out:hover{background:var(--ink);color:var(--sand);border-color:var(--ink)}

/* ---- the portfolio viewer: the same sheet, portrait ----
   Every picture in it is 4:5, so the gallery is too. On a phone the photo
   is capped at 62% of the screen so the caption and the button are never
   pushed below the fold; on a wide screen the photo sets the panel's
   height and the text column sits beside it. The caption changes with the
   photograph (app.js), so it is set to read the same at every length. */
.sheet.portrait .sheetgal{aspect-ratio:4/5;max-height:62dvh}
.sheet.portrait .sheetbody{gap:10px}
@media(min-width:900px){
  .sheet.portrait{--gh:min(calc(100dvh - 48px),760px)}
  .sheet.portrait .sheetpanel{width:min(100%,960px);grid-template-columns:auto minmax(320px,1fr)}
  .sheet.portrait .sheetgal{height:var(--gh);width:calc(var(--gh) * .8);max-height:none;min-height:0;aspect-ratio:auto}
}
.fsmeta{font-family:var(--mono);font-size:12.5px;letter-spacing:.02em;line-height:1.5;color:var(--muted)}
.fscredit{font-family:var(--mono);font-size:11px;letter-spacing:.04em;color:var(--muted-2);display:flex;justify-content:space-between;gap:12px}
.fscount{font-variant-numeric:tabular-nums}

/* ---- FAQ ---- */
.sfaq h2{font-size:clamp(26px,3.2vw,44px);letter-spacing:-.035em;margin-bottom:clamp(20px,2.4vw,34px)}
.sfaq details{border-top:1px solid var(--line-2);max-width:74ch}
.sfaq details:last-of-type{border-bottom:1px solid var(--line-2)}
.sfaq summary{
  list-style:none;cursor:pointer;
  padding-block:20px;padding-right:42px;position:relative;
  font-weight:700;font-size:clamp(16.5px,1.6vw,20px);letter-spacing:-.02em;line-height:1.35;
}
.sfaq summary::-webkit-details-marker{display:none}
.sfaq summary::after{
  content:"";position:absolute;right:8px;top:50%;
  width:11px;height:11px;
  border-right:2px solid var(--accent);border-bottom:2px solid var(--accent);
  transform:translateY(-70%) rotate(45deg);
  transition:transform .28s var(--ease);
}
.sfaq details[open] summary::after{transform:translateY(-30%) rotate(-135deg)}
.sfaq summary:hover{color:var(--accent-ink)}
.sfaq details p{padding-bottom:22px;max-width:66ch;font-size:15.8px;line-height:1.68;color:var(--muted)}

/* ---- prev / next ---- */
.snav{display:grid;gap:12px}
@media(min-width:700px){.snav{grid-template-columns:1fr 1fr}}
.snav a{
  border:1px solid var(--line-2);border-radius:var(--r);
  padding:clamp(20px,2.2vw,28px);
  display:flex;flex-direction:column;gap:8px;
  background:linear-gradient(180deg,var(--fill),transparent);
  transition:border-color .22s ease,box-shadow .3s ease,background .22s ease,transform .22s var(--ease);
}
.snav a:hover{border-color:var(--ink);box-shadow:var(--sh-1);transform:translateY(-3px)}
.snav a:last-child{text-align:right;align-items:flex-end}
.snav .dir{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-ink)}
.snav .ttl{font-weight:800;font-size:clamp(18px,1.9vw,25px);letter-spacing:-.03em;line-height:1.1}

/* ============================================================
   WORK / PROOF
   ============================================================ */

.wgrid{display:grid;gap:14px}
@media(min-width:820px){.wgrid{grid-template-columns:1fr 1fr}}
/* A work card that goes somewhere. The link sits on the heading, where a
   screen reader expects it, and its ::after stretches over the whole card so
   the photo and the body are clickable too - one link, not three. */
.wcard.wlink{position:relative;cursor:pointer}
.wstretch{color:inherit}
.wstretch::after{content:"";position:absolute;inset:0;z-index:2;border-radius:inherit}
.wcard.wlink:focus-within{outline:3px solid var(--accent-ink);outline-offset:3px}
.wstretch:focus-visible{outline:none}
.wmore{
  display:inline-flex;align-items:center;gap:8px;margin-top:4px;
  font-weight:700;font-size:14.5px;color:var(--accent-ink);
}
.wmore::after{content:"";width:18px;height:1.5px;background:currentColor;transition:width .3s var(--ease)}
@media(hover:hover){.wcard.wlink:hover .wmore::after{width:30px}}
.wcard{
  border-radius:var(--r);overflow:hidden;background:var(--sand-2);
  border:1px solid var(--line);
  display:flex;flex-direction:column;
  box-shadow:var(--sh-1);
  transition:transform .24s var(--ease),box-shadow .3s ease;
}
.wcard:hover{transform:translateY(-5px);box-shadow:var(--sh-2)}
.wplate{
  aspect-ratio:16/10;position:relative;overflow:hidden;
  display:flex;flex-direction:column;justify-content:space-between;
  padding:clamp(20px,2.4vw,30px);color:#fff;
}
.wplate::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(150deg,rgba(255,255,255,.15),transparent 44%,rgba(0,0,0,.18));
}
/* the same light-tone correction, for the work cards */
.wplate.t-sand{color:var(--ink)}
.wplate.t-amber{color:var(--on-light)}
.wplate .cat{font-family:var(--mono);font-size:11px;letter-spacing:.11em;text-transform:uppercase;opacity:.82;position:relative;z-index:1}
.wplate .tag{font-weight:800;font-size:clamp(19px,2.2vw,29px);line-height:1.06;letter-spacing:-.032em;max-width:16ch;position:relative;z-index:1}
.wplate .art{position:absolute;right:-20px;bottom:-24px;width:180px;height:180px;opacity:.24;pointer-events:none}
/* Every work card carries a real photograph now - a site from the decks or a
   frame from the client's reels (tools/make-work-media.py, the wk- stills) -
   as a decorative .tphoto <img>, the same element and scrim as the photo
   tiles on the home rail. White type on a photo has no fixed contrast: the
   eyebrow sits on whatever is at the top, so the top is dimmed, and the
   tagline sits at the bottom, where the photo fades into a darkened copy of
   the card's tone. Two classes, so it beats the .t-* it keeps as the ground a
   failed image falls to - amber and paper included, whose ink it overrides. */
.wplate.photo{color:#fff;--mix:48%}
.wplate.photo.t-amber{--mix:40%}
.wplate.photo.t-sand{--mix:18%}
.wplate.photo::after,.wplate.photo .art{display:none}
.wplate.photo .cat{opacity:1}
.wplate.photo .tag{text-shadow:0 1px 14px rgba(0,0,0,.28)}
.wplate.photo::before{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(180deg,rgba(10,9,13,.5) 0%,rgba(10,9,13,.08) 30%,rgba(10,9,13,.5) 58%,rgba(10,9,13,.92) 100%);
}
@supports (color:color-mix(in srgb,red 50%,blue)){
  .wplate.photo{--shade:color-mix(in srgb,var(--tone) var(--mix),#0A090D)}
  .wplate.photo::before{
    background:linear-gradient(180deg,
      rgba(10,9,13,.5) 0%,rgba(10,9,13,.08) 30%,
      color-mix(in srgb,var(--shade) 55%,transparent) 58%,
      color-mix(in srgb,var(--shade) 95%,transparent) 100%);
  }
}
.wcard:hover .wplate .tphoto{scale:1.05}
.wbody{padding:clamp(20px,2.2vw,28px);display:flex;flex-direction:column;gap:14px;flex:1}
.wbody h3{font-weight:800;font-size:clamp(18px,1.7vw,23px);letter-spacing:-.025em;line-height:1.12}
.wbody p{font-size:14.6px;color:var(--muted)}
.wbody .slist{margin-top:auto}
.wstats{display:flex;flex-wrap:wrap;gap:10px;margin-top:auto;padding-top:6px}
.wstats div{background:var(--fill);border-radius:11px;padding:10px 14px;min-width:88px}
.wstats b{display:block;font-weight:900;font-size:clamp(20px,2vw,26px);letter-spacing:-.03em;line-height:1}
.wstats span{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-2)}

/* a sample-content warning that is impossible to ship by accident */
.sample{
  border:1.5px dashed var(--line-2);border-radius:14px;
  padding:16px 20px;margin-bottom:26px;
  font-family:var(--mono);font-size:12.5px;line-height:1.6;color:var(--muted-2);
  display:flex;gap:12px;align-items:flex-start;
}
.sample b{color:var(--accent-ink);font-weight:500;flex:none}

/* ============================================================
   ENQUIRY FORM
   ============================================================ */

.form{max-width:760px;display:grid;gap:20px;position:relative}
.frow{display:grid;gap:20px}
@media(min-width:640px){.frow{grid-template-columns:1fr 1fr}}
.field{display:grid;gap:7px;min-width:0}
.field label{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted);display:flex;align-items:baseline;gap:6px;
}
.req{color:var(--accent-ink)}
.field input,.field select,.field textarea{
  font-family:var(--disp);font-size:16px;color:var(--ink);
  background:var(--field-bg);
  border:1.5px solid var(--line-2);border-radius:var(--r-sm);
  padding:13px 15px;width:100%;
  transition:border-color .2s ease,box-shadow .24s ease,background .2s ease;
}
.field textarea{resize:vertical;min-height:130px;line-height:1.55}
.field select{
  appearance:none;-webkit-appearance:none;cursor:pointer;
  padding-right:42px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='M1 1l6 6 6-6' fill='none' stroke='%23141318' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 16px center;
}
.field input:hover,.field select:hover,.field textarea:hover{border-color:var(--ink)}
.field input:focus,.field select:focus,.field textarea:focus{
  outline:none;background:var(--field-bg-focus);
  border-color:var(--accent);
  box-shadow:0 0 0 4px rgba(51,64,138,.18);
}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{outline:none}
.hint{font-size:13.2px;line-height:1.55;color:var(--muted-2)}
/* .err is always in the DOM so the aria-describedby target exists from the
   start; it only takes up space once app.js writes a message into it. */
.err{font-size:13.2px;color:var(--err);font-weight:600;line-height:1.5}
.err:empty{display:none}
.field.invalid input,.field.invalid select,.field.invalid textarea{
  border-color:var(--err);background:rgba(184,45,24,.05);
}
.field.invalid input:focus,.field.invalid select:focus,.field.invalid textarea:focus{
  box-shadow:0 0 0 4px rgba(184,45,24,.16);
}
.field.invalid label{color:var(--err)}
/* honeypot: positioned off-screen rather than display:none, because some
   bots skip hidden fields but happily fill a positioned one. Not focusable
   - the markup carries tabindex="-1" and aria-hidden. */
.hp{
  position:absolute!important;left:-9999px!important;top:0!important;
  width:1px!important;height:1px!important;overflow:hidden;opacity:0;
}
.formfoot{
  display:flex;flex-wrap:wrap;align-items:center;gap:18px;
  border-top:1px solid var(--line);padding-top:24px;margin-top:6px;
}
.formfoot .btn{flex:none;border:none;cursor:pointer;font-family:var(--disp)}
.formfoot .snote{font-size:13.4px;color:var(--muted-2);max-width:46ch;flex:1 1 260px}
.formfoot .snote a{text-decoration:underline;text-underline-offset:3px}
.formstatus{
  font-size:15px;line-height:1.6;font-weight:600;
  border-radius:var(--r-sm);padding:15px 18px;
  background:var(--fill);border:1px solid var(--line);
}
.formstatus:empty{display:none}
.formstatus.ok{background:rgba(31,138,95,.1);border-color:rgba(31,138,95,.4);color:var(--ok-text)}
.formstatus.bad{background:rgba(184,45,24,.08);border-color:rgba(184,45,24,.4);color:var(--err)}

/* ============================================================
   LEGAL PAGES
   ============================================================ */

.legal{display:grid;gap:clamp(28px,4vw,60px)}
@media(min-width:1000px){
  .legal{grid-template-columns:clamp(230px,24vw,290px) minmax(0,1fr)}
  /* the table of contents is source-ordered after the prose so the document
     itself is what a reader (or a reader-mode) meets first */
  .toc{order:-1}
}
.toc{align-self:start}
@media(min-width:1000px){.toc{position:sticky;top:calc(var(--headh,76px) + 20px)}}
.toc h2{
  font-family:var(--mono);font-weight:400;font-size:11px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--muted-2);margin:0 0 14px;line-height:1.4;
}
/* the list is authored as bare <a> children inside the <nav>, so the counter
   increments on the link itself; the li rule keeps an <ol>-wrapped variant
   numbering correctly too. */
.toc{counter-reset:toc}
.toc ol,.toc ul{list-style:none;display:grid;gap:1px}
.toc > a,.toc li{counter-increment:toc}
.toc a{
  display:flex;gap:11px;
  padding:9px 12px;border-radius:9px;
  font-size:14px;line-height:1.45;color:var(--muted);
  transition:background .2s ease,color .2s ease;
}
.toc a::before{
  content:counter(toc,decimal-leading-zero);
  font-family:var(--mono);font-size:11px;color:var(--muted-2);flex:none;padding-top:2px;
}
.toc a:hover{background:var(--fill);color:var(--ink)}

.prose{max-width:72ch}
.prose h2{
  font-size:clamp(21px,2.2vw,29px);line-height:1.1;letter-spacing:-.03em;
  margin-top:clamp(34px,3.8vw,54px);scroll-margin-top:calc(var(--headh,76px) + 24px);
}
.prose h2:first-child{margin-top:0}
.prose h3{font-weight:800;font-size:clamp(16.5px,1.6vw,19px);letter-spacing:-.02em;margin-top:26px}
.prose p{margin-top:14px;font-size:16px;line-height:1.7;color:var(--prose)}
.prose ul,.prose ol{margin-top:14px;padding-left:20px;display:grid;gap:9px}
.prose li{font-size:15.6px;line-height:1.65;color:var(--prose)}
.prose li::marker{color:var(--accent-ink)}
.prose a{text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
.prose a:hover{color:var(--accent-ink)}
.prose strong{font-weight:800}
.prose table{width:100%;border-collapse:collapse;margin-top:18px;font-size:14.8px}
.prose th,.prose td{text-align:left;padding:12px 14px;border-bottom:1px solid var(--line);vertical-align:top}
.prose th{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2);font-weight:400}

/* the "this is a template, a lawyer has not reviewed it" box. Same contract
   as .sample on work.html: the caveat and the copy ship together or not at
   all. Delete the box only when a real policy replaces the text. */
.legalnote{
  border:1.5px dashed var(--line-2);border-radius:14px;
  padding:18px 22px;margin-bottom:30px;
  font-family:var(--mono);font-size:12.5px;line-height:1.65;color:var(--muted-2);
  display:flex;gap:12px;align-items:flex-start;
}
.legalnote b{color:var(--accent-ink);font-weight:500;flex:none}

/* ============================================================
   404
   ============================================================ */

.nf{min-height:64vh;display:flex;flex-direction:column;justify-content:center;gap:26px;padding-block:clamp(50px,9vw,120px)}
.nf .code{font-family:var(--mono);font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-ink)}
.nf h1{font-size:clamp(52px,13vw,150px);line-height:.86;letter-spacing:-.05em;font-weight:900}
.nf p{max-width:46ch;font-size:17px;color:var(--muted)}
.nflinks{display:flex;flex-wrap:wrap;gap:10px}

/* ============================================================
   LETTERHEAD  (letterhead.html - A4, built to print)
   Sized in mm throughout so what the browser shows and what the
   printer lays down are the same document.
   ============================================================ */

.lh-page{
  width:210mm;min-height:297mm;margin:24px auto;
  background:#fff;color:var(--ink);
  position:relative;overflow:hidden;
  display:flex;flex-direction:column;
  box-shadow:var(--sh-3);
}
.lh-band{height:7mm;flex:none;background:var(--grad-accent)}
.lh-head{
  display:flex;justify-content:space-between;align-items:flex-start;gap:14mm;
  padding:14mm 18mm 0;
}
/* The real lockup, printed. Height is fixed in mm and the width follows,
   because on paper the mark's physical size is the thing that matters. */
.lh-mark{display:flex;flex-direction:column;align-items:flex-start;gap:2.6mm}
.lh-mark img{height:19mm;width:auto;display:block}
.lh-mark .tl{font-family:var(--mono);font-size:2.5mm;letter-spacing:.2em;text-transform:uppercase;color:var(--muted-2)}
.lh-meta{text-align:right;font-family:var(--mono);font-size:2.9mm;line-height:1.85;color:var(--muted)}
.lh-meta b{display:block;font-family:var(--disp);font-weight:800;font-size:3.4mm;letter-spacing:-.01em;color:var(--ink);margin-bottom:1mm}
.lh-body{padding:12mm 18mm 0;flex:1}
.lh-kick{font-family:var(--mono);font-size:2.7mm;letter-spacing:.18em;text-transform:uppercase;color:var(--accent-ink);margin-bottom:3mm}
.lh-title{font-weight:900;font-size:8mm;line-height:1.02;letter-spacing:-.04em;max-width:22ch}
.lh-lede{margin-top:4mm;font-size:3.6mm;line-height:1.62;color:var(--muted);max-width:64ch}
/* the nine verticals, as a printable capability list */
.lh-svcs{display:grid;grid-template-columns:1fr 1fr;gap:0 10mm;margin-top:8mm}
@media(max-width:560px){.lh-svcs{grid-template-columns:1fr}}
.lh-svc{border-top:.3mm solid var(--line);padding:3.4mm 0;display:flex;gap:4mm;break-inside:avoid}
.lh-svc .no{font-family:var(--mono);font-size:2.7mm;letter-spacing:.1em;color:var(--accent-ink);padding-top:.9mm;flex:none;width:7mm}
.lh-svc .nm{font-weight:800;font-size:3.7mm;letter-spacing:-.02em;line-height:1.25}
.lh-svc .ds{font-size:3mm;line-height:1.5;color:var(--muted-2);margin-top:1mm}
.lh-foot{
  padding:8mm 18mm 10mm;margin-top:auto;
  display:flex;flex-wrap:wrap;justify-content:space-between;gap:5mm;
  font-family:var(--mono);font-size:2.7mm;letter-spacing:.05em;color:var(--muted-2);
  border-top:.3mm solid var(--line);
}
.lh-foot a{color:inherit}
.lh-tools{
  max-width:210mm;margin:0 auto;padding:26px var(--gut) 0;
  display:flex;flex-wrap:wrap;gap:12px;align-items:center;
}
.lh-tools .snote{flex:1 1 240px;font-size:13.5px}
/* below A4 width the sheet stops pretending to be paper and just reflows */
@media(max-width:840px){
  .lh-page{width:auto;min-height:0;margin:16px var(--gut)}
}

/* ============================================================
   THE TRIVISION BOARD  (index.html hero, right column)
   SIGNATURE: a working three-sided hoarding, built in CSS 3D.
   Ten triangular prisms, each carrying the same three creatives.
   They turn in a staggered wave, which is how a real trivision
   board changes ad - the drive shaft reaches one slat before the
   next. Drag the stage to orbit it; tap the board to turn it.

   Everything is prefixed tv- because .board already means the
   rail track on this site and the two must not collide.

   GEOMETRY IS MEASURED IN JS, NOT COMPUTED HERE.
   Only --tvw (board width) and --tvh (height) are CSS. The three
   values that need a division -

     --tvsw   slat width          = board width / slat count
     --tvr    prism inradius      = slat width / 2·tan60° = / 3.4641
     --tvcw   creative width      = board width
     --tvox   per-slat offset     = slat index × slat width

   - are written as pixels by measure() in app.js on load, on
   resize and on document.fonts.ready. That is deliberate: dividing
   by a var() inside calc() is legal but fragile, and the rail
   already re-measures on the same three events for the same
   reason. If the board renders with seams between its faces,
   measure() did not run - check that before touching the maths.
   ============================================================ */

.tvstage{
  /* Fallback only. measure() in app.js overwrites both with plain pixels, so
     nothing downstream depends on a clamp() nested inside a calc() inside a
     var() - which is valid CSS but the kind of chain that fails quietly. */
  --tvw:clamp(250px,38vw,440px);
  --tvh:calc(var(--tvw) * .58);

  position:relative;
  perspective:1500px;perspective-origin:50% 42%;
  display:flex;align-items:center;justify-content:center;
  min-height:clamp(340px,44vw,520px);
  /* the board is centred, but the pole and the floor plane hang below it, so
     the whole assembly is nudged up to sit inside the stage rather than
     spilling over the cards underneath */
  padding-bottom:clamp(96px,10vw,120px);
  touch-action:pan-y;
  user-select:none;-webkit-user-select:none;
}
.tvstage.ready{cursor:grab}
.tvstage.dragging{cursor:grabbing}
.tvrig{
  position:relative;transform-style:preserve-3d;
  transform:rotateX(var(--rx,-6deg)) rotateY(var(--ry,-22deg));
  will-change:transform;
}

/* floor plane and contact shadow - depth needs something to sit on,
   otherwise the structure reads as a flat sticker at an angle */
.tvfloor{
  position:absolute;left:50%;top:calc(var(--tvh) + 130px);
  width:calc(var(--tvw) * 2.4);height:calc(var(--tvw) * 2.4);
  transform:translate(-50%,-50%) rotateX(90deg);
  background:
    repeating-linear-gradient(0deg,var(--grid) 0 1px,transparent 1px 44px),
    repeating-linear-gradient(90deg,var(--grid) 0 1px,transparent 1px 44px);
  -webkit-mask-image:radial-gradient(ellipse at center,#000 5%,transparent 64%);
  mask-image:radial-gradient(ellipse at center,#000 5%,transparent 64%);
}
.tvshadow{
  position:absolute;left:50%;top:calc(var(--tvh) + 128px);
  width:calc(var(--tvw) * .5);height:calc(var(--tvw) * .27);
  transform:translate(-50%,-50%) rotateX(90deg);
  background:radial-gradient(ellipse at center,rgba(20,19,24,.4),transparent 70%);
  filter:blur(14px);
}

/* the pole: four faces, so it stays solid from any orbit angle */
/* Starts at the BOTTOM edge of the board and is pushed back in Z, so it reads
   as the structure holding the sign up rather than a bar drawn across the
   artwork. Both parts matter: top:var(--tvh) puts it below the face, and the
   negative translateZ puts it behind it at every orbit angle. */
.tvpole{
  position:absolute;left:50%;top:var(--tvh);
  width:calc(var(--tvw) * .062);height:130px;
  transform:translateX(-50%) translateZ(calc(var(--tvr,7px) * -2.2));
  transform-style:preserve-3d;
}
.tvpole span{position:absolute;inset:0;background:linear-gradient(90deg,#8E8B82,#C9C6BC 42%,#7C7A72)}
.tvpole span:nth-child(1){transform:translateZ(calc(var(--tvw) * .031))}
.tvpole span:nth-child(2){transform:rotateY(90deg) translateZ(calc(var(--tvw) * .031));background:linear-gradient(90deg,#65635C,#9B988F)}
.tvpole span:nth-child(3){transform:rotateY(180deg) translateZ(calc(var(--tvw) * .031));background:linear-gradient(90deg,#7C7A72,#AFACA2)}
.tvpole span:nth-child(4){transform:rotateY(-90deg) translateZ(calc(var(--tvw) * .031));background:linear-gradient(90deg,#5B5952,#8E8B82)}

.tvboard{
  position:relative;width:var(--tvw);height:var(--tvh);
  transform-style:preserve-3d;
}
/* THE FALLBACK POSTER.
   Authored in the HTML and drawn in plain 2D, sitting under the slats. Once
   app.js builds the ten prisms they cover it completely, so nobody normally
   sees it - but if the script fails, or the engine flattens the preserve-3d
   scene, or the geometry never gets measured, the visitor still sees a
   hoarding with a poster on it rather than an empty rectangle. Everything
   above this line can break and the hero still reads. */
.tvposter{
  position:absolute;inset:0;overflow:hidden;
  display:flex;flex-direction:column;justify-content:flex-end;gap:calc(var(--tvw) * .016);
  padding:calc(var(--tvw) * .055);
  background:#15141A url(sites/nh9-gaur-bizz-park-60x20-lg.webp) center 30%/cover;
  color:#fff;
}
/* the same scrim the creatives carry, so the fallback reads the same way */
.tvposter::before{content:"";position:absolute;inset:0;background:var(--tvscrim)}
.tvposter > *{position:relative}
.tvposter .brand{font-family:var(--mono);font-size:calc(var(--tvw) * .028);letter-spacing:.18em;text-transform:uppercase;opacity:.9}
.tvposter .head{font-weight:900;font-size:calc(var(--tvw) * .078);line-height:.96;letter-spacing:-.03em;max-width:78%}
.tvposter .foot{font-family:var(--mono);font-size:calc(var(--tvw) * .026);letter-spacing:.13em;text-transform:uppercase;opacity:.88}
.tvstage.ready .tvboard{cursor:pointer}
.tvboard:focus-visible{outline:3px solid var(--ink);outline-offset:14px;border-radius:4px}
.tvframe{
  position:absolute;inset:calc(var(--tvw) * -.018);
  border:1px solid rgba(20,19,24,.4);border-radius:3px;
  transform:translateZ(calc(var(--tvr,12px) * -1.15));
  background:linear-gradient(160deg,#3A382F,#1E1D19);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.16);
}
/* a real hoarding has floodlights above it */
.tvflood{
  position:absolute;left:8%;right:8%;top:calc(var(--tvw) * -.072);height:4px;
  transform:translateZ(calc(var(--tvr,12px) * 1.6));
  background:linear-gradient(90deg,transparent,rgba(255,231,190,.95),transparent);
  filter:blur(1px);box-shadow:0 12px 40px 6px rgba(255,214,150,.42);
}

.tvslats{position:absolute;inset:0;display:flex;transform-style:preserve-3d;z-index:1}
.tvslat{
  position:relative;
  flex:0 0 var(--tvsw,10%);height:100%;
  transform-style:preserve-3d;
  transform:rotateY(var(--turn,0deg));
  will-change:transform;
}
.tvface{
  position:absolute;inset:0;overflow:hidden;
  backface-visibility:hidden;
  box-shadow:inset -1px 0 0 rgba(0,0,0,.14);
}
.tvface:nth-child(1){transform:translateZ(var(--tvr,12px))}
.tvface:nth-child(2){transform:rotateY(120deg) translateZ(var(--tvr,12px))}
.tvface:nth-child(3){transform:rotateY(240deg) translateZ(var(--tvr,12px))}

/* Each face holds the FULL creative at board width and shifts it left by its
   own slat's offset, so ten independent prisms compose one continuous poster.
   Both values are pixels written by measure(). The fallbacks keep the board
   legible - rather than a stack of ten identical posters - if it never runs. */
.tvcreative{
  position:absolute;top:0;left:0;height:100%;
  width:var(--tvcw,1000%);
  transform:translateX(calc(var(--tvox,0px) * -1));
  display:flex;flex-direction:column;justify-content:flex-end;gap:calc(var(--tvw) * .016);
  padding:calc(var(--tvw) * .055);
  color:#fff;
}
/* The three creatives are real site photographs from the client's media
   decks (tools/make-site-photos.py) - a hoarding on the board showing the
   hoardings we sell. Each image is sized to the FULL board width, like the
   type, so ten slats still compose one picture.

   The type is white on a photograph, which has no fixed contrast: sky is
   pale, road is grey. So all the type sits at the BOTTOM, over --tvscrim,
   a gradient that is near-black where the type is and clear over the
   board in the upper half of each photo. Do not move the brand line back
   to the top - it would be white on sky. The dark ground colour under each
   url() is what shows while the photo loads, and it is dark for the same
   reason. Type never takes var(--ink)/var(--sand): the ground does not
   flip with the theme. */
.tvstage{--tvscrim:linear-gradient(180deg,rgba(10,9,13,0) 28%,rgba(10,9,13,.55) 58%,rgba(10,9,13,.9) 100%)}
.tv-1{background:var(--tvscrim),#15141A url(sites/nh9-gaur-bizz-park-60x20-lg.webp) center 30%/cover}
.tv-2{background:var(--tvscrim),#15141A url(sites/nh9-makanpur-55x20-lg.webp) center 100%/cover}
.tv-3{background:var(--tvscrim),#15141A url(sites/nh9-up-gate-30x15-lg.webp) center 100%/cover}
/* .9 / .88, not .75 / .7. Small uppercase mono at wide tracking, under
   the gloss - faded any further it drops under 4:1 even on the scrim. */
.tvcreative .brand{
  font-family:var(--mono);font-size:calc(var(--tvw) * .028);
  letter-spacing:.18em;text-transform:uppercase;opacity:.9;
}
.tvcreative .head{
  font-weight:900;font-size:calc(var(--tvw) * .078);
  line-height:.96;letter-spacing:-.03em;max-width:78%;
}
.tvcreative .foot{
  font-family:var(--mono);font-size:calc(var(--tvw) * .026);
  letter-spacing:.13em;text-transform:uppercase;opacity:.88;
}
/* Gloss across the face, so it reads as printed vinyl rather than a fill.
   NOT mix-blend-mode: a blend mode forces its element out of the parent's
   preserve-3d context in some engines, which is exactly how a 3D scene
   collapses into a flat smear. A plain translucent gradient is safe. */
.tvgloss{
  position:absolute;inset:0;pointer-events:none;
  transform:translateZ(calc(var(--tvr,12px) + 1px));
  /* The white stop is .12, not .2. It lands on the indigo corner of tv-1
     and the pale corner of tv-3 - exactly where the type is lightest and
     darkest respectively - and at .2 it was washing both out. Sheen, not
     a second light source. */
  background:linear-gradient(118deg,rgba(255,255,255,.12) 0%,rgba(255,255,255,0) 36%,rgba(0,0,0,.2) 100%);
}
.tvhint{
  position:absolute;bottom:2px;left:50%;transform:translateX(-50%);
  font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--muted-2);
  display:flex;align-items:center;gap:9px;white-space:nowrap;
}
.tvhint .d{width:5px;height:5px;border-radius:50%;background:var(--accent);animation:tvpulse 2.6s ease-in-out infinite}
@keyframes tvpulse{0%,100%{opacity:.35;transform:scale(1)}50%{opacity:1;transform:scale(1.35)}}
/* no JS, no board - say so rather than leaving an empty 500px hole */
.tvstage:not(.ready) .tvhint{display:none}

/* the pair of positioning cards under the hero */
.hstrip{display:grid;gap:16px;margin-top:16px}
@media(min-width:760px){.hstrip{grid-template-columns:1fr 1fr}}
.hstrip .mini{min-height:0;flex-direction:row;align-items:baseline;gap:18px;flex-wrap:wrap}
.hstrip .mini .lab{flex:none}
.hstrip .mini .big{flex:1 1 260px;font-size:clamp(17px,1.6vw,21px)}

/* screen-reader-only live region for the board's current panel */
.sr{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* ============================================================
   PROGRESSIVE ENHANCEMENTS
   All four elements below are created by app.js, not authored in
   the pages - the shell is copy-pasted seventeen times and none
   of this is content. With JS off none of it exists, and nothing
   is lost.
   ============================================================ */

/* reading progress - a hairline on the header's bottom edge */
.progress{
  position:absolute;left:0;right:0;bottom:0;height:2px;
  background:var(--line);overflow:hidden;display:none;
}
.progress i{
  display:block;height:100%;width:100%;
  background:var(--grad-accent);
  transform:scaleX(0);transform-origin:left center;
  will-change:transform;
}

/* the current section in a legal page's contents list */
.toc a[data-current]{
  background:var(--fill-2);color:var(--ink);font-weight:600;
}
.toc a[data-current]::before{color:var(--accent-ink)}

/* a heading jumped to from the contents list, so the eye lands on the right
   line instead of hunting for it */
.prose h2:target{
  animation:flash 1.6s var(--ease) 1;
}
@keyframes flash{
  0%,55%{background:rgba(242,194,48,.4);box-shadow:-10px 0 0 rgba(242,194,48,.4),10px 0 0 rgba(242,194,48,.4)}
  100%{background:transparent;box-shadow:none}
}

/* back to top */
.totop{
  position:fixed;right:clamp(14px,3vw,32px);bottom:clamp(14px,3vw,32px);z-index:45;
  width:46px;height:46px;border-radius:50%;border:none;cursor:pointer;
  background:var(--ink);color:var(--sand);font-size:18px;line-height:1;
  box-shadow:var(--sh-2),inset 0 1px 0 rgba(237,235,228,.16);
  opacity:0;visibility:hidden;transform:translateY(10px) scale(.9);
  transition:opacity .28s ease,visibility .28s,transform .28s var(--ease),background .2s ease;
}
.totop.on{opacity:1;visibility:visible;transform:none}
.totop:hover{background:var(--accent)}
@media print{.totop,.progress{display:none!important}}
@media print{main::before,.segwrap,.sheet,.sitecount{display:none!important}.sitegroup[hidden],.foliogrp[hidden]{display:block!important}}

/* Long documents get skipped rendering work for the parts nobody has scrolled
   to yet. contain-intrinsic-size is a placeholder height so the scrollbar
   does not jump as sections come into range - without it this optimisation
   causes exactly the layout instability it is meant to help with. */
.sfaq details,.wcard,.vcard{
  content-visibility:auto;
  contain-intrinsic-size:auto 320px;
}

/* ---- tilt & sheen ----
   Cards lean a few degrees under a mouse pointer, with a soft highlight
   that follows it. Written to the independent `rotate` property as ONE
   axis-angle rotation, so it composes with the reveal (`translate`), the
   hover lift (`translate`) and the press (`scale`) instead of fighting them
   for `transform`. Perspective lives on the parent, because a card cannot
   give itself depth without using `transform`. Mouse only: a finger has no
   hover, and tilting under a drag would fight the scroll. Off entirely
   under reduced motion (app.js never wires it; the rule below is the belt). */
.vgrid,.wgrid,.sitecard,.foliogrid figure{perspective:1100px}
.tilt{position:relative;transform-style:flat}
.tsheen{
  position:absolute;inset:0;z-index:3;pointer-events:none;border-radius:inherit;
  opacity:0;
  background:radial-gradient(circle at var(--sx,50%) var(--sy,50%),rgba(255,255,255,.26),rgba(255,255,255,0) 55%);
}

/* ============================================================
   PREFERENCE MEDIA QUERIES
   ============================================================ */

@media(prefers-reduced-transparency:reduce){
  .sitecount,.sheetnav,.sheetx{background:#141318;-webkit-backdrop-filter:none;backdrop-filter:none}
  header,header.floating{background:var(--sand);backdrop-filter:none;-webkit-backdrop-filter:none}
  .navscrim{backdrop-filter:none;-webkit-backdrop-filter:none}
  .pane{background:var(--panel);backdrop-filter:none;-webkit-backdrop-filter:none}
  body::before,body::after{display:none}
  .hmain::after{display:none}
}

@media(prefers-contrast:more){
  :root{
    --line:rgba(20,19,24,.3);--line-2:rgba(20,19,24,.5);
    --muted:#39383F;--muted-2:#4A4952;
    --edge:rgba(237,235,228,.34);--edge-lit:rgba(237,235,228,.6);
    --dim:rgba(237,235,228,.92);--dim-2:rgba(237,235,228,.78);
  }
  body::before,body::after,main::before{display:none}
  .hmain::after,.band::before,footer::before{display:none}
  .tile::after,.vcard::after,.sdhero::after,.wplate::after{display:none}
  .tvgloss,.tvfloor{display:none}
  .pane{background:var(--panel);backdrop-filter:none;-webkit-backdrop-filter:none}
}

/* Reduced motion. LOAD-BEARING - read CLAUDE.md before touching it.
   The rail IIFE in app.js returns before it wires any gestures under RM, so
   `.rail{overflow-x:auto;touch-action:auto}` is the ONLY thing that makes the
   nine tiles reachable; without it they sit behind overflow:hidden with nothing
   able to scroll them, and it looks perfectly fine with reduced motion off. */
@media(prefers-reduced-motion:reduce){
  .tilt{rotate:none!important}
  .rline .rstops::before,.rline .rstops::after,.rline .rpin{scale:1!important}
  .tsheen{display:none}
  .tphoto,.vphoto img{scale:1!important}   /* the hover zoom would otherwise jump, not glide */
  *{transition-duration:.01ms!important;animation:none!important}
  html{scroll-behavior:auto}
  .rv{opacity:1!important;transform:none!important;translate:none!important}
  .rail{overflow-x:auto;touch-action:auto}
  .board{transform:none!important}
  /* app.js already refuses to set .hidden under RM; this is the belt to it,
     because a header that slid away and never came back is unrecoverable. */
  header.hidden{transform:none!important}

  /* the trivision board. app.js never starts the auto-flip timer and never
     wires the orbit drag under RM, so the rig is parked at a fixed angle and
     the board only turns when someone asks it to - at which point Spring.step
     snaps straight to target, so the turn is instant rather than animated. */
  /* A cross-document transition is unrequested movement between pages, which
     is exactly what this setting exists to stop. The navigation still works;
     it just cuts instead of animating. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){animation:none!important}

  /* the enhancements: app.js skips the progress bar and the stat counters
     entirely under RM, so these cover the two that still render. */
  .prose h2:target{animation:none!important;background:rgba(242,194,48,.4)}
  .totop{transition:opacity .01ms!important;transform:none!important}

  .tvrig{transform:rotateX(-6deg) rotateY(-20deg)!important}
  .tvstage{cursor:auto}
  .tvhint .d{animation:none!important}

  /* multipage components */
  .snav a:hover,.vcard:hover,.wcard:hover,.tile:hover,.fcol a:hover{transform:none!important;translate:none!important}
  .tile:hover .go{transform:none!important;translate:none!important}
  .tile:hover .art{transform:none!important}
  /* <details> is a native toggle, so "instant" here just means the open state
     brings no fade/slide of its own. */
  .sfaq details[open] > *{animation:none!important;opacity:1!important}
  .sfaq summary::after{transition:none!important}
  /* Belt to app.js's braces: it skips the draw-on entirely under RM, but if a
     dash offset was set inline before the check, `*{transition-duration}` only
     kills the tween - the element would still sit at its undrawn offset. An
     !important declaration beats a JS-set inline style, so force it home. */
  .art svg [stroke-dasharray],.art svg path,.art svg circle,.art svg rect,.art svg line{
    stroke-dashoffset:0!important;
  }
}

/* ============================================================
   PRINT
   Someone will print a service page or a policy and hand it across a
   desk. Give them a document rather than a screenshot of a website.
   ============================================================ */

@media print{
  :root{--sand:#fff;--muted:#333;--muted-2:#555}
  body{background:#fff;color:#000;font-size:11pt}
  body::before,body::after{display:none}
  header,.navpanel,.navscrim,.skip,.railbar,.railhint,.navtoggle,
  .snav,.cta-block,.lh-tools,.fcol,.fcontact,.tvstage{display:none!important}
  .rv{opacity:1!important;transform:none!important;translate:none!important}
  .sec{padding-block:12pt}
  .hmain,.band,.sdhero,footer{
    background:#fff!important;color:#000!important;box-shadow:none!important;
    border:1pt solid #ccc;
  }
  .hmain::after,.band::before,footer::before{display:none!important}
  .hmain h1 .o,.hmain h1 .y{color:#000!important;-webkit-text-fill-color:#000!important;background:none!important}
  .tile,.vcard,.wcard,.mini,.pane,.sdaside{
    background:#fff!important;color:#000!important;border:1pt solid #ccc;box-shadow:none!important;
    break-inside:avoid;
  }
  .rail{overflow:visible}
  .board{display:block}
  .tile{margin-bottom:8pt}
  .art{display:none!important}
  .sfaq details{break-inside:avoid}
  .sfaq details p{display:block!important}
  .prose h2,.sdmain h2{break-after:avoid}
  a[href^="http"]::after{content:" (" attr(href) ")";font-size:8pt;color:#666;word-break:break-all}

  /* the letterhead is the one page whose print layout IS the design, so it
     keeps its colour and loses the page margin entirely */
  .lh-page{width:auto;min-height:0;margin:0;box-shadow:none}
  .lh-band,.lh-svc .no,.lh-kick{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  @page{size:A4;margin:0}
}
