/* ═══════════════════════════════════════════════════════════════════════════
   rzv.studio — work.css  (WORK agent: #work grid + work/*.html case pages)

   Loaded last, after base.css. base.css already owns the grid geometry, the
   .wt-media / .case-* boxes, the mesh recipe and the tilt sheen contract —
   everything here is choreography on top of it:

     · per-tile mesh glow that breathes, and blooms through the cover on hover
     · the cursor sheen (RZV.attachTilt writes --mx/--my onto the tile; the
       gradient lives here because .work-tile is not a .tilt card)
     · the hover reel <video> layer (work.js builds it, fine-pointer only)
     · case-page editorial flow: full-bleed / two-up figures, reel block,
       next-project link with its own mesh
     · the hover thumbnails work.js injects into #purpose rows

   THREE RULES OBEYED THROUGHOUT (base.css header):
   1. No backdrop-filter above a mix-blend-mode child — the ≤760px block drops
      every blend on this page too.
   2. GSAP owns `transform` on tiles and on parallaxed media. `transform` is
      deliberately absent from those transition lists.
   3. NOTHING here starts hidden. Every reveal is set up at runtime by work.js
      and only when RZV.motionOK() — so with motion off, or if work.js never
      runs, the grid and every case page are already fully composed.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════
   #work — the grid
   ═══════════════════════════════════════════════════════════════════════ */

/* The tile becomes the positioning + stacking context for its own glow.
   `transform` stays out of the transition list: RZV.attachTilt writes
   rotationX/rotationY/y/scale every frame (trap 2). */
.work-tile{
  isolation:isolate;
  transition:opacity .4s var(--ease);
}

/* ── Outer mesh glow ──
   Parked behind the media on z-index:-1 (attachTilt's transformPerspective
   makes the tile a stacking context, so this stays inside the tile). It
   breathes on a long alternate loop, per-tile phase-shifted by --breathe,
   which work.js writes so eight tiles never pulse in lockstep. */
.work-tile::before{
  content:'';position:absolute;z-index:-1;pointer-events:none;
  left:4%;right:4%;top:1%;height:74%;border-radius:50%;
  background:
    radial-gradient(58% 62% at 28% 38%, color-mix(in srgb, var(--m1,transparent) 62%, transparent), transparent 72%),
    radial-gradient(52% 58% at 74% 56%, color-mix(in srgb, var(--m3,transparent) 50%, transparent), transparent 72%),
    radial-gradient(46% 50% at 52% 82%, color-mix(in srgb, var(--m2,transparent) 40%, transparent), transparent 74%);
  filter:blur(48px);
  opacity:0.24;
  animation:wtBreathe 11s ease-in-out infinite alternate;
  animation-delay:var(--breathe,0s);
  transition:opacity .65s var(--ease);
}
.work-tile:hover::before,
.work-tile:focus-visible::before{opacity:0.62}
/* Scale ONLY. A running animation beats a plain declaration, so an `opacity`
   keyframe here would silently swallow the hover bump above — the swell is
   what reads as breathing anyway, and the drifting .mb blobs underneath carry
   the brightness variation. */
@keyframes wtBreathe{
  0%{transform:scale(0.94)}
  100%{transform:scale(1.08)}
}
body.phase-light .work-tile::before{filter:blur(54px) saturate(1.2)}

/* ── The media box ──
   Layer order inside .wt-media (base.css sets the box itself):
     0  .mesh          the three drifting blobs
     1  .wt-img        the cover, always underneath — the hover cycle below
                        never touches it, so it's what's left when the cycle
                        stops and every layer above it has faded back to 0
     2  .wt-cycle / .wt-reel   the hover cycle's current slide — an image or
                        the reel, never both at once, sharing the one slot
     3  ::before       mesh tint blooming through the image (screen)
     4  ::after        cursor sheen
   `transform` is on the transition list for the IMAGE only — GSAP never
   touches it, CSS owns the 1.03 hover scale. */
.wt-media{
  transition:border-color .45s var(--ease), box-shadow .55s var(--ease);
}
.work-tile:hover .wt-media{
  border-color:var(--border-hi);
  box-shadow:0 26px 60px -34px rgba(0,0,0,0.75);
  /* Scoped to :hover only, never a bare declaration on .wt-media itself — a
     permanent will-change would promote all eight tiles to their own layer
     at rest for no reason. This one keeps the shadow repaint isolated to the
     hovered tile's own layer instead of invalidating the grid section it
     paints into, and the promotion is dropped the instant hover ends. */
  will-change:box-shadow;
}
body.phase-light .work-tile:hover .wt-media{
  box-shadow:0 26px 60px -34px rgba(10,8,10,0.28);
}

.wt-img{
  transform:scale(1.001);
  transform-origin:center center;
  transition:transform .85s var(--ease), opacity .6s var(--ease), filter .7s var(--ease);
}
.work-tile:hover .wt-img{transform:scale(1.03)}

/* The tint. Screen, so it reads as light thrown across the artwork rather
   than paint laid over it — same reasoning as .mb in base.css. Its own slow
   drift keeps it from looking like a flat wash. */
/* The per-project gradient tint that sat over the thumbnail on hover is GONE —
   2 Sep: "remove the gradient tint on portfolio thumbnails." The picture is
   the picture. (The cursor sheen ::after below stays.) */

/* Cursor sheen — the .tilt::before recipe from base.css, re-pointed at
   .wt-media because a work tile is a picture + a caption, not a glass card.
   --mx/--my are written on .work-tile by RZV.attachTilt and inherit down. */
.wt-media::after{
  content:'';position:absolute;inset:0;z-index:4;pointer-events:none;opacity:0;
  background:radial-gradient(150% 150% at var(--mx,50%) var(--my,50%),
    var(--tile-sheen), rgba(255,255,255,0.05) 26%, rgba(255,255,255,0.014) 54%, transparent 80%);
  transition:opacity .45s var(--ease);
}
.work-tile:hover .wt-media::after{opacity:0.85}

/* ── Hover cycle ──
   work.js cuts through a project's images and, for six of the eight
   projects, its reel — roughly every 2.5s while the tile is hovered
   (initGrid()'s CYCLES map). Two elements share one job: `.wt-cycle` for
   stills, `.wt-reel` for the one video slide, only ever one of them at
   opacity:1 at a time. Both fade over the cover, which stays put underneath
   as the poster for every gap between slides and for when the cycle isn't
   running at all. Built on first hover and torn down (or, for the still
   layer, just left at opacity:0) on leave, so eight tiles' worth of media
   never sits in memory at once. */
.wt-reel{
  position:absolute;inset:0;z-index:2;
  width:100%;height:100%;object-fit:cover;
  opacity:0;pointer-events:none;
  transition:opacity .5s var(--ease);
}
.wt-reel.is-on{opacity:1}

/* The still-image half of the cycle — same box, same fade, as .wt-reel.
   Kept as a separate element (rather than reusing .wt-reel's slot with an
   <img>/<video> swap) because it's cheap to leave parked at opacity:0
   between hovers: unlike video there is no decode/network cost to release,
   so it never needs .wt-reel's careful create-on-hover/destroy-on-leave
   lifecycle — work.js builds it once per tile and just keeps changing its
   `src`. */
.wt-cycle{
  position:absolute;inset:0;z-index:2;
  width:100%;height:100%;object-fit:cover;
  opacity:0;pointer-events:none;
  transition:opacity .5s var(--ease);
}
.wt-cycle.is-on{opacity:1}
/* Ni'mah Health's 05 is a transparent logo mark, not a photo — work.js
   toggles this when the current slide's filename ends .png. Same "never
   crop a mark" rule as .cg-mark below, scaled down for a grid tile: contain
   instead of cover, a neutral backing instead of the bare transparency (the
   busy cover photo would otherwise show through around the logo), enough
   padding that it doesn't touch the tile's edges. */
.wt-cycle.is-mark{
  object-fit:contain;box-sizing:border-box;
  padding:14%;background:var(--surface);
}

/* Caption. The name lifts a hair with the tile; the meta warms from --dim. */
.wt-text{transition:transform .5s var(--ease)}
.wt-name{transition:color .35s var(--ease), letter-spacing .5s var(--ease)}
.work-tile:hover .wt-meta{color:var(--muted)}

/* ── CAPTION INSIDE THE PICTURE (6 Sep) ──
   Scoped to tiles that carry a .wt-veil (index.html's eight); /work/ and the
   services' related grids have no veil and keep their caption under the
   picture. Name + project types sit bottom-left over a PROGRESSIVE blur:
   .wt-veil holds SIX sibling <i> layers, each a backdrop blur whose radius
   doubles (1 → 32px) and each feathered in by its own mask, so the strong
   layers only ever cover the lowest strip and the weak ones reach highest.
   Blur strength rises smoothly from nothing mid-card to strong at the bottom
   edge — no band edge, no tint except a faint dark floor on the 32px layer.
   Siblings, never nested: an element with backdrop-filter is the backdrop
   root for its own descendants, so a blur inside a blur samples nothing —
   .wt-veil itself carries NO filter, only isolation:isolate so each layer
   composites in its own box (Safari). .work-tile's isolation:isolate makes
   the TILE the root, so the cover — and the hover slide lit above it — is
   what gets sampled. Inset 1px / radius −1px keeps the band inside
   .wt-media's hairline. The text colours are literal, not tokens: this is
   type over a photograph, not over the page, and it must not follow a phase
   swap. Kept OUTSIDE .wt-media on purpose: work-modal.js Flips the media box
   into the project modal and the caption must stay behind in the grid. */
.wt-text:has(> .wt-veil){
  position:absolute;left:1px;right:1px;bottom:1px;z-index:5;
  padding:clamp(64px,9vw,128px) clamp(18px,2.2vw,28px) clamp(18px,2.4vw,30px);
  border-radius:0 0 calc(var(--r-tile) - 1px) calc(var(--r-tile) - 1px);
  overflow:hidden;gap:6px;pointer-events:none;
}
.wt-text > .wt-veil{position:absolute;inset:0;z-index:0;isolation:isolate;pointer-events:none}
/* 6 Sep, Haseeb: "kill the backdrop blur" — the six blur layers are gone; the
   empty .wt-veil stays only as the :has() hook that puts the caption inside. */
.wt-text:has(> .wt-veil) .wt-name,
.wt-text:has(> .wt-veil) .wt-meta{position:relative;z-index:1}
.wt-text:has(> .wt-veil) .wt-name{color:#FAFAFA;width:fit-content}   /* the modal's ghost is fitted to this box */
.wt-text:has(> .wt-veil) .wt-meta{color:rgba(250,250,250,0.62)}
.work-tile:hover .wt-text:has(> .wt-veil) .wt-meta{color:rgba(250,250,250,0.85)}

/* Keyboard parity — a tab-focused tile gets the same bloom as a hovered one. */
.work-tile:focus-visible{outline:none}
.work-tile:focus-visible .wt-media{
  border-color:var(--border-hi);
  outline:2px solid var(--a2);outline-offset:4px;
}
.work-tile:focus-visible .wt-img{transform:scale(1.03)}
.work-tile:focus-visible .wt-media::before{opacity:0.52}

/* ── Cursor "View" badge (DOM built by work.js, one shared instance) ──
   COMPOSES base.css `.glass` — class="glass work-cursor" in the markup, so
   the border, 160deg gradient, backdrop-filter and box-shadow all come from
   there, phase-light swap included. Two things earn their own declaration:
     · border-radius: .glass's --r-tile is a rounded-square radius; a cursor
       badge is a circle. Same override shape as .pf-input's 18px below.
     · transition: .glass springs `transform` for a pressable surface: this
       is a target work.js drives with gsap.quickTo every frame, and a CSS
       transition on a property GSAP is already writing smears each frame
       into the next (trap 2) — so it is re-declared here without transform,
       the same trade .pf-input makes for the same reason.
   pointer-events:none is mandatory, not defensive: without it this sits
   directly over the tile under the real cursor and would take the click
   meant for the <a>. */
.work-cursor{
  position:fixed;top:0;left:0;z-index:550;      /* above .site-header's 500 */
  width:84px;height:84px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  pointer-events:none;will-change:transform,opacity;
  transition:border-color .35s var(--ease),background .35s var(--ease);
}
/* var(--text), not a fixed colour — .glass's phase-light swap (base.css)
   brightens the badge to a near-white panel, and the token flips dark
   alongside it so the label stays legible in both phases with nothing
   extra to maintain (base.css's own "token swap, not per-element
   overrides" rule). */
.work-cursor .overline{color:var(--text);font-size:11px}
/* ── TEXT ONLY ── "add the view cursor back but without the glass backdrop
   filter bg just text." The markup still composes base.css's .glass (so the
   class contract with work.js is unchanged), but every panel property is
   taken back off here: no backdrop blur, no fill, no border, no shadow, no
   box — the word "View" rides the pointer on its own. Declared after .glass's
   own rules and at equal-or-higher specificity, so no !important needed. */
.work-cursor,.work-cursor:hover{
  background:transparent;border:0;box-shadow:none;
  -webkit-backdrop-filter:none;backdrop-filter:none;
  width:auto;height:auto;padding:0;border-radius:0;
}
html.rzv-anim .work-cursor{background:transparent}

/* ── THE PUCK ONLY OVER A TILE (portfolio page) ──
   work.js toggles html.puck-on by pointer delegation on the grid; the page
   lens canvas fades on it. Scoped to the portfolio page so the home page's
   section solids and the hero are untouched. Opacity on the canvas is
   compositor-only and nothing in glass.js writes it. */
/* Specificity is the whole trick here: glass.css turns the lens on with
   canvas.rzv-glass--page.is-live.is-active (0,3,1), so a plain scoped rule
   loses to it and the puck would sit visible at rest. Both rules repeat that
   selector under the page scope so the rest state (0,4,1) and the hover state
   (0,4,2) each outrank it; hover keys on nothing but the class — a lens that is not
   yet live is a transparent canvas, so showing it early costs nothing and
   the gate never waits on arbiter state — verified live, not assumed. */
body[data-page="work"] canvas.rzv-glass--page,
body[data-page="home"] canvas.rzv-glass--page,
body[data-page="work"] canvas.rzv-glass--page.is-live.is-active,
body[data-page="home"] canvas.rzv-glass--page.is-live.is-active{opacity:0;transition:opacity .45s var(--ease)}
html.puck-on body[data-page="work"] canvas.rzv-glass--page,
html.puck-on body[data-page="home"] canvas.rzv-glass--page,
/* THE STATE A REAL BROWSER IS IN: is-live AND is-active once the pointer has
   moved. The rest rule above lists that combination at (0,4,1); a show rule
   at (0,3,1) lost to it, so the puck never appeared on a real cursor — only
   in a probe where the canvas carried one class or the other. (0,5,1) wins. */
html.puck-on body[data-page="work"] canvas.rzv-glass--page.is-live.is-active,
html.puck-on body[data-page="home"] canvas.rzv-glass--page.is-live.is-active{opacity:1}

/* work.js adds .has-cursor to the grid only once the badge above actually
   exists (fine pointer + motion + gsap all present) — cursor:none must
   never ship as a bare rule keyed only on hover:hover/pointer:fine, or a
   reduced-motion visitor on a fine pointer would lose the native cursor
   with no badge built to replace it. */
.work-grid.has-cursor .work-tile{cursor:none}

/* ═══════════════════════════════════════════════════════════════════════
   #purpose — hover thumbnails (DOM injected by work.js)

   index.html is FOUNDATION's file, so the markup for these is built at
   runtime and only on a fine pointer with motion on. The row stays exactly
   as shipped everywhere else.
   ═══════════════════════════════════════════════════════════════════════ */
.pp-row{position:relative}
/* A two-card deck that trails the cursor across the row. GSAP owns every
   transform on both the wrapper (x/y follow) and the cards (rest pose +
   entrance), so neither carries a CSS transform (trap 2). */
.pp-thumbs{
  position:absolute;top:0;left:0;z-index:2;
  width:clamp(150px,13vw,210px);aspect-ratio:4/3;
  pointer-events:none;opacity:0;will-change:transform;
}
.pp-thumb{
  position:absolute;inset:0;overflow:hidden;
  border-radius:var(--r-card);border:1px solid var(--border-hi);
  background:var(--bg-2);
  box-shadow:0 22px 48px -26px rgba(0,0,0,0.8);
}
body.phase-light .pp-thumb{box-shadow:0 22px 48px -28px rgba(10,8,10,0.3)}
.pp-thumb img{width:100%;height:100%;object-fit:cover}
/* Who is Hussain, The Muslim Vibe and Muslim Family Hub all have case pages
   now, so their names are links. Deliberately quiet — a hairline that only
   appears on hover, never a button: this section's whole character is that it
   does not sell. */
.pp-link{color:inherit;border-bottom:1px solid transparent;padding-bottom:2px;
  transition:border-color .35s var(--ease),color .35s var(--ease)}
.pp-link:hover,.pp-link:focus-visible{border-bottom-color:var(--border-hi);color:var(--text)}
.pp-link:focus-visible{outline:none;border-bottom-color:var(--a2)}

/* Name reacts even when the thumbnails are not built (coarse pointer). */
.pp-row .pp-name{transition:color .4s var(--ease), opacity .4s var(--ease)}
.purpose-projects.is-hovering .pp-row:not(.is-hover) .pp-name{opacity:0.42}

/* ═══════════════════════════════════════════════════════════════════════
   CASE PAGES — work/*.html

   base.css already lays out .case-hero / .case-cover / .case-meta /
   .case-body / .case-pn. This section adds the editorial image flow and
   the treatments that make the page feel like a spread rather than a form.
   ═══════════════════════════════════════════════════════════════════════ */

/* A page-wide mesh backdrop, tinted by the same --m1/2/3 as the tile that
   led here, so the case page keeps the colour the transition arrived with.
   .mesh--page is base.css's fixed + masked variant; the .mesh.on compound is
   needed to out-specify base.css's own `.mesh.on{opacity:1}`. */
.mesh.case-atmos{--mesh-blur:90px;opacity:0.32;z-index:0}
body.phase-light .mesh.case-atmos{opacity:0.2}

/* The backdrop is fixed at z-index 0, so both the article and the footer have
   to be positioned to stay above it. */
.case-main{position:relative;z-index:1}
body[data-page="case"] .site-footer{position:relative;z-index:1}

/* ── Hero ── */
.case-hero .overline{margin-bottom:clamp(18px,2.4vw,30px)}
/* Each title line rides inside its own clipping box so the SplitText-style
   rise has an edge to come from. Overflow is the only thing the mask needs;
   the line itself is never hidden by CSS (rule 3). */
.case-title .line{display:block;overflow:hidden;padding-bottom:0.08em;margin-bottom:-0.08em}
.case-title .line > span{display:block}

/* ── Cover ──
   Deliberately NOT full-bleed to the viewport edge: it is the receiving half
   of the workhero morph and the tile it grows from is inside the same 1360px
   wrap, so keeping both in the same measure makes the interpolation read as
   one object expanding instead of two rectangles crossing. */
.case-cover{box-shadow:0 40px 90px -50px rgba(0,0,0,0.9)}
body.phase-light .case-cover{box-shadow:0 40px 90px -52px rgba(10,8,10,0.3)}
.case-cover .mesh{opacity:0.9}
.case-cover.no-img .mesh{opacity:1}

/* ── Meta strip ── */
.cm-item .cm-value{font-weight:300;letter-spacing:-0.01em;line-height:1.45}
/* 6 Sep: a plain vertical list. The dotted inline row wrapped mid-list in a
   narrow column and left a stray separator at the start of each wrapped line. */
.cm-list{display:flex;flex-direction:column;gap:3px}
.cm-list span{font-size:var(--fs-meta);color:var(--text);font-weight:300;letter-spacing:-0.01em}
.cm-visit{
  display:inline-flex;align-items:center;gap:8px;width:fit-content;
  font-size:var(--fs-meta);color:var(--text);
  border-bottom:1px solid var(--border-hi);padding-bottom:3px;
  transition:color .3s var(--ease),border-color .3s var(--ease),gap .3s var(--ease);
}
.cm-visit:hover{color:var(--a2);border-color:var(--a2);gap:12px}
.cm-visit .cm-arrow{font-family:var(--mono);font-size:11px;line-height:1}

/* ── The editorial flow ──
   Text blocks live in .wrap; full-bleed figures are siblings of it, so they
   are genuinely viewport-wide without any negative-margin arithmetic against
   a 70ch measure. ── */
/* One rhythm for the whole spread. `gap` rather than per-element margins
   because .case-flow is a flex column — margins would not collapse, so a text
   block's bottom and a figure's top would stack into a canyon. */
.case-flow{position:relative;display:flex;flex-direction:column;
  gap:clamp(48px,6.5vw,104px);padding-top:clamp(16px,2.5vw,36px)}
.case-flow .case-block{max-width:70ch}
.case-flow .case-block .t-body{font-size:var(--fs-body);line-height:1.72;color:var(--muted)}
.case-flow .case-block .overline{margin-bottom:6px}

/* Figures. `.cg-media` is the element GSAP parallaxes — it is over-tall by
   design (112%) so the scrubbed drift never exposes an edge. */
.cg{position:relative;margin:0;overflow:hidden}
.cg-full{width:100%;background:var(--bg-2)}
/* <span>, so it needs blockifying — a <div> inside <figure> would be legal
   too, but the span keeps the figure's only child a phrasing wrapper. */
.cg-full .cg-media{display:block;position:relative;width:100%;aspect-ratio:16/9;overflow:hidden}
.cg-full .cg-media img{position:absolute;inset:-6% 0;width:100%;height:112%;object-fit:cover}

.cg-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(18px,2.4vw,36px);overflow:visible}
.cg-two .cg-cell{position:relative;overflow:hidden;border-radius:var(--r-tile);
  border:1px solid var(--border);background:var(--bg-2);aspect-ratio:4/3}
.cg-two .cg-cell img{position:absolute;inset:-5% 0;width:100%;height:110%;object-fit:cover}
/* Second cell drops, so a two-up row reads as a spread rather than a table. */
.cg-two .cg-cell:nth-child(2){margin-top:clamp(0px,4vw,64px)}

/* Transparent-PNG marks (Ni'mah Health's 05) get air and a contained box
   instead of a bleed — object-fit:cover on a logo is vandalism. */
.cg-mark .cg-cell{display:flex;align-items:center;justify-content:center;
  padding:clamp(44px,7vw,110px) clamp(24px,4vw,64px);
  border-radius:var(--r-tile);border:1px solid var(--border);background:var(--surface)}
.cg-mark img{width:min(340px,60%);height:auto;object-fit:contain}

/* Reel block — autoplay/muted/loop/playsinline. Sits in the wrap so it reads
   as a piece of evidence in the argument, not another bleed. */
.cg-reel{border-radius:var(--r-tile);overflow:hidden;
  border:1px solid var(--border);background:var(--bg-2)}
.cg-reel video{width:100%;height:auto;display:block}

/* ── Next project ──
   Its own mesh, its own thumbnail, and the thumbnail is where work.js stamps
   view-transition-name:workhero for a case → case morph. ── */















/* ═══════════════════════════════════════════════════════════════════════
   #work — FULL-SCREEN PROJECT MODAL  (work-modal.js)

   Clicking a .work-tile opens its project here, full-screen, instead of
   navigating to work/<slug>.html — Haseeb: "will make for smoother GSAP
   flip morphing" (a real page load cannot animate across itself the way an
   in-page element can). Built by work-modal.js at first open and appended
   to <body>, exactly like services.js does for .svc-modal: behaviour, not
   content, so with JS dead none of this is ever instantiated and the tile
   is still a plain working link.

   DELIBERATELY A SEPARATE SHAPE FROM .svc-modal, not a second visual
   language: same z-index TIER (9000s), same 40px circular icon-button
   recipe (ported from ~/rzv-dashboard client-page.css .cp-hero-back — its
   tokens are already this site's tokens, base.css lifts the whole set), same
   --r-modal vocabulary where a radius applies. What differs is the SHAPE —
   full-screen here, not a centred size-capped card — because the content is
   an entire fetched case page, not a few lines already sitting in the tile.
   work-modal.js's header has the fuller version of this call, including why
   the morph itself (GSAP's Flip plugin, which Haseeb asked for specifically)
   is a separate code path from services.js's hand-rolled FIRST/LAST tween
   rather than a shared function.

   NO backdrop-filter anywhere in this block, unlike .svc-backdrop: a
   FULL-SCREEN panel has nothing visible behind it to blur, so the
   precondition base.css trap 1 warns about (a blur above the aurora's
   mix-blend-mode:screen blobs) never arises here — there is no backdrop
   element sitting between the viewport and the panel for the trap to bite. */
/* Rebuilt 6 Sep on GSAP Flip. Shape of the DOM work-modal.js appends:
     .wm  (fixed, z 9000, data-glass-skip, --m1/2/3 copied from the tile)
       .wm-bg              opaque page ground, faded in/out by JS
       .mesh.case-atmos    the case page's own tinted backdrop
       .wm-scroll          THE scroll container (body is locked underneath)
         .wrap.wm-head
           .wm-hero        66% of the viewport — the tile's .wt-media lands here
           .wm-title       33% — h1 + project types, vertically centred
         .wm-body          article.case fetched from work/<slug>.html
       .wm-close           top-right, composes .glass
       .wm-nav             top-left: .wm-arrow.wm-prev + .wm-arrow.wm-next, the close's circle
       .wm-ghost           the tile name in flight (clone, removed on landing)
   Nothing here starts hidden: opacity 0 is set by JS at open, never by CSS.
   The title is the tile's .subhead — same face, weight, tracking, case and
   line-height — at a larger size, so the ghost Flipped with scale:true is
   the SAME glyphs growing: pure geometry, no cross-fade between two fonts.
   Tile: clamp(0.8rem,1.05vw,0.98rem). Modal: ≈2.75× at 1280, 2× at 375,
   one line for a 29-character name at both widths. */

/* Scroll lock. scrollbar-gutter:stable is the same trick the loader lock uses
   (base.css html:has(body.loading)) — the gutter stays reserved so the page
   never jumps sideways under the morph. work-modal.js pads body instead on an
   engine without it. */
html.wm-lock{overflow:hidden;scrollbar-gutter:stable}

.wm{
  position:fixed;inset:0;z-index:9000;isolation:isolate;
  --wm-pad:clamp(16px,2vw,28px);
  --m1:var(--a1);--m2:var(--a2);--m3:var(--a3);   /* overwritten inline per project */
}
.wm-bg{position:absolute;inset:0;z-index:0;background:var(--bg)}
.wm-scroll{
  position:absolute;inset:0;z-index:1;
  overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;outline:none;
}

/* ── header: hero 66 + title 33 = one viewport ── */
.wm-head{padding-top:var(--wm-pad)}
.wm-hero{
  position:relative;
  height:calc(var(--vh,1vh) * 66 - var(--wm-pad));
  border-radius:var(--r-modal);
  /* 10 Sep 2026, Haseeb: "there is still instances where when i click on item a
     black fill appears". It was this slot: it painted var(--bg-2) at full size
     from the first frame — and full-bleed, it was a dark slab across the top
     before the picture had even started to move. The slot paints nothing now;
     the only thing that darkens the screen is .wm-bg, and it fades. */
  background:none;
}
/* NO overflow:hidden on the slot: the media flies in and out of it with
   absolute:true and must not be clipped on the way. The media clips itself
   (base.css .wt-media is overflow:hidden with its own radius). */
/* The tile's own .wt-media, reparented. Its grid geometry (aspect-ratio,
   --r-tile) is released here; Flip interpolates the radius on the way. */
.wm-hero .wt-media{
  position:absolute;inset:0;width:100%;height:100%;
  aspect-ratio:auto;border-radius:var(--r-modal);--mesh-blur:60px;
}

/* 6 Sep (Haseeb, screenshot): the title sits close under the picture and the
   meta strip close under the title — no 33% slab. Height is the content's. */
.wm-title{min-height:0;
  display:flex;flex-direction:column;justify-content:flex-start;
  gap:clamp(8px,1vw,12px);padding:clamp(20px,2.4vw,32px) 0 clamp(16px,2vw,28px);
}
/* fit-content on both ends of the title morph: the ghost is fitted to the
   TEXT box, not a stretched block box. */
/* 10 Sep 2026, Haseeb: "2x the portfolio title when modal open". Twice the
   tile's .subhead size; the ghost's flight scales by target / source font, so it
   grows x2 on the way in and x0.5 on the way home with no script change. The
   title never wraps (the ghost is one line), so it is capped at 3.6vw — the
   longest name, "Romeo Zúlu Investment Capital", still fits a narrow desktop —
   and a phone keeps the tile's own size, which that name only just fits. */
.wm-h1{margin:0;width:fit-content;white-space:nowrap;
  font-size:min(calc(var(--fs-mono) * 2), 3.6vw)}
@media(max-width:760px){ .wm-h1{font-size:var(--fs-mono)} }
.wm-meta{margin:0;width:fit-content;white-space:nowrap}
.wm-ghost{
  position:absolute;top:0;left:0;z-index:3;margin:0;
  white-space:nowrap;pointer-events:none;transform-origin:0 0;
  transition:none;   /* .wt-name's colour transition would smear the GSAP tween */
}

/* ── close + prev/next ── one circle, composes base.css .glass. The close
   sits top-right; the two arrows share its exact size and ring, top-left. */
.wm-close,.wm-arrow{
  position:fixed;top:var(--wm-pad);right:var(--wm-pad);z-index:4;
  width:44px;height:44px;padding:0;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  color:var(--text);cursor:pointer;
}
.wm-close svg,.wm-arrow svg{width:15px;height:15px;fill:currentColor;display:block}
.wm-close:focus-visible,.wm-arrow:focus-visible{outline:2px solid var(--a2);outline-offset:3px}
.wm-nav{position:fixed;top:var(--wm-pad);left:var(--wm-pad);z-index:4;display:flex;gap:8px}
.wm-arrow{position:relative;top:auto;right:auto}

/* ── body: the fetched article. Full-bleed figures need the full width. ── */
.wm-body{position:relative;padding-bottom:clamp(60px,8vw,120px)}
.wm-body .case-main{padding-top:0}
/* 6 Sep, Haseeb: "too much spacing" between the title block and the meta strip.
   The standalone page's 38–64px top margin is for a page with a hero above it;
   in the modal the title block's own bottom padding is the whole gap. */
.wm-body .case-meta{margin-top:0}

/* Skeleton while the article is in flight — three quiet bars. */
.wm-skel{display:flex;flex-direction:column;gap:18px;padding:clamp(30px,4vw,50px) 0}
.wm-skel i{
  display:block;height:12px;border-radius:6px;
  background:linear-gradient(90deg,var(--surface) 0%,var(--surface-hi) 50%,var(--surface) 100%);
  background-size:200% 100%;animation:wmShimmer 1.4s linear infinite;
}
.wm-skel i:nth-child(1){width:38%}
.wm-skel i:nth-child(2){width:62%}
.wm-skel i:nth-child(3){width:54%}
@keyframes wmShimmer{from{background-position:200% 0}to{background-position:-200% 0}}

@media(max-width:760px){
  .wm{--wm-pad:12px}
  /* 66% of a phone crops a landscape cover into a portrait slot; 54 keeps it
     a picture. The title block gives back the difference. */
  .wm-hero{height:calc(var(--vh,1vh) * 54 - var(--wm-pad));border-radius:18px}
  .wm-hero .wt-media{border-radius:18px}
  .wm-title{min-height:0}
  .wm-close,.wm-arrow{width:40px;height:40px}
  .wm-close svg,.wm-arrow svg{width:14px;height:14px}
  .wm-nav{gap:6px}
  /* The in-card blur: the 1px layer is invisible on a phone GPU's budget. */
}
html.rzv-no-motion .wm-skel i{animation:none}
@media(prefers-reduced-motion:reduce){.wm-skel i{animation:none}}

/* ═══════════════════════════════════════════════════════════════════════
   FREEZE — reduced motion / motion switch off  (base.css trap 3)
   Everything above is already legible at rest; these rules only stop things
   travelling. Nothing is un-hidden here, because nothing was hidden.
   ═══════════════════════════════════════════════════════════════════════ */
html.rzv-no-motion .work-tile::before,
html.rzv-no-motion .wt-media::before,
html.rzv-no-motion .work-tile::before{opacity:0.24}
html.rzv-no-motion .wt-img,
html.rzv-no-motion .wt-reel,
html.rzv-no-motion .wt-cycle{display:none}
/* The cursor badge is JS-built and JS-gated on motionOK() at grid-init time,
   so it is simply never created with motion off from the start — this pair
   only matters for the LIVE case: motion switched off mid-session while a
   tile happens to be hovered. `moving` was captured once in initGrid() and
   is never re-read, so display:none is the only thing that can still reach
   an already-built badge — same reasoning as .wt-reel just above. The
   cursor:none reset has to travel with it: work.js added .has-cursor once,
   permanently, and never removes it, so without this the native cursor
   would stay hidden with no badge left to replace it. */
html.rzv-no-motion .work-cursor{display:none}
html.rzv-no-motion .work-grid.has-cursor .work-tile{cursor:auto}

@media(prefers-reduced-motion:reduce){
  .work-tile::before,.wt-media::before{animation:none}
  .wt-img,.wt-text,.wt-media,.pp-thumb{transition:none}
  .work-tile:hover .wt-img,.work-tile:focus-visible .wt-img{transform:none}
  .wt-reel,.wt-cycle{display:none}
  /* Same pair, defence-in-depth for the OS-level preference specifically:
     RZV.motionOK() may honour the site's own switch over prefers-reduced-
     motion (nav.js), so this is the backstop that answers to the OS signal
     regardless of what the switch decided — .wt-reel above draws the same
     distinction between the two blocks. */
  .work-cursor{display:none}
  .work-grid.has-cursor .work-tile{cursor:auto}
}

/* ═══════════════════════════════════════════════════════════════════════
   MOBILE (≤760px) — mirrors base.css: no blend modes anywhere near a blurred
   ancestor, no hover-only affordances, single column.
   ═══════════════════════════════════════════════════════════════════════ */
@media(max-width:760px){
  .wt-media::before{mix-blend-mode:normal;opacity:0.1}
  .work-tile::before{filter:blur(38px);opacity:0.26;left:2%;right:2%}
  .wt-media::after{display:none}
  .wt-reel,.wt-cycle{display:none}
  .pp-thumbs{display:none}

  .case-flow{gap:clamp(38px,9vw,64px)}
  .cg-full .cg-media{aspect-ratio:4/3}
  .cg-two{grid-template-columns:1fr;gap:18px}
  .cg-two .cg-cell:nth-child(2){margin-top:0}
  .cg-mark .cg-cell{padding:38px 24px}
  .cg-mark img{width:70%}

  
  
  
  .mesh.case-atmos{--mesh-blur:60px;opacity:0.28}
}

/* Coarse pointers never get the hover-only layers, whatever the width. */
@media(hover:none){
  .wt-reel,.wt-cycle{display:none}
  .pp-thumbs{display:none}
}

/* ═══════════════════════════════════════════════════════════════════════════
   STANDALONE PAGES — /work/ /journey/ /services/ /purpose/ /contact/

   Five folder-style pages that share this stylesheet with the case pages.
   All of them are night phase, always: none of the index's phase choreography
   runs here, so there is no white takeover to style against.

   Same three rules as everything above: no blend mode under a blurred
   ancestor, GSAP owns transform on anything it animates, and NOTHING starts
   hidden — work.js hides and reveals at runtime, only when motion is on.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Header: five links instead of four ─────────────────────────────────
   The nav is now wide enough to crowd a phone. Below 620px it becomes a
   shrink-and-scroll strip rather than wrapping or overflowing the header —
   .nav-pill positions off offsetLeft, which stays correct inside a scroll
   container because the pill scrolls with the links. ── */
@media(max-width:620px){
  .header-inner{gap:10px}
  .nav-center{flex:1 1 auto;min-width:0;overflow-x:auto;overscroll-behavior-x:contain;
    scrollbar-width:none;-ms-overflow-style:none}
  .nav-center::-webkit-scrollbar{width:0;height:0}
  .nav-link{padding:0 9px;font-size:10px;flex:none}
}

/* ── Shared page hero ───────────────────────────────────────────────────
   Same masked line rise as .case-title; work.js wraps each .line's contents
   in a span to give it something to travel inside. ── */
.page-main{position:relative;z-index:1;padding-top:var(--header-h)}
body[data-page] .site-footer{position:relative;z-index:1}

/* ── TWO-ELEMENT HERO ───────────────────────────────────────────────────
   The pattern for every standalone page (and the /services/<slug>/ subpages):
   a mono page NAME, then one block of statement copy, and nothing else.

   The title is no longer display type — it is the page's label, so it takes
   the house overline spec and is the ONE element on the site that wears that
   spec at full --text rather than --muted. That also makes a separate
   .overline above it redundant, which is why the heroes no longer carry one:
   two mono uppercase labels stacked would read as a mistake. The <h1> stays
   an <h1> — this changes how the page's name looks, not what it is. ── */
.page-hero{padding:clamp(72px,13vh,170px) 0 clamp(34px,4.4vw,64px);padding:clamp(72px,13lvh,170px) 0 clamp(34px,4.4vw,64px)}
.page-title{
  font-family:var(--mono);
  font-size:clamp(10.5px,0.82vw,12px);
  font-weight:700;                       /* Space Mono ships 400/700 only */
  letter-spacing:0.16em;
  text-transform:uppercase;
  color:var(--text);
  line-height:1.4;
}
.page-title .line{display:block;overflow:hidden;padding-bottom:0.14em;margin-bottom:-0.14em}
.page-title .line > span{display:block}
/* 8 Sep 2026, Haseeb: "Even the page title - it's the chip. No breadcrumbs just
   back arrow glass circle." The title's .line holds a static chip (chips.css
   .chip--medium, markup from _tools/chips.mjs); ONE glass circle back to the
   parent — the portfolio modal's .wm-close/.wm-arrow geometry and material
   (.glass, 44px, 15px glyph), in flow rather than fixed. 44px on a phone too:
   it is a tap target first.
   8 Sep, later: "put on same row as back button. put back button on right."
   .page-head is that row — the name chip left, the circle on the right edge,
   the two centred on each other; phones the same. */
.page-head{display:flex;align-items:flex-start;justify-content:flex-start;gap:16px}   /* 8 Sep: left aligned, stacked title chip (icon top, title bottom) */
.page-head .page-back{margin:0;flex:none}
.page-head .page-title{min-width:0}
/* The chip is the .line's only child, so work.js leaves it unwrapped and the
   line-rise travels the chip itself — and `.line > span{display:block}` above
   would otherwise flatten it. Stay a chip. */
.page-title .line > .chip{display:inline-flex;vertical-align:top}
.page-back{display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;padding:0;border-radius:50%;
  color:var(--text);text-decoration:none;margin:0 0 clamp(18px,2.2vw,28px)}
.page-back svg{width:15px;height:15px;fill:currentColor;display:block}
.page-back:focus-visible{outline:2px solid var(--a2);outline-offset:3px}

/* The lede is now the page's primary voice, so it carries the weight the
   title used to. It is either a <p> or a <div> of <p>s — the typography sits
   on the container either way, and the child rule handles the multi-block
   case where sentences genuinely do not join (an ellipsis, a line with no
   full stop). One visual block regardless. */
.page-lede{margin-top:clamp(20px,2.4vw,32px);max-width:34ch;
  font-size:var(--fs-lede);font-weight:300;letter-spacing:var(--ls-lede);
  line-height:var(--lh-lede);color:var(--text)}
.page-lede > p{margin:0}
.page-lede > p + p{margin-top:clamp(14px,1.7vw,24px)}

/* Every standalone page carries the same fixed mesh backdrop as a case page,
   tinted per page so each has its own colour temperature. */
.mesh.page-atmos{--mesh-blur:90px;opacity:0.3;z-index:0}

/* ═══ /work/ — the portfolio index ══════════════════════════════════════ */
.pf-note{display:flex;flex-direction:column;gap:12px;max-width:64ch;
  margin-top:clamp(56px,7vw,110px);padding-top:clamp(30px,4vw,48px);border-top:1px solid var(--border)}

/* ═══ /bio/ — the journey as a list of cards ════════════════════════════
   7 Sep 2026 (Haseeb): "same style as the dashboard Today list, same
   animations; each milestone a card that expands into a modal". The rail,
   spine and dots are gone — a stacked list has nothing to hang them on; the
   dates on the right carry the chronology. Card metrics are the dashboard's
   .tt-row (task-tile.css): 50px row, 18px sides, 10px stack, 14px corner,
   --card on --border, hover to --card-hi / --border-hi; title 16/500/-0.01em;
   the date is the .tt-time read — mono, uppercase, --dim, tabular. ═══ */
.jn-timeline{position:relative;padding-top:clamp(16px,2.4vw,32px);--jn-rail:34px;--jn-line-x:9px}
/* the thread: a hairline down the rail with a scroll-drawn fill (work.js) and a dot per card */
.jn-thread{position:absolute;left:var(--jn-line-x);top:calc(clamp(16px,2.4vw,32px) + 1.6em + clamp(18px,2.4vw,32px));bottom:0;width:1px;background:var(--border);pointer-events:none}
.jn-thread-fill{position:absolute;inset:0;background:var(--border-hi);transform-origin:top center}
.jn-entry{position:relative;padding-left:var(--jn-rail)}
.jn-entry::before{content:'';position:absolute;left:calc(var(--jn-line-x) - 3px);top:50%;width:7px;height:7px;border-radius:50%;background:var(--bg);border:1px solid var(--border-hi);transform:translateY(-50%);z-index:1;transition:background .35s var(--ease),border-color .35s var(--ease),box-shadow .35s var(--ease)}
.jn-entry.is-lit::before{background:var(--text);border-color:var(--text);box-shadow:0 0 0 3px color-mix(in srgb,var(--text) 14%,transparent)}   /* lit by the progress line */
html.rzv-no-motion .jn-entry::before{background:var(--text);border-color:var(--text)}
.jn-begin{padding-left:var(--jn-rail)}
@media(max-width:760px){ .jn-timeline{--jn-rail:26px;--jn-line-x:6px} }
.jn-begin{margin-bottom:clamp(18px,2.4vw,32px);color:var(--dim)}
.jn-entry + .jn-entry{margin-top:10px}
/* no flash before the scrub takes over: hidden from first paint while motion is on, until work.js marks the list ready */
html.rzv-anim .jn-timeline:not(.jn-ready) .jn-card{opacity:0}
.jn-title{margin:0;font:inherit;letter-spacing:normal;line-height:inherit}
.jn-card{
  all:unset;box-sizing:border-box;cursor:pointer;
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  width:100%;min-height:50px;padding:0 18px;
  background:var(--card);border:1px solid var(--border);border-radius:var(--r-card);
  color:var(--text);text-align:left;
  transition:border-color .2s var(--ease),background .35s var(--ease);
  -webkit-tap-highlight-color:transparent;
}
.jn-card:hover{border-color:var(--border-hi);background:var(--card-hi)}
.jn-card:focus-visible{outline:2px solid var(--a2);outline-offset:3px}
.jn-card-title{flex:0 1 auto;min-width:0;
  font-size:16px;font-weight:500;letter-spacing:-0.01em;line-height:1.3;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.jn-card-date{flex:none;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
.jn-more[hidden]{display:none}

/* ── the milestone modal (.jm) — the dashboard's TaskModal shell: 720 wide,
   --bg-2 on --border, the modal corner, the deep shadow. The head carries the
   card's two facts (title left, date right), the body the copy in the entry's
   own type (.jn-lede + .t-body). journey-modal.js Flips it from the card's
   rect; scroll lock (html.wm-lock), inert and history are work-modal.js's. ── */
.jm{position:fixed;inset:0;z-index:9000;isolation:isolate;
  display:grid;place-items:center;padding:32px 24px;--jm-x:36px}
.jm-bg{position:absolute;inset:0;z-index:0;
  background:color-mix(in srgb,var(--bg) 70%,transparent);
  backdrop-filter:blur(28px);-webkit-backdrop-filter:blur(28px)}
.jm-panel{position:relative;z-index:1;
  width:min(720px,100%);max-height:calc(var(--vh,1vh) * 100 - 64px);
  display:flex;flex-direction:column;overflow:hidden;outline:none;
  background:var(--bg-2);border:1px solid var(--border);border-radius:var(--r-modal);
  box-shadow:0 60px 160px -20px rgba(0,0,0,0.7),0 0 1px rgba(255,255,255,0.08)}
/* flex-wrap: on a phone a long title takes the line and the date drops under
   it, still right-aligned — the head never shrinks the title below 20px for it. */
.jm-head{flex:none;display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;
  gap:6px 18px;padding:30px 80px 24px var(--jm-x);border-bottom:1px solid var(--border)}
/* One line, fit-content: the card title's ghost lands on a single text box
   (journey-modal.js fitTitle shrinks the size, never below the card's 16px). */
.jm-h2{flex:0 1 auto;min-width:0;margin:0;width:fit-content;color:var(--text);
  font-size:clamp(20px,3vw,26px);font-weight:500;letter-spacing:-0.01em;line-height:1.15;
  white-space:nowrap;overflow:hidden}
.jm-date{flex:none;margin-left:auto;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
.jm-scroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;outline:none}
.jm-body{display:flex;flex-direction:column;gap:16px;padding:26px var(--jm-x) 36px}
.jm-body .t-body + .t-body{margin-top:2px}
/* The lede is the live page's h5 — the sentence that carries the entry. */
.jn-lede{font-size:var(--fs-body-lg);font-weight:300;letter-spacing:-0.018em;
  line-height:1.5;color:var(--text)}
.jm-close{position:absolute;top:24px;right:24px;z-index:4;
  width:38px;height:38px;padding:0;border-radius:50%;
  display:flex;align-items:center;justify-content:center;color:var(--text);cursor:pointer}
.jm-close svg{width:13px;height:13px;fill:currentColor;display:block}
.jm-close:focus-visible{outline:2px solid var(--a2);outline-offset:3px}
/* In flight: the card's surface, and its title and date, as clones. */
.jm-ghost-card{position:absolute;top:0;left:0;z-index:2;margin:0;pointer-events:none;transition:none}
.jm-ghost-card > *{visibility:hidden}
.jm-ghost{position:absolute;top:0;left:0;z-index:3;margin:0;display:block;
  max-width:none;overflow:visible;white-space:nowrap;pointer-events:none;
  transform-origin:0 0;transition:none}
/* Motion off / no Flip: the same modal, a plain fade. */
.jm.jm-plain{opacity:0;transition:opacity .25s var(--ease)}
.jm.jm-plain.is-in{opacity:1}
html.rzv-no-motion .jm.jm-plain{transition:none}
@media(max-width:760px){
  .jn-card{min-height:56px;padding:12px 15px}
  .jn-card-title{font-size:15px}
  .jm{padding:12px;--jm-x:22px}
  .jm-panel{max-height:calc(var(--vh,1vh) * 100 - 24px);border-radius:22px}
  .jm-head{padding:22px 66px 18px var(--jm-x);gap:4px 12px}
  .jm-close{top:16px;right:16px;width:36px;height:36px}
  .jm-body{padding:20px var(--jm-x) 28px}
}

/* ═══ /services/ ════════════════════════════════════════════════════════ */

/* ── The tile material (dashboard parity) ──
   The dashboard's answer to a quiet section is rows INSIDE a card tile —
   task-tile.css .tt-row at 14px sitting in a .widget-tile at 24px. So the
   pillar list takes base.css's .tilt surface (24px, sheen + outer glow) and
   each pillar becomes an inner row at --r-card.

   No RZV.attachTilt here, deliberately: these are tall reading panels, and
   rotating a paragraph in 3D as the cursor crosses it fights the text. The
   MATERIAL is the parity item; work.js attaches the cursor sheen ALONE
   (attachSheen) so the surface still tracks the pointer without the tilt. */
.sv-pillars{display:flex;flex-direction:column;gap:10px}
.sv-pillars.tilt{padding:clamp(12px,1.4vw,18px)}
.sv-pillar{position:relative;isolation:isolate;
  display:grid;grid-template-columns:minmax(0,0.9fr) minmax(0,1.1fr);
  gap:clamp(22px,4vw,72px);
  padding:clamp(30px,4vw,58px) clamp(20px,2.6vw,38px);
  border-radius:var(--r-card);
  border:1px solid transparent;background:var(--surface);
  transition:border-color .3s var(--ease),background .35s var(--ease)}
.sv-pillars.tilt .sv-pillar:hover{border-color:var(--border);background:var(--surface-2)}
/* The accent: a soft bloom anchored to the pillar's left edge. Breathing is
   deliberately absent here — this page is a reference, not a showreel. */
.sv-pillar::before{
  content:'';position:absolute;z-index:-1;pointer-events:none;
  left:-6%;top:6%;width:46%;height:88%;border-radius:50%;
  background:radial-gradient(circle at 40% 45%, color-mix(in srgb, var(--acc,var(--a1)) 42%, transparent), transparent 70%);
  filter:blur(64px);opacity:0.34;
  transition:opacity .6s var(--ease);
}
.sv-pillar:hover::before{opacity:0.55}

.sv-head{display:flex;flex-direction:column;gap:14px}
.sv-num{font-family:var(--mono);font-size:clamp(9px,0.7vw,11px);font-weight:700;
  letter-spacing:0.16em;text-transform:uppercase;
  color:color-mix(in srgb, var(--acc,var(--a1)) 74%, var(--text))}
.sv-name{font-size:var(--fs-title);font-weight:300;letter-spacing:-0.04em;line-height:1.06;color:var(--text)}
.sv-body{display:flex;flex-direction:column;gap:clamp(20px,2.4vw,32px);padding-top:clamp(4px,0.8vw,10px)}
.sv-desc{font-size:var(--fs-body-lg);font-weight:300;letter-spacing:-0.015em;
  line-height:1.6;color:var(--muted);max-width:56ch}
.sv-subs{display:flex;flex-wrap:wrap;gap:8px}
.sv-subs li{font-family:var(--mono);font-size:var(--fs-label-s);font-weight:700;letter-spacing:0.11em;
  text-transform:uppercase;color:var(--muted);
  border:1px solid var(--border);border-radius:var(--r-pill);padding:7px 13px;
  background:var(--surface);
  transition:color .3s var(--ease),border-color .3s var(--ease),background .3s var(--ease)}
.sv-pillar:hover .sv-subs li{border-color:var(--border-hi);color:var(--text)}

/* The tentative offering — kept quiet at 0.62, as it is on the page itself.
   This used to say "index.html marks it .cn-tentative", which was never true
   of any file in the repo: the constellation's own .cn-tentative rule had no
   element either and has since been deleted. The dimming is done HERE, by this
   rule, on the services pages that use it — there is no cross-file marker and
   nothing to keep in step. */

/* ── Process ── */
.sv-process{display:flex;flex-direction:column}
.sv-step{display:grid;grid-template-columns:clamp(58px,7vw,110px) minmax(0,1fr);
  gap:clamp(20px,3vw,52px);
  padding:clamp(30px,4vw,56px) 0;border-top:1px solid var(--border)}
.sv-step:last-child{border-bottom:1px solid var(--border)}
/* The dashboard's .tl-num treatment (perf-sections.css:24): a numeral is
   drawn light and tight, not bolded like a label. --faint at weight 300 would
   have vanished, so it sits at --dim: quiet, still legible as a figure. */
.sv-step-n{font-family:var(--mono);font-weight:300;font-size:clamp(15px,2.2vw,30px);
  letter-spacing:-0.04em;color:var(--dim);line-height:1;
  font-variant-numeric:tabular-nums}
.sv-step-c{display:flex;flex-direction:column;gap:14px;max-width:70ch}
.sv-step-t{font-size:var(--fs-sub);font-weight:300;letter-spacing:-0.03em;
  line-height:1.15;color:var(--text)}

/* ── Closing statement ── */
/* ── /services/<slug>/ subpages ─────────────────────────────────────────
   Same tile material as the parent's pillar list, so a subpage reads as the
   same page one level down. Differences: no 01–04 numeral and no chip list,
   so the left column only has to hold a name and can be narrower. ── */
.sv-subpage .sv-pillar{grid-template-columns:minmax(0,0.62fr) minmax(0,1.38fr)}
.sv-copy{display:flex;flex-direction:column;gap:14px}
/* .sv-body's own gap is sized to separate copy from a chip list; two
   paragraphs of the SAME thought need to sit closer than that. */
.sv-copy .sv-desc{max-width:62ch}

/* The quiet pointer — used on the parent's pillars (to a subpage) and on each
   subpage (back to the process). Deliberately not a button: it is a sideways
   step, not the page's action. */
.sv-more{
  display:inline-flex;align-items:center;gap:8px;width:fit-content;
  font-family:var(--mono);font-size:var(--fs-label-s);font-weight:700;letter-spacing:0.12em;
  text-transform:uppercase;color:var(--muted);
  transition:color .3s var(--ease),gap .3s var(--ease);
}
.sv-more:hover{color:var(--text);gap:13px}
.sv-pillar .sv-more{margin-top:clamp(6px,1vw,12px)}
.sv-process-link{margin-top:clamp(26px,3.4vw,44px)}

/* Related work: the same .work-tile machinery (tilt, mesh, hover reel, the
   workhero stamp into the case page), just a plainer grid — the editorial
   every-second-tile drop belongs to the eight-up portfolio, not to a
   two-or-three-tile footnote. */
.work-grid.sv-related{grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));
  gap:clamp(20px,2.6vw,36px)}
.work-grid.sv-related > .work-tile:nth-child(even){margin-top:0}
.work-grid.sv-related .wt-media{aspect-ratio:16/10}
/* The doc's one-line reason this project is here. */
.wt-note{font-size:var(--fs-xs);line-height:1.6;color:var(--muted);max-width:44ch}

.sv-cta-row{display:flex;flex-wrap:wrap;align-items:center;gap:14px}

.sv-close{display:flex;flex-direction:column;align-items:flex-start;gap:clamp(26px,3.4vw,44px);
  padding:clamp(60px,8vw,130px) 0 0}
.sv-close-line{max-width:20ch}

/* ═══ /purpose/ ═════════════════════════════════════════════════════════ */
.pu-mission{display:flex;flex-direction:column;gap:clamp(16px,2vw,26px);max-width:60ch;
  padding:clamp(46px,6vw,96px) 0;border-top:1px solid var(--border);border-bottom:1px solid var(--border);
  margin:clamp(46px,6vw,90px) 0}
/* On /purpose/ each project row is a full spread: name + blurb, then its two
   images. The compact index.html row keeps its floating hover deck instead. */
.purpose-projects .pp-row .cg-two{margin-top:clamp(24px,3vw,40px);grid-column:1 / -1}

/* ── The tile material (dashboard parity) ──
   Same tile-with-rows structure as the services pillars.

   Scoped to `.tilt` — which /purpose/ opts into in its own markup — rather
   than to `.purpose-projects` outright, because index.html's copy of this
   list carries the cursor-following thumbnail deck: .tilt brings
   `overflow:hidden`, which would clip that deck the moment it neared a row
   edge. The standalone page has no deck (its images are in the row), so it
   takes the tile; index keeps its editorial hairline rows. */
.purpose-projects.tilt{padding:clamp(12px,1.4vw,18px);display:flex;flex-direction:column;gap:10px}
/* The `border` shorthand replaces base.css's border-top/last-child
   border-bottom hairlines outright — no need to unset them first. */
.purpose-projects.tilt .pp-row{
  border:1px solid transparent;border-radius:var(--r-card);
  background:var(--surface);
  padding:clamp(26px,3.2vw,42px) clamp(20px,2.6vw,34px);
  transition:border-color .3s var(--ease),background .35s var(--ease)}
.purpose-projects.tilt .pp-row:hover{border-color:var(--border);background:var(--surface-2)}
.pu-collab{display:flex;flex-direction:column;gap:clamp(20px,2.6vw,32px);align-items:flex-start;
  max-width:62ch;margin-top:clamp(52px,7vw,110px)}
.pu-mail{font-size:var(--fs-sm);color:var(--text);border-bottom:1px solid var(--border-hi);padding-bottom:3px;
  transition:color .3s var(--ease),border-color .3s var(--ease)}
.pu-mail:hover{color:var(--a2);border-color:var(--a2)}

/* ═══ /contact/ ═════════════════════════════════════════════════════════ */
.ct-actions{display:flex;flex-wrap:wrap;align-items:center;gap:clamp(18px,2.4vw,32px);
  margin-top:clamp(34px,4.4vw,60px)}
.ct-mail{font-family:var(--mono);font-size:clamp(12px,1.15vw,14.5px);letter-spacing:0.02em;
  color:var(--muted);border-bottom:1px solid var(--border);padding-bottom:4px;
  transition:color .3s var(--ease),border-color .3s var(--ease)}
.ct-mail:hover{color:var(--text);border-color:var(--border-hi)}

.ct-socials{display:flex;flex-wrap:wrap;gap:9px;margin-top:clamp(46px,6vw,90px)}

/* ═══════════════════════════════════════════════════════════════════════════
   PORTAL MATERIAL — lifted from ~/rzv-dashboard/assets/portal.css

   The dashboard's portal is the brand's most finished surface, so the two
   places on this site where someone actually ACTS — the Start project button
   and the enquiry form — are built from its literal recipes rather than an
   impression of them. Sources are credited per block; go read the original
   before changing a number.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── PRIMARY BUTTON  (portal.css .ptl-cbtn.primary:405–406,
      lift/press shadow stack from .ptl-newchat-pearl:164–200) ────────────
   base.css already carries the exact aurora face on .btn::before, but keeps
   it at opacity 0 until hover. The portal's primary button is not a hover
   trick — it wears the aurora at rest and reads as the one committed action
   on the page. So: face on, #0b0b14 text, the 0.42 white border, and the
   pearl's three-layer lift underneath.

   btnDrift (base.css) keeps animating background-position on the same
   pseudo-element; it never touches opacity, so it cannot fight the face
   being permanently on. ── */
.btn-primary{
  color:#0b0b14;
  border-color:rgba(255,255,255,0.42);
  box-shadow:
    0 1px 2px rgba(13,13,20,0.35),
    0 7px 14px -6px rgba(129,140,248,0.55),
    0 16px 32px -14px rgba(0,0,0,0.55);
  transition:
    transform .25s var(--ease-spring),
    box-shadow .25s var(--ease),
    filter .3s var(--ease),
    border-color .35s var(--ease);
}
.btn-primary::before{opacity:1}

/* Spring lift. The mid glow warms and widens; the contact shadow stays tight
   so the button reads as rising off the page rather than growing. */
.btn-primary:hover,
.btn-primary:focus-visible{
  color:#0b0b14;
  transform:translateY(-2px) scale(1.03);
  filter:brightness(1.06) saturate(1.05);
  border-color:rgba(255,255,255,0.55);
  box-shadow:
    0 2px 3px rgba(13,13,20,0.30),
    0 10px 20px -8px rgba(129,140,248,0.60),
    0 20px 40px -16px rgba(0,0,0,0.55);
}
/* Pressed: the lift collapses to a contact line and an inner shade takes
   over, so the button visibly sinks — .ptl-newchat-pearl:active, verbatim. */
.btn-primary:active{
  transform:translateY(1px) scale(0.985);
  filter:none;
  box-shadow:
    inset 0 2px 6px rgba(13,13,20,0.16),
    inset 0 -1px 1px rgba(255,255,255,0.7),
    inset 0 -3px 8px rgba(129,140,248,0.24),
    0 1px 2px rgba(13,13,20,0.30),
    0 3px 8px -4px rgba(129,140,248,0.40);
}
/* On white the indigo bloom disappears and the button loses its edge — the
   same swap base.css makes for the pearl. The aurora face is opaque enough
   that #0b0b14 stays legible on it in both phases. */
body.phase-light .btn-primary{
  border-color:rgba(10,8,10,0.16);
  box-shadow:
    0 1px 2px rgba(13,13,20,0.14),
    0 7px 14px -6px rgba(129,140,248,0.40),
    0 16px 32px -16px rgba(10,8,10,0.28);
}
body.phase-light .btn-primary:hover{border-color:rgba(10,8,10,0.26)}

/* ── CONTACT AURORA  (aurora spec; hosts built by RZV.aurora) ───────────
   The point of this is the glass: .pf-input and .pf-chip carry
   backdrop-filter, and until now they were blurring flat #08080A — a blur of
   nothing looks like nothing. These two hosts put light BEHIND the form so
   the composer surfaces finally have something to refract.

   FOUNDATION owns `.au` itself (position/overflow/opacity, and the six-blob
   DOM via RZV.aurora). Everything here is per-host placement only.

   Geometry is ratio-locked, so every host pins an aspect-ratio; the mask is
   what keeps a hard blob edge from ever meeting a text column. The hosts sit
   OUTSIDE .wrap and outside every reveal group on purpose — a `filter` on an
   ancestor changes what a descendant's backdrop-filter samples (the same
   lesson that put the blur:0 guard in work.js's revealItems). ── */
#enquiry > .au{
  left:0;top:clamp(-40px,-4vw,0px);
  width:min(980px,100%);aspect-ratio:4/5;
  --au-o:0.30;
  transform:translateX(-10%);
  -webkit-mask-image:radial-gradient(120% 90% at 22% 34%,#000 0%,rgba(0,0,0,0.66) 52%,transparent 92%);
  mask-image:radial-gradient(120% 90% at 22% 34%,#000 0%,rgba(0,0,0,0.66) 52%,transparent 92%);
}
#enquiry > .wrap{position:relative;z-index:1}

.page-hero > .au{
  right:0;top:0;
  width:min(620px,80%);aspect-ratio:1/1;
  --au-o:0.20;
  transform:translate(18%,-14%);
  -webkit-mask-image:radial-gradient(90% 90% at 70% 30%,#000,transparent 88%);
  mask-image:radial-gradient(90% 90% at 70% 30%,#000,transparent 88%);
}
.page-hero > .wrap{position:relative;z-index:1}

/* Half the blobs are display:none on a phone, so the hosts widen and lift
   their opacity to compensate for the light that goes with them. */
@media(max-width:760px){
  #enquiry > .au{--au-o:0.40;width:130%;aspect-ratio:3/4}
  .page-hero > .au{--au-o:0.26}
}

/* ── ENQUIRY FORM  (/contact/) ──────────────────────────────────────────
   The portal composer, unrolled into a form. Every surface here is the same
   glass: .ptl-bubble:422–429 for the fields, .ptl-glass:914–920 (== the
   ask-user chips in client-chat.js:375–386) for the options, .sd-seg
   (settings.js:28–31) for the two single-choice rows.

   No JS is required for any of it to work: the options are real checkboxes
   and radios, and :has() paints their checked state (base.css already
   depends on :has(), so this is not a new assumption). work.js only adds the
   sliding pill and the mailto compose. ── */
.pf-form{display:flex;flex-direction:column;gap:clamp(26px,3.4vw,40px);
  max-width:900px;margin-top:clamp(30px,4vw,48px)}
.pf-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(16px,2vw,24px)}
.pf-field{display:flex;flex-direction:column;gap:10px;min-width:0}
.pf-label{color:var(--muted)}
.pf-req{color:var(--a2)}

/* The composer bubble, as a field.
   ── COMPOSES base.css `.glass` ──  The markup is class="glass pf-input", so
   the border, the 160deg gradient, backdrop-filter and the box-shadow all
   come from there and are NOT repeated here. What follows is only what a
   FIELD adds on top.

   The transition is re-declared rather than inherited on purpose: .glass
   springs `transform`, because the dashboard's glass is a pressable surface
   and a text field is not one. This trades that for `box-shadow`, which is
   what animates the focus ring below. */
.pf-input{
  width:100%;display:block;
  border-radius:18px;                    /* tighter than .glass's --r-tile */
  padding:15px 18px;
  color:var(--text);font-family:inherit;font-size:var(--fs-sm);line-height:1.5;
  caret-color:#818CF8;outline:none;
  transition:border-color .35s var(--ease),background .35s var(--ease),box-shadow .35s var(--ease);
}
.pf-input::placeholder{color:var(--dim)}
/* Focus = the bubble's focus-within state plus a faint aurora ring. Same
   specificity as base.css's `.glass:hover` (0,2,0) and declared later, so a
   field that is hovered AND focused reads as focused. The drop shadow is
   restated because a box-shadow cannot be added to — the ring has to ship
   with the layers it sits alongside. */
.pf-input:focus{
  border-color:rgba(255,255,255,0.24);
  background:linear-gradient(160deg,rgba(255,255,255,0.13),rgba(255,255,255,0.04) 50%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.18),
    0 10px 28px -16px rgba(0,0,0,0.4),
    0 0 0 3px rgba(129,140,248,0.16),
    0 0 26px -8px rgba(129,140,248,0.55);
}
.pf-textarea{resize:vertical;min-height:150px;border-radius:var(--r-tile)}

.pf-set{border:none;padding:0;margin:0;display:flex;flex-direction:column;gap:14px;min-width:0}
.pf-set legend{padding:0}

/* ── option chips (client-chat.js .cc-chip — the ask-user recipe) ──
   A real <input> lives inside each label, visually hidden but focusable, so
   selection works, announces and persists with no JS at all. */
.pf-chips{display:flex;flex-wrap:wrap;gap:8px}
/* ── COMPOSES base.css `.glass` ──  class="glass pf-chip" in the markup, so
   the border, gradient, backdrop-filter, box-shadow AND the --r-tile radius
   all come from there.

   SHADOW: the dashboard genuinely disagrees with itself here — portal.css
   `.ptl-glass` ends its drop shadow on -16px, client-chat.js `.cc-chip` on
   -18px, and this chip was lifted from the latter. Consolidated onto .glass's
   -16px: keeping the 2px would mean re-declaring BOTH shadow layers just to
   move one spread value, which is the duplication this change removes, and
   2px of spread under a 28px blur at 40% alpha is not visible. */
.pf-chip{
  position:relative;display:inline-flex;align-items:center;gap:7px;max-width:100%;
  font:inherit;font-size:12.5px;line-height:1.35;color:var(--text);text-align:left;
  padding:9px 15px;cursor:pointer;
  /* Its own: .glass springs transform at .25s and does not transition color;
     a chip needs both, and slightly quicker. */
  transition:border-color .3s var(--ease),background .3s var(--ease),
             transform .22s var(--ease-spring),color .3s var(--ease);
}
.pf-chip input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.pf-chip:hover{border-color:rgba(255,255,255,0.24);
  background:linear-gradient(160deg,rgba(255,255,255,0.14),rgba(255,255,255,0.04) 50%);
  transform:translateY(-1px)}
.pf-chip:has(input:focus-visible){outline:2px solid var(--a2);outline-offset:3px}
/* Selected — .cc-chip.on: brighter border and an aurora tint through it. */
/* Selected — the same white treatment as the choice tiles (7 Sep): a soft white
   gradient edge over a whisper of surface, white tick. */
.pf-chip:has(input:checked){
  border-color:transparent;
  background:linear-gradient(160deg,rgba(255,255,255,0.075),rgba(255,255,255,0.025) 60%) padding-box,
             linear-gradient(135deg,rgba(255,255,255,0.38),rgba(255,255,255,0.14) 55%,rgba(255,255,255,0.07)) border-box;
}
.pf-chip::after{content:'';width:0;height:12px;flex:none;margin-left:-7px;opacity:0;transform:scale(.35);background:var(--text);
  transition:width .3s var(--ease),margin-left .3s var(--ease),opacity .25s var(--ease),transform .38s var(--ease-spring);
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209l17-17L352 158.1l-17 17-111 111-47-47-17-17L126.1 256l17 17 64 64 17 17 17-17L369 209z'/></svg>") center/contain no-repeat;
          mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209l17-17L352 158.1l-17 17-111 111-47-47-17-17L126.1 256l17 17 64 64 17 17 17-17L369 209z'/></svg>") center/contain no-repeat}
/* selected: the tick scales up as the chip widens to make room (7 Sep, "smooth animation, resize transition") */
.pf-chip:has(input:checked)::after{width:12px;margin-left:0;opacity:.8;transform:scale(1);
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209l17-17L352 158.1l-17 17-111 111-47-47-17-17L126.1 256l17 17 64 64 17 17 17-17L369 209z'/></svg>") center/contain no-repeat;mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209l17-17L352 158.1l-17 17-111 111-47-47-17-17L126.1 256l17 17 64 64 17 17 17-17L369 209z'/></svg>") center/contain no-repeat}   /* FA Sharp Regular circle-check, as on the tiles */

/* ── segmented pills (settings.js .sd-seg) ──
   Without JS the checked segment simply takes the solid fill, exactly as the
   dashboard does it. With JS, .has-pill hands that job to a sliding element
   and the label only carries the inverted text colour. */
.pf-seg{position:relative;display:flex;width:100%;flex-wrap:wrap;gap:2px;padding:3px;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-pill);
  max-width:100%}
.pf-seg-btn{
  position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;
  /* 9 Sep, Haseeb: "budget and timeline switch labels - use regular sans font
     here looks too confusing as mono. justify width switches on desktop." */
  font-family:var(--sans,'Inter',system-ui,sans-serif);font-size:var(--fs-sm);font-weight:400;letter-spacing:-0.01em;text-transform:none;
  color:var(--muted);border-radius:var(--r-pill);padding:9px 14px;cursor:pointer;white-space:nowrap;
  transition:color .25s var(--ease),background .25s var(--ease);
}
.pf-seg-btn input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.pf-seg-btn:hover{color:var(--text)}
.pf-seg:not(.has-pill) .pf-seg-btn:has(input:checked){background:var(--text);color:var(--bg)}
.pf-seg.has-pill .pf-seg-btn:has(input:checked){color:var(--bg)}
.pf-seg-btn:has(input:focus-visible){outline:2px solid var(--a2);outline-offset:3px}
/* The slider. ALL four coordinates are written by work.js off the checked
   label's own box — top/height included, because the row WRAPS at narrow
   widths and a pill sized to the container would then span every line of it
   instead of sitting on its own. The values here are only the pre-JS resting
   state (invisible, so they are never seen). */
.pf-seg-pill{position:absolute;top:3px;height:0;left:3px;width:0;
  border-radius:var(--r-pill);background:var(--text);z-index:0;opacity:0;pointer-events:none;
  transition:left .38s var(--ease-spring),width .38s var(--ease-spring),
             top .38s var(--ease-spring),height .38s var(--ease-spring),opacity .3s var(--ease)}
.pf-seg.has-pill .pf-seg-pill{opacity:1}

.pf-submit{display:flex;flex-wrap:wrap;align-items:center;gap:clamp(16px,2.2vw,26px);
  margin-top:clamp(6px,1vw,14px)}
/* Says what the button does. No promise, no pitch. */
.pf-hint{color:var(--dim)}

/* ═══ FREEZE ════════════════════════════════════════════════════════════ */
html.rzv-no-motion .sv-pillar::before{opacity:0.34}
html.rzv-no-motion .btn-primary:hover{transform:none}
html.rzv-no-motion .pf-seg-pill{transition:none}
@media(prefers-reduced-motion:reduce){
  .sv-subs li,.sv-pillar::before,.pu-mail,.ct-mail{transition:none}
  .pf-chip,.pf-seg-btn,.pf-seg-pill,.pf-input,.cf-opt{transition:none}
  .pf-chip:hover,.cf-opt:hover{transform:none}
  .btn-primary{transition:none}
  .btn-primary:hover,.btn-primary:focus-visible,.btn-primary:active{transform:none}
}

/* ═══ MOBILE ════════════════════════════════════════════════════════════ */
@media(max-width:900px){
  .sv-pillar,.sv-subpage .sv-pillar{grid-template-columns:1fr;gap:20px}
  .sv-pillar::before{width:70%;left:-14%}
}
@media(max-width:760px){
  .page-lede{max-width:none;font-size:var(--fs-lede-m)}

  .sv-step{grid-template-columns:1fr;gap:10px}
  .sv-step-n{font-size:13px;color:var(--dim)}

  .purpose-projects .pp-row .cg-two{grid-template-columns:1fr}
  .ct-actions{gap:16px}

  /* ── iOS TRAP (base.css rule 1) ──
     The fields and chips are `.glass`, so base.css's own ≤760px block already
     strips their backdrop-filter and paints them var(--bg-2). Neither is
     repeated here — what remains is only what base.css does NOT cover:

       · the STATE backgrounds, which live on higher-specificity selectors
         (`.glass:hover` is 0,2,0 and would otherwise put the translucent
         gradient back over an un-blurred surface);
       · flattening the drop shadow — a 28px blurred shadow under every chip
         is real work for a phone GPU and the lift it buys is invisible there;
       · 16px text, or iOS Safari zooms the page on focus (portal.css:743). */
  .pf-input,.pf-chip,.cf-opt{box-shadow:inset 0 1px 0 rgba(255,255,255,0.10)}
  .pf-input:focus{
    background:var(--bg-2);
    box-shadow:inset 0 1px 0 rgba(255,255,255,0.14), 0 0 0 3px rgba(129,140,248,0.18);
  }
  .pf-chip:hover,.cf-opt:hover{background:var(--bg-2)}
  .pf-input{font-size:16px}

  .pf-grid{grid-template-columns:1fr}
  .pf-seg{width:100%}
  .pf-seg-btn{flex:1 1 auto;padding:9px 10px;font-size:9.5px}
  .pf-submit{gap:14px}
  .pf-submit .btn-primary{width:100%}
}

/* ── SUB-SERVICE PAGES (services/<discipline>/<sub-service>/) — IN work.css
   because that is the sheet the discipline pages and their generated children
   load (services.css is the home page's tile grid). First landed in the wrong
   file: the hero icon rendered as an unstyled full-width black SVG. ────────────
   Generated from each discipline page, 1 Sep 2026 ("Make a page for every
   subservice. Service level pages. keep the use of the icons."). The hero
   carries the sub-service's own icon (Font Awesome Pro regular, inlined — the
   same set the #chips wall uses), the discipline as a linked overline, and
   the pillar's own copy as the lede; the detail block lists the discipline's
   siblings as pills. Nothing here is new copy. */
/* 8 Sep: .sv-hero-ico and .sv-hero-parent are gone — the title is the chip
   and the parent is .page-back (above). The siblings are chips too; .sv-sibs
   stays as their row. */
.sv-ov-ico{width:1.2em;height:1.2em;fill:currentColor;vertical-align:-.25em;margin-right:.55em}
.sv-sibs{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0 26px}
.sv-name-link{color:inherit;text-decoration:none}
.sv-name-link:hover{text-decoration:underline;text-underline-offset:.14em}

/* The o3 block on each sub-service page: four inclusions as a plain list, one outcome sentence. */
.sv-solve{margin:0 0 clamp(36px,5vw,64px);max-width:56ch}
.sv-includes{list-style:none;padding:0;margin:10px 0 18px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 22px}
.sv-includes li{position:relative;padding-left:1.1em;color:var(--text)}
.sv-includes li::before{content:'';position:absolute;left:0;top:.62em;width:5px;height:5px;border-radius:50%;background:var(--muted)}
.sv-outcome{color:var(--muted);margin:0}
@media(max-width:560px){.sv-includes{grid-template-columns:1fr}}

/* ═══ #purpose — THE EDITORIAL INDEX (3 Sep 2026) ══════════════════════════
   Three rows: name at display scale, meta chips on the right, one line under.
   Hairlines between. ONE floating image (.pp-float, built by work.js) follows
   the cursor over the list and crossfades between rows; the rows not hovered
   dim. Everything static here; work.js owns .pp-float's transform (trap 2). */
#purpose .range-inner{margin-bottom:clamp(40px,6vw,88px)}
.purpose-projects{position:relative;border-top:1px solid var(--border)}
/* 3 Sep: five columns — title 1/5 · meta 1/5 · blank 1/5 · sentence 2/5. */
.pp-row{
  display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:start;column-gap:clamp(16px,2.4vw,32px);row-gap:12px;
  padding:clamp(22px,3.2vw,40px) 0;border-bottom:1px solid var(--border);
}
.pp-name{margin:0;grid-column:1}
.pp-meta{grid-column:2;display:flex;flex-direction:column;align-items:flex-start;gap:6px}
.pp-chip{color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:.5em .85em;white-space:nowrap;
  transition:color .35s var(--ease),border-color .35s var(--ease)}
.pp-row.is-hover .pp-chip{color:var(--text);border-color:var(--border-hi)}
.pp-blurb{grid-column:4 / span 2;margin:0;color:var(--muted);max-width:60ch}
.purpose-projects.is-hovering .pp-row:not(.is-hover) .pp-name,
.purpose-projects.is-hovering .pp-row:not(.is-hover) .pp-blurb,
.purpose-projects.is-hovering .pp-row:not(.is-hover) .pp-chip{opacity:0.38}
.pp-blurb,.pp-chip{transition:opacity .4s var(--ease),color .35s var(--ease),border-color .35s var(--ease)}
.pp-float{
  position:absolute;top:0;left:0;z-index:3;pointer-events:none;opacity:0;
  width:clamp(220px,22vw,340px);aspect-ratio:4/3;border-radius:var(--r-card);overflow:hidden;
  border:1px solid var(--border-hi);background:var(--bg-2);box-shadow:0 30px 70px -30px rgba(0,0,0,0.85);
  will-change:transform;
}
.pp-float img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0}
@media(max-width:760px){
  .pp-row{grid-template-columns:1fr}
  .pp-name,.pp-meta,.pp-blurb{grid-column:1}
  .pp-meta{flex-direction:row;flex-wrap:wrap}
}

/* The 3D object sits BEHIND the purpose rows (3 Sep). The run's canvas is
   z-index:2 inside .glass-run; the section is already position:relative, so
   lifting it above the canvas puts the names, chips and float in front while
   the solid shows through the open ground between them. */
#purpose{z-index:3}

/* AND BEHIND THE PORTFOLIO GRID (9 Sep 2026, Haseeb: "scaling up behind
   portfolio > revealing on top of marquee"). #work is inside .glass-run now,
   so the same one-line move applies: the section rides above the canvas's
   z-index:2 and the growing cube passes UNDER the tiles. #chips takes no
   z-index of its own, so the moment the run leaves the grid the cube is in
   front again — the reveal is the boundary itself, no class to toggle. */
#work{position:relative;z-index:3}

/* The first portfolio tile runs the full width (4 Sep), a 21:9 lead image.
   Not on home (11 Sep): home has its own --full breaks, and a lead there left
   tile 2 alone in its row with an empty column beside it. */
@media(min-width:761px){
  body:not([data-page="home"]) .work-grid > .work-tile:first-child{grid-column:1 / -1}
  body:not([data-page="home"]) .work-grid > .work-tile:first-child .wt-media{aspect-ratio:21/9}
}

/* 7 Sep: the Mission button under the dreamers & doers line (home #purpose). */
.purpose-cta-row{margin-top:clamp(20px,2.4vw,32px)}
.purpose-cta{display:inline-flex}
#purpose .purpose-projects[hidden]{display:none}   /* 7 Sep: the rows are hidden on home; .purpose-projects is display:flex otherwise */

/* ── Contact (7 Sep 2026, Haseeb: "centre the page and centre the form; use the
   retainer card styling to wrap the form") ── */
body[data-page="contact"] .page-hero .wrap{text-align:center}
body[data-page="contact"] .page-hero .page-lede{margin-left:auto;margin-right:auto}
body[data-page="contact"] .ct-actions{justify-content:center}
body[data-page="contact"] #enquiry .wrap{display:flex;flex-direction:column;align-items:center}
.ct-card{width:100%;max-width:820px;margin:0 auto;height:auto;min-height:0;align-self:center}
.ct-card .pf-form{position:relative;z-index:1}
.ct-hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none}   /* honeypot */
body[data-page="contact"] .ct-socials{justify-content:center}

/* ── THE CONVERSATIONAL FORM (7 Sep 2026, Haseeb: "make the form a conversational
   form"). One .cf-step at a time once form.js adds .cf-live; before that, or with
   JS off, every step stacks as an ordinary form and the head/nav/review are not
   there at all. form.js animates .cf-steps' height between steps (GSAP, 0.45s,
   expo.out) so the card never jumps; the card's min-height holds its presence. ── */
.ct-card{min-height:clamp(380px,46vh,480px)}
.ct-card .pf-form{margin-top:0;max-width:none;flex:1 1 auto}
.pf-form.cf-live{display:flex;flex-direction:column;justify-content:space-between;gap:clamp(22px,3vw,34px)}
.cf-head,.cf-nav,.cf-review{display:none}                                  /* no JS: plain stacked form */
.cf-live .cf-head{display:flex;align-items:center;gap:14px;flex:1;min-width:0;margin:0;padding:0 8px}   /* 7 Sep: in the nav row, between Back and Continue */
.cf-live .cf-back[hidden]{display:inline-flex;visibility:hidden}   /* keeps the row's geometry on step 1 */
.cf-live .cf-send[hidden]{display:none}
.cf-live .cf-nav{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:1.8em}
.cf-live .cf-review{display:flex;flex-direction:column;margin:0 0 clamp(18px,2.4vw,26px)}
.cf-count{color:var(--dim);flex:0 0 auto;min-width:3.4em}
/* 9 Sep, Haseeb: "make the progress line a fade out gradient." The track and the
   fill both run to nothing at the right-hand end, so the line reads as a measure
   that dissolves rather than a bar that stops. */
.cf-bar{flex:1 1 auto;height:1px;position:relative;overflow:hidden;
  background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,0.12) 58%,rgba(255,255,255,0.12))}
.cf-bar-fill{position:absolute;inset:0;transform-origin:left center;transform:scaleX(0.2);transition:transform .6s var(--ease);
  background:linear-gradient(90deg,rgba(255,255,255,0),var(--text) 45%,var(--text))}
.cf-steps{position:relative}
.cf-live .cf-step{display:none}
.cf-live .cf-step.is-on{display:block}
/* the incoming step rides in already CENTRED: absolute at the container's middle via
   the `translate` property, which GSAP's y tween (transform) leaves alone — so it
   lands exactly where flow will put it and never jumps (7 Sep). */
.cf-live .cf-step.is-in{display:block;position:absolute;left:0;top:50%;translate:0 -50%;width:100%}
.cf-q{display:block;font-size:var(--fs-head);font-weight:500;letter-spacing:-0.02em;line-height:1.15;
  color:var(--text);margin:0 0 clamp(16px,2.2vw,24px);max-width:24ch;text-wrap:balance}
.cf-step .pf-field{gap:0}
.cf-step .pf-textarea{min-height:120px}
.cf-hint{color:var(--dim);margin-top:10px}
.pf-error{font-family:var(--mono);font-size:var(--fs-micro);letter-spacing:0.01em;color:var(--neg);margin:10px 0 0}
.pf-error[hidden]{display:none}
.pf-input[aria-invalid="true"]{border-color:var(--neg)}
/* 7 Sep (Haseeb): Continue is the PRIMARY pill (the site's white 66→33% gradient
   over a backdrop blur, dark ink), Back the SECONDARY pill (5% white, white ink,
   his .btn-secondary recipe at this row's scale). Inter, not the mono overline. */
.cf-back,.cf-next{display:inline-flex;align-items:center;gap:.55em;cursor:pointer;border:0;border-radius:999px;
  padding:.9em 1.45em;font:inherit;font-size:var(--fs-btn);font-weight:500;letter-spacing:-0.01em;line-height:1;text-transform:none;
  -webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);
  transition:transform .35s var(--ease),opacity .3s var(--ease)}
.cf-next{background:linear-gradient(90deg,rgba(250,250,250,0.66),rgba(250,250,250,0.33));color:#0B0B0F}
.cf-back{background:linear-gradient(90deg,rgba(250,250,250,0.05),rgba(250,250,250,0.05));color:#fff}
.cf-next:hover{transform:translateY(-1px)}
.cf-back:hover{color:#fff}
.cf-back:focus-visible,.cf-next:focus-visible{outline:2px solid var(--a2);outline-offset:4px;border-radius:4px}
.cf-back[hidden],.cf-next[hidden]{display:none}
.cf-key{font-weight:400;opacity:.6}
@media(hover:none){.cf-key{display:none}}
.cf-status{margin:0;font-family:var(--mono);font-size:var(--fs-micro);color:var(--muted);flex:1 1 auto;text-align:center}
.cf-status[hidden]{display:none}
/* Review rows: the retainer list's hairlines; each row is a button back to its step. */
.cf-row{display:grid;grid-template-columns:6.5em minmax(0,1fr);align-items:baseline;gap:14px;width:100%;padding:.7em 0;margin:0;
  background:none;border:0;border-top:1px solid rgba(255,255,255,0.10);color:var(--text);font:inherit;text-align:left;cursor:pointer}
.cf-row:first-child{border-top:0}
.cf-row[hidden]{display:none}                                            /* the other branch's rows */
.cf-row-k{color:rgba(255,255,255,0.5);transition:color .25s var(--ease)}
.cf-row:hover .cf-row-k{color:var(--text)}
.cf-row:focus-visible{outline:2px solid var(--a2);outline-offset:3px;border-radius:6px}
.cf-row-v{font-size:.96rem;color:rgba(255,255,255,0.92);white-space:pre-wrap;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}
.cf-row-v:empty::before{content:'—';color:rgba(255,255,255,0.35)}
.cf.is-sent .cf-row{pointer-events:none}
.cf.is-sent .pf-submit{display:none}
/* ── THE BRANCHED FORM (7 Sep, later). Two-field steps carry an overline per
   field; the doors and the tiers are .glass tiles the size of a chip's big
   sibling; the budget screen is two labelled .pf-seg rows; the Stripe control
   after a retainer send is base.css's .btn-secondary, centred under the status. ── */
.cf-step .pf-grid .pf-field{gap:8px}
.cf-step .pf-label{color:var(--dim)}
.cf-opts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.cf-opts--3{grid-template-columns:repeat(3,minmax(0,1fr))}
/* ── COMPOSES base.css `.glass` (class="glass cf-opt") — the chip's recipe at
   tile scale: same hover, same checked tint, the ✓ in the corner. ── */
.cf-opt{position:relative;display:flex;flex-direction:column;gap:6px;padding:18px 44px 20px 20px;cursor:pointer;text-align:left;min-width:0;
  transition:border-color .3s var(--ease),background .3s var(--ease),transform .22s var(--ease-spring)}
.cf-opt input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.cf-opt:hover{border-color:rgba(255,255,255,0.24);
  background:linear-gradient(160deg,rgba(255,255,255,0.14),rgba(255,255,255,0.04) 50%);
  transform:translateY(-1px)}
.cf-opt:has(input:focus-visible){outline:2px solid var(--a2);outline-offset:3px}
/* 7 Sep (Haseeb, screenshot): selected = a WHITE gradient border (border-box
   gradient under a padding-box surface, so the radius is kept) and a slightly
   brighter surface; the tick is FA Sharp Regular circle-check via a mask. */
.cf-opt:has(input:checked){
  border-color:transparent;
  /* toned down (Haseeb: "you went too far"): a whisper of surface, a soft white edge */
  background:linear-gradient(160deg,rgba(255,255,255,0.075),rgba(255,255,255,0.025) 60%) padding-box,
             linear-gradient(135deg,rgba(255,255,255,0.38),rgba(255,255,255,0.14) 55%,rgba(255,255,255,0.07)) border-box;
}
.cf-opt::after{content:'';position:absolute;top:16px;right:16px;width:14px;height:14px;background:var(--text);opacity:0;transition:opacity .3s var(--ease);
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209l17-17L352 158.1l-17 17-111 111-47-47-17-17L126.1 256l17 17 64 64 17 17 17-17L369 209z'/></svg>") center/contain no-repeat;
          mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path d='M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209l17-17L352 158.1l-17 17-111 111-47-47-17-17L126.1 256l17 17 64 64 17 17 17-17L369 209z'/></svg>") center/contain no-repeat}
.cf-opt:has(input:checked)::after{opacity:.8}
.cf-opt-t{font-size:1.1rem;font-weight:500;letter-spacing:-0.015em;line-height:1.2;color:var(--text)}
.cf-opt-d{font-size:var(--fs-xs);line-height:1.5;color:var(--muted)}
.cf-rows{display:flex;flex-direction:column;gap:20px}
.cf-rowlab{display:block;color:var(--dim);margin-bottom:10px}
.cf-stripe{align-self:center;margin-top:0;padding:1.05em 1.6em;font-size:var(--fs-btn)}   /* base's 1.9em pill is a hero-scale piece; here it sits under a status line, so Send's scale */
.cf-stripe[hidden]{display:none}
html.rzv-no-motion .cf-bar-fill,html.rzv-no-motion .cf-back,html.rzv-no-motion .cf-next,html.rzv-no-motion .cf-row-k{transition:none}
@media(max-width:760px){
  .ct-card{min-height:0}
  .cf-q{font-size:1.4rem}
  .cf-opts,.cf-opts--3{grid-template-columns:1fr}
  .cf-opt{padding:16px 40px 16px 16px}
  .cf-row{grid-template-columns:5.5em minmax(0,1fr)}
  .cf-live .cf-nav{flex-wrap:wrap}
  .cf-status{flex-basis:100%;order:3;text-align:left}
}

/* 7 Sep (Haseeb, screenshot): no box shadow on the contact card; the aurora
   must cover the whole card — the home card's .au geometry was tuned to the
   offer grid and left the left third of this wider card dark. */
.offer-door.ct-card{box-shadow:none}   /* outranks form.css's .offer-door shadow */
.offer-door--retainer.ct-card > .au{inset:0;top:0;left:0;right:0;bottom:0;width:100%;height:100%;aspect-ratio:auto;transform:none;border-radius:inherit}   /* outranks form.css's card rule, which loads after this file */

/* ── Budget / timeline on phones: a radio LIST (7 Sep, Haseeb). The segmented
   pill becomes stacked rows with an outline circle; the slider element becomes
   the filled dot and slides between rows when a choice changes (form.js
   placeSeg). ── */
@media(max-width:760px){
  .cf-rows .pf-seg{display:flex;flex-direction:column;gap:0;padding:0;background:none;border:0;border-radius:0;width:100%}
  .cf-rows .pf-seg-btn{justify-content:flex-start;padding:13px 0 13px 30px;border-radius:0;border-bottom:1px solid var(--border);font-size:11px;letter-spacing:0.08em;white-space:normal}
  .cf-rows .pf-seg-btn:last-child{border-bottom:0}
  .cf-rows .pf-seg-btn::before{content:'';position:absolute;left:0;top:50%;width:16px;height:16px;border-radius:50%;border:1px solid var(--border-hi);transform:translateY(-50%)}
  .cf-rows .pf-seg:not(.has-pill) .pf-seg-btn:has(input:checked){background:none;color:var(--text)}
  .cf-rows .pf-seg:not(.has-pill) .pf-seg-btn:has(input:checked)::before{box-shadow:inset 0 0 0 4px var(--bg),inset 0 0 0 9px var(--text)}
  .cf-rows .pf-seg.has-pill .pf-seg-btn:has(input:checked){color:var(--text)}
  .cf-rows .pf-seg-pill{border-radius:50%;background:var(--text);z-index:1;
    transition:top .42s var(--ease-spring),left .3s var(--ease),width .3s var(--ease),height .3s var(--ease),opacity .3s var(--ease)}
}

.pf-grid .pf-field--wide{grid-column:1 / -1}   /* business name spans the row (7 Sep) */


/* ═══ BUTTON SYSTEM (7 Sep 2026, Haseeb: "3 buttons all follow same size and
   spacing logic — CTA pearl (steal from dashboard), primary 100%→66% white,
   secondary muted as is"). One geometry, three materials.
   · PEARL   the dashboard's New Chat pill material: radial rim light over the
             iridescent sweep over white, iriDrift, dark ink. Applied to the
             conversion CTAs: the footer's Get in touch and Send message.
   · PRIMARY the white pill, now 100% → 66%: card CTAs, Continue, Stripe start.
   · SECONDARY 5% white, white ink (base.css .btn-secondary): Back.
   Selectors are written to outrank form.css / base.css originals. ═══ */
.offer-door .offer-cta,.purpose-cta,.ft-talk-cta,.cf-next,.cf-back,.cf-stripe,.cf-send,.btn-pearl,.btn-secondary{
  display:inline-flex;align-items:center;justify-content:center;gap:.55em;
  padding:.95em 1.55em;font-size:var(--fs-btn);font-weight:500;letter-spacing:-0.01em;line-height:1;
  border:0;border-radius:999px;text-decoration:none;cursor:pointer;
  -webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);
  transition:transform .35s var(--ease),background .35s var(--ease),box-shadow .35s var(--ease)}
.cf-back,.cf-next,.cf-stripe{margin-top:0}
/* two originals outrank the shared rule by specificity or file order; restate the geometry for them */
.offer-door--projects .offer-cta,a.purpose-cta{padding:.95em 1.55em;font-size:var(--fs-btn)}
/* primary */
.offer-door .offer-cta,.purpose-cta,.cf-next,.cf-stripe{
  background:linear-gradient(90deg,rgba(250,250,250,1),rgba(250,250,250,0.66));color:#0B0B0F;box-shadow:none}
.offer-door .offer-cta:hover,.purpose-cta:hover,.cf-next:hover,.cf-stripe:hover{transform:translateY(-1px);box-shadow:none}
/* pearl — COMPOSES base.css .pearl (the site's own material: the dot, the
   brand mark, the tracer). The button only unsquares it: auto size, pill
   radius, the shared padding, dark ink. Markup: class="… pearl". */
.ft-talk-cta.pearl,.cf-send.pearl,.btn-pearl.pearl{font:inherit;border:1px solid rgba(255,255,255,0.65);cursor:pointer;display:inline-flex;align-items:center;gap:.55em;line-height:1;font-weight:500;letter-spacing:-0.01em;width:auto;height:auto;border-radius:999px;color:#0d0d14;--pearl-glow:0.45;
  padding:.95em 1.55em;font-size:var(--fs-btn);transition:transform .35s var(--ease),filter .3s var(--ease),box-shadow .4s var(--ease)}
.ft-talk-cta.pearl:hover,.cf-send.pearl:hover,.btn-pearl.pearl:hover{transform:translateY(-1px);color:#0d0d14}
/* secondary: base.css .btn-secondary is the source; Back composes it */
.cf-back{background:linear-gradient(90deg,rgba(250,250,250,0.05),rgba(250,250,250,0.05));color:#fff}
.cf-back:hover{color:#fff;transform:none}

/* 7 Sep (Haseeb, screenshot): the step sits in the card's vertical middle, and
   Back stands next to Continue on the right with the progress line on the left. */
.cf-live.pf-form,#contactForm.cf-live{display:flex;flex-direction:column}
.cf-live .cf-steps{position:relative;flex:1 1 auto;display:flex;flex-direction:column;justify-content:center;height:auto !important}   /* the JS height tween is retired by this: the card's min-height holds the frame */
.cf-live .cf-nav{gap:12px}
.cf-live .cf-head{order:0;flex:1;padding-left:0}
.cf-live .cf-status{order:1}
.cf-live .cf-back{order:2}
.cf-live .cf-next,.cf-live .cf-send{order:3}
.cf-live .cf-back[hidden]{display:none}   /* nothing to hold space for now that it sits by Continue */

/* ── THE SEND SEQUENCE (7 Sep 2026, Haseeb: "items inside the contact form blur
   fade out. Send message button gets loading animation from Pearl on the
   dashboard and moves to the center … then solid check circle"). form.js
   send() → liftOff() → land()/retreat() own every number; this is the geometry.
   · .cf-sent  the overlay the pearl lands in — absolute over the form, centred.
   · .cf-face  the dashboard pearl's three dots (portal.js 'loading'), 44px on
               the 64px disc — the same 24:34 ratio as the portal's face.
   · .cf-check FA 6 Sharp Solid circle-check, the pearl's own ink (#0d0d14 — the
               dots' colour, so the dots gathering INTO the check is one material).
   · .is-disc  the button in flight / seated: no padding, no hover, and NO
               transitions — form.js writes --pearl-glow per frame for the
               landing bloom, and a box-shadow transition would lag it. ── */
.cf-sent{display:none}
.cf-live .cf-sent{display:flex;position:absolute;inset:0;z-index:1;flex-direction:column;align-items:center;justify-content:center;text-align:center;pointer-events:none}
.cf-live .cf-sent[hidden]{display:none}
.cf-sent-seat{display:block;width:64px;height:64px;margin:0 0 clamp(20px,2.6vw,28px)}
.cf-sent-q{margin:0 0 12px;max-width:22ch;opacity:0}                 /* .cf-q carries the size; form.js rises it in */
.cf-sent-p{margin:0;font-size:var(--fs-sm);line-height:1.5;color:var(--muted);max-width:32ch;opacity:0}
html.rzv-no-motion .cf-sent-q,html.rzv-no-motion .cf-sent-p{opacity:1}
.cf-sent .cf-stripe{margin-top:22px;pointer-events:auto}
.cf-face,.cf-check{position:absolute;left:50%;top:50%;opacity:0;pointer-events:none}   /* margin-centred, not translated: GSAP owns transform for the check's pop */
.cf-face{width:44px;height:44px;margin:-22px 0 0 -22px;overflow:visible}
.cf-fd{fill:#0d0d14}
.cf-check{width:30px;height:30px;margin:-15px 0 0 -15px;fill:#0d0d14}
.cf-send.is-disc{padding:0;gap:0;overflow:hidden;white-space:nowrap;pointer-events:none;transition:none;transform:none;z-index:2}
.cf-send.is-disc:hover{transform:none;filter:none}
html.rzv-no-motion .cf-send.is-disc .cf-face{display:none}   /* no face loop without motion: the disc, then the check */

/* 7 Sep (Haseeb, phone screenshot): centred questions; on phones the progress line
   spans its own row above the buttons, Back bottom-left, Continue bottom-right. */
.cf-live .cf-step .cf-q,.cf-live .cf-hint,.cf-live .pf-error,.cf-live .cf-rowlab{text-align:center}
.cf-live .cf-step .cf-q{margin-left:auto;margin-right:auto}
.cf-live .cf-opts,.cf-live .cf-rows{margin-left:auto;margin-right:auto}
.cf-live .pf-chips,.cf-live .cf-chips,.cf-live .pf-set .pf-chip-row{justify-content:center}
.cf-live .pf-seg{margin-left:auto;margin-right:auto}
.cf-live .cf-rowset{align-items:center}
.cf-live .pf-field{text-align:left}
.ct-hp{display:none}   /* 7 Sep: Safari autofilled the off-canvas honeypot and scrolled to it; display:none is invisible to autofill and still present for bots */
@media(max-width:760px){
  .cf-live .cf-nav{flex-wrap:wrap;row-gap:14px}
  .cf-live .cf-head{order:0;flex:1 1 100%;padding:0}
  .cf-live .cf-status{order:1;flex:1 1 100%}
  .cf-live .cf-back{order:2;margin-right:auto}
  .cf-live .cf-next,.cf-live .cf-send{order:3;margin-left:auto}   /* right edge even when Back is hidden on step 1 */
}


/* ═══ CONTACT — 9 Sep 2026 ══════════════════════════════════════════════════
   The intro slide ("use first liner in the page, but now inside the aurora
   box"), the segmented switches justified across the card, and the footer's
   Let's talk row hidden here — the whole page IS the invitation. ═══ */
.pf-seg > .pf-seg-btn{flex:1 1 0;min-width:0}          /* justified on desktop; the ≤760 rule below already flexes them */
.cf-live.cf-pre .cf-nav{visibility:hidden}   /* the intro's own button is the control; the row keeps its space so nothing jumps */

/* 9 Sep: ONLY while it is the live step. `.cf-live .cf-step--intro` and
   `.cf-live .cf-step` have equal specificity, so the later rule won and the intro
   stayed on screen behind the name step. Both active classes are named here, and
   the step stretches so its content centres in the card rather than sitting on top. */
.cf-live .cf-step--intro.is-on,.cf-live .cf-step--intro.is-in{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;gap:clamp(24px,3.4vw,38px);
  min-height:clamp(220px,30vh,300px);height:100%}
.cf-live.cf-pre .cf-steps{flex:1 1 auto;display:flex;flex-direction:column;justify-content:center}
.cf-intro-line{margin:0;max-width:26ch;color:var(--text);
  font-size:var(--fs-head);font-weight:500;letter-spacing:-0.02em;line-height:1.2;text-wrap:balance}
.cf-begin{align-self:center}                            /* 9 Sep: under the line, not on the floor */
body[data-page="contact"] .ft-talk{display:none}


/* THE RIBBON BEHIND THE FORM (9 Sep 2026) — ribbon.css's host is written for a
   .page-hero / .case-hero; this one lives in #enquiry, so it takes its own box:
   the section is the containing block, the layer reaches the full card and is
   masked away before the section's edges. The card sits above it (z-index 1). */
.ct-enquiry{position:relative}
.ct-enquiry > .wrap{position:relative;z-index:1}
.ribbon-hero.ct-ribbon{top:0;bottom:0;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 14%,#000 82%,transparent 100%);
          mask-image:linear-gradient(to bottom,transparent 0,#000 14%,#000 82%,transparent 100%)}
.page-hero:has(> .ribbon-hero),.ct-enquiry:has(> .ribbon-hero){position:relative}


/* ═══ CONTACT, 9 Sep 2026 ═══════════════════════════════════════════════════
   The title lives in the card; the card centres in the viewport on phones; the
   intro centres against the card itself; and the browser's autofill paint is
   neutralised so an autofilled field looks like every other field (its yellow
   ground is also what made iOS scroll the page when it filled). ═══ */
.ct-title{margin:0 0 clamp(18px,3vw,28px);text-align:center}
/* A FULL screen, with the header's band paid on BOTH edges so the centre stays the
   viewport's centre and the card can never tuck under the fixed header. */
.ct-enquiry{display:flex;align-items:center;
  /* The section already starts one header down (the header is in flow), so a box of
     100vh centres a header LOW. Two headers come off: top offset + the box's own
     half-share, which puts the card's middle on the viewport's middle exactly. */
  min-height:calc(var(--vh,1vh) * 100 - var(--header-h,72px) * 2)}
.ct-enquiry > .wrap{width:100%}
/* The intro sits DEAD centre of the card: while it is showing, the nav row leaves
   the flow (it is invisible anyway), so the steps container is the form's only flow
   child and centres exactly. The row is still laid out, at the card's foot, so it
   costs no height when it returns. */
.pf-form.cf-live{position:relative}
.cf-live.cf-pre .cf-nav{position:absolute;left:0;right:0;bottom:0;margin:0}
.cf-live .cf-step--intro.is-on,.cf-live .cf-step--intro.is-in{justify-content:center}

/* The card centres in the viewport on its own: the socials below it were sharing
   its centring box and pushing it up by half their height. */
/* Anchored to the SECTION, not the card's wrap — the wrap is positioned (it sits
   above the ribbon), so an absolute child of it landed inside the card. */
.ct-enquiry > .ct-socials{position:absolute;left:0;right:0;bottom:clamp(16px,3.5vh,38px);margin:0;z-index:1}

/* Autofill: Chromium/Safari paint their own ground and text colour on a filled
   field. A huge inset shadow is the only way to override the ground, and
   -webkit-text-fill-color the only way to override the text. The long delay
   keeps the transition from ever revealing the yellow underneath. */
.pf-input:-webkit-autofill,
.pf-input:-webkit-autofill:hover,
.pf-input:-webkit-autofill:focus,
.pf-input:-webkit-autofill:active{
  -webkit-text-fill-color:var(--text);
  caret-color:var(--text);
  -webkit-box-shadow:0 0 0 1000px var(--field,rgba(255,255,255,0.04)) inset;
          box-shadow:0 0 0 1000px var(--field,rgba(255,255,255,0.04)) inset;
  transition:background-color 100000s ease-in-out 0s,color 100000s ease-in-out 0s;
}
@media(max-width:760px){
  .ct-enquiry{min-height:calc(var(--vh,1vh) * 100 - var(--header-h,62px) * 2)}
  .ct-title{margin-bottom:clamp(14px,4vw,22px)}
}

/* ═══ BIO HERO, 9 Sep 2026, Haseeb: "split the hero to show only the first sentence
   of the bio large 2/3 and 1/3 for the rest of the bio but smaller text." The service
   pages' lede split, applied here: the opening line at display size across two thirds,
   the rest as body copy in the last third, both top-aligned. ═══ */
.bio-lede{max-width:none;display:grid;grid-template-columns:2fr 1fr;
  column-gap:clamp(28px,5vw,80px);align-items:start}
.bio-lede-big{margin:0;font-size:var(--fs-lede);font-weight:300;
  letter-spacing:-0.03em;line-height:1.12;color:var(--text);text-wrap:balance}
/* 9 Sep: proper body text — the site's own body size, leading and colour, not just
   a smaller lede. (.t-body is on the element too; this only owns layout.) */
.bio-lede-small{margin:0;font-size:var(--fs-body);line-height:var(--lh-body);
  letter-spacing:var(--ls-body,normal);font-weight:400;color:var(--muted);text-wrap:pretty}
@media (max-width:760px){
  .bio-lede{grid-template-columns:1fr;row-gap:18px}
  .bio-lede-big{font-size:var(--fs-lede-m)}
}

/* 10 Sep, Haseeb: "milestone modal is not smooth, text jumps out and resets —
   should just be one simple motion as modal title can be same size and style
   exact." It was 16px on the card and up to 26px in the head, so the ghost had
   to SCALE across, and Flip's independent x/y scale is what made it stretch and
   snap. Identical type on both ends means the flight is a translate and nothing
   else. Scoped off .pm so the mission project modal keeps its own heading. */
.jm:not(.pm) .jm-h2{font-size:16px;font-weight:500;letter-spacing:-0.01em;line-height:1.3}
@media(max-width:760px){ .jm:not(.pm) .jm-h2{font-size:15px} }

/* 10 Sep 2026, Haseeb (screenshot, then "the image should stretch to edge of
   modal"): the picture sat inset — .wm-head is a .wrap, so the hero carried the
   wrap's side margins plus --gutter, and the head's top padding. It now breaks
   out of the wrap to the modal's full width and meets its top edge. The modal is
   edge to edge, so its corners are the screen's: the hero is square. The prev /
   next pair and the close stay where they were, now glass over the picture.
   The media's radius is 0 too — Flip.from animates borderRadius to whatever
   this says, and the close tween hands it back to the tile's own radius. */
.wm-head{padding-top:0}
.wm-hero{
  width:100vw;max-width:none;
  margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);
  height:calc(var(--vh,1vh) * 66);
  border-radius:0;
}
.wm-hero .wt-media{border-radius:0}
@media(max-width:760px){
  .wm-hero{height:calc(var(--vh,1vh) * 54);border-radius:0}
  .wm-hero .wt-media{border-radius:0}
}

/* ── Prev / next (10 Sep 2026) ──────────────────────────────────────────
   Replaces the next-project card. The site's glass circle arrows, the
   neighbouring project's name beside each — discreet: no mesh, no gradient,
   no thumbnail. The name brightens on hover; nothing else moves. */
.case-pn{display:flex;align-items:center;justify-content:space-between;gap:24px;
  padding:clamp(30px,4.5vw,52px) 0;margin-top:clamp(40px,6vw,80px);border-top:1px solid var(--border)}
.case-pn-link{display:inline-flex;align-items:center;gap:14px;min-width:0;
  color:var(--muted);text-decoration:none;transition:color .3s var(--ease)}
.case-pn-link:hover,.case-pn-link:focus-visible{color:var(--text)}
.case-pn-link:focus-visible{outline:none}
.case-pn-link:focus-visible .case-pn-arrow{outline:2px solid var(--a2);outline-offset:3px}
.case-pn-arrow{width:44px;height:44px;flex:none;border-radius:50%;padding:0;
  display:flex;align-items:center;justify-content:center;color:var(--text)}
.case-pn-arrow svg{width:15px;height:15px;fill:currentColor;display:block}
.case-pn-name{font-size:.95rem;letter-spacing:-0.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:760px){
  .case-pn{gap:12px}
  .case-pn-arrow{width:40px;height:40px}
  .case-pn-name{max-width:28vw;font-size:.88rem}
}
/* Inside the portfolio modal the pair is hidden: the modal steps through the
   grid of the page it was opened from (8 tiles on home, 16 on /work/), and its
   own top arrows already do that — names from the /work/ order at the bottom
   would disagree with where the modal actually goes. */
.wm .case-pn{display:none}
