/* ══════════════════════════════════════════════════════════════════════
   merheb.fun
   Palette carried over from prototype/hero-lando.html.
   ══════════════════════════════════════════════════════════════════════ */

:root{
  --cream:#F0E6DC;
  --ink:#1E1A17;
  --gold:#FAD214;
  /* 260, not 248. The email row is the widest fixed content the sidebar
     has to hold, and at 248 the address measured 163px into a 155px box
     — an 8px shortfall that ellipsised the .com away. (Measured on the
     old merheb.web3@gmail.com; the current merheb.xyz@gmail.com is a
     character shorter, so the clearance only grew.) Widened
     rather than shrinking the type, which was the other option: the
     address is the one piece of contact detail in the sidebar and it
     should not be the smallest text there.
     12px = the 8px shortfall plus 4px of slack, so a webfont landing
     slightly wider than the fallback can't re-clip it.
     Hero geometry is UNAFFECTED — every hero element is positioned in vw
     off the viewport, not off the sidebar — and that was verified by
     measuring all seven hero boxes before and after this change, not
     assumed. See the ghost-collapse note in CLAUDE.md for why that
     matters. */
  --nav-w:260px;
  --gutter:44px;
  --ease:cubic-bezier(.22,1,.36,1);

  /* ── Hero vertical rhythm ──────────────────────────────────────────
     The wordmark is sized in vw but everything under it used to be
     positioned in vh. Those scale on DIFFERENT axes, so on a wide-but-
     short window (i.e. any maximised browser: 1920x950 after chrome) the
     wordmark grew down into the nav-link row until they touched — at
     1920x950 the measured gap was exactly 0px, and at 1920x900 it was
     -24px, an actual overlap. The row could not be fixed by nudging its
     vh value, because the collision point moves with the aspect ratio.

     So the wordmark's own geometry is the single source of truth, and
     everything below it is offset from --mark-bottom in px. The whole
     hero stack now scales together on one axis and can't drift apart.
     Changing the wordmark's size or position automatically moves the nav
     row and the stat cards with it — do not re-hardcode either in vh. */
  --mark-top:6vh;                 /* .hero-wordmark-row's top          */
  /* 31vw, NO height cap. A min(31vw,54vh) ceiling was tried here to stop
     the mark crowding a short window — it worked, but it silently shrank
     the wordmark ~17% on a maximised 1920x910 screen, and the wordmark's
     size is settled (see CLAUDE.md). Everything below is offset from this
     mark's real bottom edge instead, so the layout adapts to the mark
     rather than the mark being shrunk to fit the layout. */
  --mark-size:31vw;               /* .wordmark-ghost's font-size       */
  --mark-lh:.78;                  /* .wordmark-ghost's line-height     */
  --mark-bottom:calc(var(--mark-top) + var(--mark-size) * var(--mark-lh));
  /* Offsets below --mark-bottom. 15px is the owner's chosen position: it
     puts the nav row at y=450 on a 1920x903 viewport (mark bottom 435),
     which is the maximised window this was picked against. Expressed as
     an offset rather than that literal 450px so the row keeps tracking
     the wordmark at other window sizes instead of colliding with it.
     Cards clear the row by a further 117px — at two labels the row and
     the first card deliberately shared a line, but at seven nav buttons
     the left cluster runs ~175px further right and straight into it. */
  --hero-navrow:calc(var(--mark-bottom) + 15px);
  /* 55px, not 132: at full 31vw the mark's bottom sits ~80px lower than it
     did under the old height cap, and a 132px offset pushed the SECOND
     card past the fold on a 910px-tall window. 55 clears the nav row
     (which is 19px tall, so ~21px of air) and still lands both cards
     fully on screen. */
  --hero-card1:calc(var(--mark-bottom) + 55px);
  --hero-cardh:clamp(110px,14.7vh,150px);
  --hero-card2:calc(var(--mark-bottom) + 55px + var(--hero-cardh) + 18px);
}

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

/* on-brand text selection — no jarring default-blue highlight */
::selection{background:var(--gold);color:var(--ink)}
::-moz-selection{background:var(--gold);color:var(--ink)}

html{-webkit-text-size-adjust:100%}

body{
  background:var(--cream);
  color:var(--ink);
  font-family:'Archivo',system-ui,-apple-system,sans-serif;
  font-weight:500;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
svg{display:block}

/* Ambient shadow silhouette — fixed behind all content, above the cream
   canvas. A dark, blurred, low-opacity cutout that persists as the page
   scrolls, giving a faint sense of him throughout. Bottom-masked so the
   torso crop is soft. Tune `opacity` to taste. */
.bg-silhouette{
  position:fixed;
  inset:0;
  z-index:-1;
  display:flex;
  justify-content:center;
  align-items:flex-end;
  overflow:hidden;
  pointer-events:none;
  opacity:.08;
}
.bg-silhouette img{
  height:96vh;
  width:auto;
  max-width:none;
  filter:brightness(0) blur(2.5px);
  -webkit-mask-image:linear-gradient(to bottom,#000 55%,transparent 96%);
          mask-image:linear-gradient(to bottom,#000 55%,transparent 96%);
}
@media (max-width:900px), (max-height:560px) and (pointer:coarse){ .bg-silhouette{display:none} }

/* ══════════════════════════════════════════════════════════════════════
   PRELOADER — opaque beige cover, painted before any content (this rule is
   in the head-linked stylesheet, so it applies on first paint). main.js
   slides it up once boot() finishes; the fully-formed hero sits underneath
   the whole time, so lifting the cover IS the reveal — no fade on the hero
   elements themselves, which keeps the ghost/z-index machinery untouched. */
.preloader{
  position:fixed;
  inset:0;
  z-index:9999;
  background:var(--cream);
  display:flex;
  align-items:center;
  justify-content:center;
  will-change:transform;
}
.preloader-word{
  font-size:clamp(38px,8vw,88px);
  font-weight:900;
  letter-spacing:-.05em;
  color:var(--ink);
}
/* No-JS / reduced-motion: never show the cover at all rather than risk it
   getting stuck (main.js is what removes it). */
@media (prefers-reduced-motion:reduce){ .preloader{display:none} }

/* Ghost targets exist only to be measured. Never seen, never focusable. */
.ghost{opacity:0;pointer-events:none;user-select:none}

/* A slot holds its element's space open once ghost.js absolutises it.
   ghost.js sets position:relative + min-width/min-height at measure time. */
/* line-height:0 kills the baseline descender gap. An in-flow inline-block
   sits on a baseline and reserves space beneath it; an absolutised one
   does not, so without this the slot collapses by a few px the moment
   ghost.js runs and everything below it creeps up. */
.slot{display:block;line-height:0}

/* Ghost-driven text must not be inline. An inline box reports its height
   from font metrics, but the moment ghost.js absolutises it the box
   becomes line-height tall — a few px of drift per element, compounding
   down the sidebar. inline-block measures the same in both states. */
[data-ghost][data-ghost-type="text"]{display:inline-block}

/* ══════════════════════════════════════════════════════════════════════
   SIDEBAR
   ══════════════════════════════════════════════════════════════════════ */

.nav{
  position:fixed;
  top:0;left:0;
  width:var(--nav-w);
  height:100vh;
  padding:20px;
  z-index:50;
}

/* Fixed 14px gaps, not space-between.

   space-between spread the leftover height evenly and the panels ended up
   25-26px apart; the reference runs a uniform 15px and reads much tighter,
   which is exactly the difference the owner flagged. But the slack is real
   — roughly 100px of it — and it has to land somewhere. It goes to
   .nav-links, which takes flex:1 and absorbs it into the menu panel
   itself (see the comment there); parked between panels instead it just
   read as a hole.

   Height stays the SAME formula Sidebar.scale() uses for `available`
   (window.innerHeight − 40, the .nav padding), so the growing panel has a
   real box to expand into, the last child (.nav-ctas' placeholder) still
   lands exactly at the bottom edge, and #cta-float — synced to that
   placeholder's rect in main.js — still follows it there, flush like
   NESH's Book a Call. When content is TALLER than the viewport the flex
   child stops growing, the box overflows instead of compressing,
   `scrollHeight` still reports the true content height, and
   Sidebar.scale() still shrinks it correctly. */
.nav-inner{
  width:calc(var(--nav-w) - 40px);
  height:calc(100vh - 40px);
  transform-origin:top left;
  display:flex;
  flex-direction:column;
  justify-content:flex-start;
  gap:14px;
}

/* Shared card treatment for every grouped block in the sidebar — same
   translucent-panel formula already used by .work-card / .quote / the old
   per-stat backgrounds, so the sidebar reads as one system with the rest
   of the page rather than a one-off.

   The panel itself carries no background — only layout. Two of these
   panels (brandrow, nav-links) ancestor a ghost-driven element, and CSS
   opacity on an ancestor suppresses every descendant regardless of how
   it's positioned. Putting the visual on a separate .nav-panel-bg layer
   means that layer's own reveal timeline never touches the ghost content
   living beside it. Applied uniformly, including to panels that ancestor
   no ghost today, so promoting something to a ghost later can't
   reintroduce this bug. (The stats are no longer such a case — they are
   ghost-driven now, and they are not .nav-panels either: each .stat-bg IS
   its own visible, travelling background.) */
.nav-panel{position:relative;isolation:isolate;padding:14px 16px}
/* 11px, down from 14. Measured off the reference rather than guessed: its
   panels sit at 10.65px on a 263px-wide sidebar, ours are 208px wide, so
   14px was proportionally rounder than the thing it was copying. */
/* Ink-tinted glass, the same values .hero-card-bg carries in the hero, so
   the sidebar's panels and the big glass cards on his torso are one
   material at both ends of the collapse. Over his photo it reads as glass;
   over the flat cream page it resolves to a warm dark slab.

   Everything sitting ON these panels had to flip with them — see
   .nav-tagline, .nav-copy, .nav-mail, .nav-client and .nav-socials. The
   nav pills are the exception: they stay light so their labels can stay
   black, which is what keeps the gold active state readable. */
/* .62, not the .44 this shipped with. Over the cream page .44 composites
   to #948c85 — a mid grey, not the dark glass it reads as over his photo
   in the hero. White text on it measured 3.31:1 and the gold figures
   2.25:1, both under WCAG AA (4.5 / 3.0 for large). .62 resolves to
   #6e6862: white 5.50:1, gold 3.74:1. Solved rather than eyeballed — the
   composite is ink*a + cream*(1-a), and .55 was still 4.48:1. */
.nav-panel-bg{
  position:absolute;
  inset:0;
  z-index:-1;
  background:rgba(30,26,23,.62);
  border:1px solid rgba(255,255,255,.26);
  border-radius:11px;
}

.nav-brandrow{
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap:8px;
}

/* The slot reserves space once ghost.js absolutises the wordmark.

   flex:0 1 auto, not flex:1 — the slot has to hug the wordmark for the
   gold pill below to have anything to trace. ghost.js writes the
   wordmark's own measured rect into this element's min-width/min-height
   anyway, so hugging is what it already resolves to at rest; the © keeps
   its right-edge position from .nav-brandrow's space-between either way. */
.nav-logo-slot{position:relative;flex:0 1 auto}
/* The reference sets its wordmark in black ON a gold slab, and that slab
   is the single strongest bit of colour in its sidebar.

   It cannot live on .wordmark itself: that node is ghost-driven, so any
   background it carries scales to hero size with it and paints a gold
   block the width of the screen. As a separate layer inside the slot it
   inherits the slot's rest-state box — which ghost.js has already sized to
   the wordmark exactly — and simply fades in as the mark lands. Timed to
   44-50% so it arrives with the wordmark, not before it (at hero scale the
   slot is still only a 190px reservation in the corner, and a gold
   rectangle sitting there while MERHEB is elsewhere reads as a bug). */
.nav-logo-pill{
  position:absolute;
  z-index:-1;
  left:-8px;right:-8px;
  top:-6px;bottom:-5px;
  border-radius:8px;
  background:var(--gold);
}

/* Uppercase to match heynesh.com's NESH scale — caps are ~40% taller per
   font-size than lowercase, which is what lets the mark fill the hero the
   way theirs does. Applied here (the REAL element, which is what actually
   renders at hero scale — the ghost is only a measuring stick) AND on
   .wordmark-ghost, so both render identical "MERHEB" text and ghost.js
   measures a width that matches what's painted. DOM text stays "merheb"
   for accessibility; text-transform handles the display. */
/* font-stretch pulls in Archivo's condensed width instance (loaded
   alongside the regular weights in index.html): narrower per unit cap-
   height, so the hero-scale mark can grow TALL without growing wide enough
   to clip the outer letters — see .wordmark-ghost below. */
.wordmark{
  display:inline-block;
  font-size:30px;
  font-weight:900;
  font-stretch:62.5%;
  letter-spacing:-.03em;
  line-height:.86;
  text-transform:uppercase;
  white-space:nowrap;
}

/* No opacity on either of these — both already carry a scroll-linked
   reveal that writes an inline opacity, so the CSS values they used to
   declare (.55 and .62) never actually applied and only read as an
   intention to be grey. Removed rather than left to mislead. */
.nav-copy{
  font-size:11px;
  font-weight:800;
  line-height:1;
  padding-top:3px;
  /* White, not --cream: cream on the panel measures 4.47:1, which misses
     AA by 0.03. Everything else on these panels is already #fff. */
  color:#fff;
}

/* No max-width. A 19ch cap held the copy to ~135px inside a 176px panel,
   so every line broke early and left a ragged column of dead space down
   the right-hand side. Let it use the panel's full measure and size it up
   to fill the block properly. */
.nav-tagline{
  font-size:12.5px;
  line-height:1.5;
  font-weight:500;
  color:#fff;
}

/* align-items:flex-start makes every pill hug its own label instead of
   stretching to the panel's full width — the reference's rows measure 103,
   145, 141 and 187px against a 263px panel, never edge to edge, and that
   ragged right edge is a large part of why its menu reads as a menu rather
   than a stack of bars.

   flex:1 is where .nav-inner's leftover height goes — into this PANEL,
   not into the gaps between panels. The reference does the same: its menu
   block is the tall one and everything else stays its natural size. Left
   in the gaps it read as drift (25px everywhere); parked between two
   panels it read as a hole; absorbed by the glass it just reads as the
   menu being the biggest thing in the sidebar, which it is. */
.nav-links{display:flex;flex-direction:column;align-items:flex-start;justify-content:space-between;gap:2px;flex:1 1 auto}

/* ── Nav pill ────────────────────────────────────────────────────────
   Row = [bg layer] [icon] [slot → the ghost-driven label].

   .nav-item MUST NOT ever receive an opacity tween: it ancestors a
   ghost-driven link, and ancestor opacity kills a descendant's paint even
   while that descendant is absolutely positioned at hero scale on the far
   side of the screen. Both the reveal fade and the gold active colour
   therefore live on .nav-item-bg — a SIBLING layer, not a property of the
   row. (Same reasoning and same fix as .nav-panel-bg above.)

   The icon is a sibling of .slot rather than padding inside it, because
   ghost.js lands the label at its slot's top-left corner and knows nothing
   about padding added within. Shifting the whole slot is measurable;
   padding inside it is not. */
.nav-item{
  position:relative;
  isolation:isolate;
  display:flex;
  align-items:center;
  gap:8px;
  /* 11px top / 9px bottom, not a symmetric 10. The label is all caps, so
     the space below its baseline (descender + half-leading = 6.5px) is far
     larger than the space above its cap line (3.6px) — split the padding
     evenly and the ink sits 1.4px above the pill's true centre. Measured,
     not eyeballed: this puts the cap band within 0.4px of centre and the
     icon within 1px, which is the closest the two can be reconciled while
     they share a row. The "they don't seem to be in the middle" fix. */
  padding:11px 12px 9px;
  border-radius:10px;
  /* pointer-events:none, with the link opting back in below.

     While a link is ghosted it is absolutely positioned at HERO scale,
     but the boxes that reserve its space — .slot, .nav-item-bg, .nav-ico —
     stay behind in the sidebar and remain hit-testable. The sidebar's nav
     panel occupies x20–228, and the giant "HOME"/"ABOUT" labels happen to
     render inside that band, so those two rows' reserved boxes (whichever
     paints last in DOM order) were swallowing the hover before it reached
     the label the cursor was actually over — HOME and ABOUT silently lost
     their roll while the other five, which sit right of x=228, kept it.

     Turning the row's own hit-testing off and re-enabling it only on the
     <a> means the hover always lands on the real label, wherever ghost.js
     currently has it. Same class of bug as the .hero-link-group
     pointer-events note below.

     Once the collapse has landed (main.js's CollapseFlag sets
     .is-collapsed on <html>) every element is back in its own box and the
     conflict is gone, so the whole pill becomes the hover target — a 40px
     row instead of just the glyphs of the word. */
  pointer-events:none;
}
.nav-item .nav-link{pointer-events:auto}
.is-collapsed .nav-item,
.no-ghost .nav-item{pointer-events:auto}
/* The pills stay LIGHT while their panel went dark. That's deliberate: the
   labels are ghost-driven, so whatever colour they rest at is the colour
   they'd also paint at hero scale — white labels here would mean white
   labels over the cream hero. Keeping the pills light lets the labels stay
   black at both ends, and keeps black-on-gold for the active row. */
.nav-item-bg{
  position:absolute;
  inset:0;
  z-index:-1;
  border-radius:9px;
  background:rgba(255,255,255,.86);
  /* 200ms — long enough to read as a state change, short enough not to lag
     the scroll position it reflects. */
  transition:background .2s var(--ease);
}
.nav-item:hover .nav-item-bg{background:#fff}

/* The gold arrives as its OWN layer, fading and settling in, rather than as
   a background-color swap on .nav-item-bg itself.

   As a crossfade it read as "a block of yellow appearing": interpolating
   white -> gold passes through a run of muddy creams that don't look like
   either state, so the eye discards the middle and registers only the
   endpoints — a snap, however long the duration. Fading a separate gold
   layer over the resting white keeps both endpoints honest for the whole
   transition, and lets the pill carry a small settle at the same time.

   Safe on this element specifically: .nav-item-bg is a SIBLING of .slot,
   never an ancestor of the ghost-driven link, so the ancestor-opacity trap
   documented at the top of this block does not apply here. Its own reveal
   tween ends at opacity:1, so nesting inside it costs nothing. */
.nav-item-bg::after{
  content:'';
  position:absolute;
  inset:0;
  border-radius:inherit;
  background:var(--gold);
  opacity:0;
  transform:scale(.92);
  transition:opacity .38s var(--ease),transform .38s var(--ease);
}
.nav-item.is-active .nav-item-bg::after{opacity:1;transform:scale(1)}

@media (prefers-reduced-motion:reduce){
  .nav-item-bg::after{transition:none;transform:none}
  .nav-item.is-active .nav-item-bg::after{transform:none}
}

/* State lives in `color`, NOT opacity. The reveal tween writes an inline
   opacity on this element as it fades in with the panel, and an inline style
   beats every CSS rule — a rest/hover/active set built on opacity here would
   simply never apply. currentColor drives the stroke below, so colour gives
   the same visual result with no collision. */
/* A bare glyph, NOT the dark badge this was. The badge idea came from
   .nav-socials, but seven of them stacked in a column read as a row of
   solid black squares — the reference puts a small, light-stroked mark
   straight onto the pill with no chip behind it, and that is what keeps
   its menu quiet enough to sit under a wordmark.

   Still 20px of box, so the row height is unchanged at 40px: .nav-item is
   its 20px vertical padding plus the taller of (icon, link line-box), and
   the link's line box is 19.5px. The glyph inside just no longer fills it. */
.nav-ico{
  display:flex;
  align-items:center;
  justify-content:center;
  flex:0 0 auto;
  width:20px;height:20px;
  color:var(--ink);
  transition:color .2s var(--ease);
}
/* FILLED, not stroked. The outline set these replaced was stock-icon-pack
   shapes at a 1.5 stroke — correct but anonymous. Solid silhouettes carry
   the same weight as the 900-condensed wordmark above them, and several
   use fill-rule:evenodd to knock detail OUT of the solid form (the play
   triangle, the door, the chat dots, the envelope flap) rather than
   drawing it as a line. */
.nav-ico svg{
  width:15px;height:15px;
  fill:currentColor;
  stroke:none;
}
/* Ink on gold, not gold on ink — the active pill is already the gold, so
   the glyph has to go the other way to stay visible on it. */
.nav-item.is-active .nav-ico{color:var(--ink)}

.nav-link{
  position:relative;
  font-size:13px;
  font-weight:800;
  text-transform:uppercase;
  letter-spacing:.04em;
  line-height:1.5;
  white-space:nowrap;
  /* FULL ink, no opacity. This element is ghost-driven: whatever rest
     state it carries travels with it to hero scale, and a previous
     opacity:.72 here was the sole reason the giant hero labels painted
     grey (~#5A5148) instead of black. The hover/active signal lives on
     .nav-item-bg (the pill) instead, which is not a ghost ancestor. */
  color:var(--ink);
}

/* ── Email row ─────────────────────────────────────────────────────── */
/* 14px 16px, up from 8px 9px — the owner asked for this panel bigger.
   .nav-links' flex:1 1 auto absorbs the reclaimed slack automatically. */
.nav-mail-panel{padding:14px 16px}
.nav-mail{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:6px;
  width:100%;
  font:inherit;
  font-size:13px;
  font-weight:600;
  letter-spacing:-.01em;
  color:#fff;
  background:none;
  border:0;
  cursor:pointer;
  text-align:left;
}
/* No rest-state opacity on either of these. Both were visibly grey against
   a sidebar the owner asked to read as solid black throughout; the hover
   signal now comes from the copy affordance changing, not from the address
   getting darker. */
.nav-mail-addr{
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.nav-mail-hint{
  display:flex;
  flex:0 0 auto;
  opacity:.55;
  transition:opacity .2s var(--ease);
}
.nav-mail:hover .nav-mail-hint{opacity:1}
.nav-mail-hint svg{
  width:15px;height:15px;
  fill:none;stroke:currentColor;stroke-width:1.75;
  stroke-linecap:round;stroke-linejoin:round;
}
/* copyButtons() swaps the icon for this word on success. */
.nav-mail.copied .nav-mail-hint{
  opacity:1;
  font-size:9px;
  font-weight:800;
  text-transform:uppercase;
  letter-spacing:.06em;
}

/* ── Client strip ──────────────────────────────────────────────────── */
.nav-clients{padding:10px 0}
/* The names run edge to edge and dissolve at both sides, so the loop has no
   visible start or end — a hard cut at the panel edge would read as clipped
   text rather than a moving strip. */
.nav-clients .marquee{
  overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 20%,#000 80%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 20%,#000 80%,transparent);
}
/* gap:0 is deliberate, not an oversight: the page-wide .marquee-track rule
   (below, for Trusted By) sets gap:44px, and since this scoped rule never
   declared its own gap, that 44px was leaking in unchallenged — item
   spacing here is meant to come entirely from .nav-client's own
   padding-right, same as the rest of the sidebar's spacing model. */
.nav-clients .marquee-track{display:flex;width:max-content;align-items:center;gap:0}
.nav-client{
  flex:0 0 auto;
  display:flex;
  align-items:center;
  gap:6px;
  padding-right:10px;
  font-size:12px;
  font-weight:700;
  letter-spacing:-.005em;
  white-space:nowrap;
  color:#fff;
}
/* All 15 are real logos now. Sized up from an original 16px — wordmark-
   style marks (VanEck, Upshot) were reading as an illegible smudge at that
   size, and object-fit:contain means growing the box only helps every
   other icon-style mark too, never crops or distorts them. background/
   color/font-size below are dead weight for the current all-<img> set but
   stay as the fallback presentation if an alt-text ever has to render
   (a broken image, a screen reader with images off). */
.nav-client-mark{
  flex:0 0 auto;
  display:grid;
  place-items:center;
  width:22px;height:22px;
  border-radius:6px;
  background:var(--ink);
  color:var(--cream);
  font-size:9px;
  font-weight:900;
  line-height:1;
  object-fit:contain;
  /* Mono. These are raster brand marks — profile artwork and wordmarks, not
     vectors — so this is a filter rather than a swap to black-and-white
     files: nothing to re-export, nothing to keep in sync, and it reverts by
     deleting one line. Fifteen different brand palettes running past in a
     190px strip was the loudest thing in the sidebar; desaturated, the
     strip reads as one row of marks. Contrast is nudged up because
     straight luminance leaves the mid-tone logos flat. */
  filter:grayscale(1) contrast(1.12);
}
/* Masked box, exactly one line tall — clips the clone until js/nav-roll.js
   rolls it into view on hover. Shared by the nav links AND the CTA buttons
   (both marked [data-roll-host]). 1lh inherits whatever line-height/
   font-size is current on the host, which matters here: ghost.js tweens
   the host's font-size continuously through the hero-scale collapse, and
   1lh tracks that automatically instead of a fixed px height that would
   only be correct at one scale. */
.roll-mask{
  position:relative;
  display:inline-block;
  overflow:hidden;
  /* Own the line metric explicitly. height:1lh reads THIS element's
     computed line-height — and the CTA button inherits line-height:0 from
     its .slot wrapper, which collapsed the mask (and the clipped label) to
     nothing. Setting it here means every host — nav link or button —
     clips to a real single line regardless of what it would otherwise
     inherit. 1.2 comfortably fits the uppercase caps without leading. */
  line-height:1.2;
  height:1lh;
  vertical-align:top;
}
/* Nav-pill-only: measured ink centers (canvas actualBoundingBox, not
   getBoundingClientRect) showed the all-caps label sitting ~2.8px above
   its .nav-ico's own ink center — the classic ascender-only-glyph vs.
   line-box asymmetry, same family of issue as the wordmark/nav optical
   centring elsewhere in this file. A transform here is safe: it's a
   sibling of ghost.js's target (.nav-link measures its own box, unaffected
   by a descendant's transform) and nav-roll.js only ever writes yPercent
   on the .roll-line children, never this element — so the two never
   collide. Scoped to .nav-link so the shared CTA buttons (Let's Work /
   About Me), which weren't reported as misaligned, are untouched. */
.nav-link .roll-mask{transform:translateY(2.8px)}
.roll-line{
  display:block;
  height:1lh;
}
.roll-line[data-roll="clone"]{
  position:absolute;
  top:0;
  left:0;
}

/* gap:0, and the two cards' inner corners and inner borders zeroed below,
   so the pair lands as ONE divided panel — the reference's arrangement.

   They stay two independent ghost-driven backgrounds all the same: the
   hero still shows two separate glass cards on his torso, at two different
   offsets, and they only become a single shape at the end of the collapse.
   That's what per-corner radius support in ghost.js buys — before it, the
   engine read one radius off the top-left corner and wrote it to all four,
   which reinstated the rounded inner corners at the end of every scrub. */
.nav-stats{display:flex;gap:0;position:relative}
/* The hairline between the halves. Its own element rather than a border on
   either card, because ghost.js tweens border COLOUR uniformly across all
   four sides — a divider expressed as one card's left border would drag
   every other edge to the same value with it. */
.stat-divider{
  position:absolute;
  /* z-index:1 is required, not decorative. The divider sits at exactly the
     seam, and the second .stat-card follows it in DOM order and paints its
     own background over that 1px — so at the default `auto` the line was
     rendering but permanently covered. */
  z-index:1;
  left:50%;top:12px;bottom:12px;
  width:1px;
  /* Light, not ink — an ink hairline on ink glass is invisible. */
  background:rgba(255,255,255,.26);
  pointer-events:none;
}

/* Left-aligned deliberately: no text-align/align-items here means the
   ghost-driven inline-block children rest flush at the card's left edge by
   default, matching ghost.js's positioning math (position:absolute;left:0;
   top:0 assumes exactly that resting position — centering it, as an
   earlier version of this did, leaves an offset ghost.js doesn't know
   about and the text lands a few px off). */
.stat-card{
  position:relative;
  flex:1;
  /* 116, up from 84 (itself up from 64) — the owner asked for this panel
     bigger. .nav-links keeps flex:1 1 auto in .nav-inner's column, so it
     absorbs the slack this reclaims automatically; the sidebar still fills
     exactly to the bottom with no gap, just with tighter pill spacing.
     Only the SIDEBAR card grows: the hero's counterpart pins its icon out
     of flow (see .hero-card-ico), because the two hero cards sit 18px
     apart at frozen offsets and a taller card would run into the one
     below it. */
  min-height:116px;
  padding:14px 14px 14px;
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
  gap:1px;
  isolation:isolate;
}
/* Pins the icon to the top of the card while the figure and label stay
   bottom-aligned, which is the reference's arrangement and also means the
   figure's resting position is unchanged by the icon's arrival. */
.stat-ico-slot{margin-bottom:auto}
.stat-ico{
  display:block;
  width:20px;height:20px;
  color:var(--gold);
}
/* 100% of a box ghost.js is actively tweening — the glyph scales with the
   card instead of being redrawn at each end. */
.stat-ico svg{
  width:100%;height:100%;
  display:block;
  fill:none;
  stroke:currentColor;
  stroke-width:1.9;
  stroke-linecap:round;
  stroke-linejoin:round;
}
/* Real, ghost-driven background: starts as a big glass card in the hero
   (see .hero-card-bg) and shrinks continuously into this small panel as
   you scroll. A separate box that only cross-fades in place, at a fixed
   size, reads as idle next to everything else that's actually traveling —
   this is the fix for exactly that. */
/* Resting values are EXACTLY .nav-panel-bg's, so every panel in the
   sidebar reads as one material. This card is the only one that isn't a
   .nav-panel — it's a travelling ghost — so any difference here shows up
   as the one panel that doesn't match. Both ends of the tween are now the
   same value, so ghost.js skips the colour crossfade entirely: the card
   keeps one tone from hero scale all the way into the sidebar.

   That ruled out the sheen and the drop shadow this used to carry. Both
   sat on the REAL element, which is what paints at every scale, so they
   applied in the sidebar as well as the hero and there is no way to have
   them at one end only. backdrop-filter stays: it's the glass over his
   torso in the hero, and in the sidebar it blurs a flat cream background
   into itself, which is a no-op. */
.stat-bg{
  position:absolute;
  inset:0;
  z-index:-1;
  border-radius:10px;
  background:rgba(30,26,23,.62);
  border:1px solid rgba(255,255,255,.26);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
}
/* The seam. Both facing edges lose their radius and their border so the
   two cards butt together into one continuous panel; .stat-divider draws
   the single line that belongs there. ghost.js reads all four corners and
   all four sides now, so these zeroes survive the scrub. */
.stat-card:first-child .stat-bg{
  border-top-right-radius:0;
  border-bottom-right-radius:0;
  border-right-width:0;
}
.stat-card:last-child .stat-bg{
  border-top-left-radius:0;
  border-bottom-left-radius:0;
  border-left-width:0;
}
/* Gold at BOTH ends (see .hero-card-num) — the reference sets its figures
   in yellow and its labels in solid bold ink, and matching that also means
   ghost.js skips the colour tween for this pair entirely. */
.stat-num{
  display:inline-block;
  font-size:28px;
  font-weight:900;
  letter-spacing:-.045em;
  line-height:1.3;
  white-space:nowrap;
  color:var(--gold);
}
/* Full ink, expressed as COLOUR rather than opacity. opacity is not a
   property ghost.js tweens, so the opacity:.55 this once carried applied
   at hero scale too and greyed the giant white label over his shirt; the
   rgba(...,.72) that replaced it was still visibly soft in the sidebar.
   As a solid colour it reads black where it rests and still crossfades to
   pure white at hero scale via .hero-card-label. */
.stat-label{
  display:inline-block;
  font-size:10px;
  font-weight:800;
  letter-spacing:.14em;
  text-transform:uppercase;
  line-height:1;
  white-space:nowrap;
  /* White at BOTH ends now that the card is dark at both ends — same value
     as .hero-card-label, so ghost.js skips the colour tween for this pair
     the way it already does for the gold figures. */
  color:#fff;
}

/* Small dark badges, not bare icons — grouped with the wordmark rather
   than trailing near the bottom of the sidebar on their own. */
/* 24px badges at 6px gaps = 174px across six of them, inside a 176px
   content width. At the 26px they were, the row measured 186px and the
   sixth — Abstract — wrapped onto a line of its own. */
.nav-socials{display:flex;gap:6px;flex-wrap:wrap;margin-top:12px}
/* Inverted with the panel: an ink badge on ink glass disappears, so these
   are cream chips with ink glyphs now. Hover still goes gold. */
.nav-socials a{
  width:24px;height:24px;
  display:flex;
  align-items:center;
  justify-content:center;
  border-radius:7px;
  background:var(--cream);
  color:var(--ink);
  transition:background .25s var(--ease),color .25s var(--ease);
}
.nav-socials a:hover{background:var(--gold);color:var(--ink)}
.nav-socials svg{width:12px;height:12px}

.nav-ctas{display:flex;flex-direction:column;gap:7px}

.btn{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:center;
  height:38px;
  border-radius:10px;
  font-size:13px;
  font-weight:800;
  letter-spacing:-.01em;
  white-space:nowrap;
  /* Was missing entirely, so the :hover background snapped instead of
     animating. background-color only, deliberately — this element (the
     real Let's Work button) has its OWN transform under continuous GSAP
     control throughout the scroll-collapse (see ghost.js); a CSS
     transition on transform here would fight that, the same class of
     conflict as the wordmark scaleY issue earlier this project. */
  transition:background-color .25s var(--ease);
}
.btn-primary{background:var(--gold);color:var(--ink)}
.btn-primary:hover{background:#ffe45c}
.nav-ctas .slot{width:100%}

/* Same width as .btn (via the shared .btn class) so .nav-ctas reserves
   exactly the layout space the real buttons need — invisible (no
   background/text, and visibility:hidden as a second guarantee) since
   the actual buttons live in #cta-float now. */
.cta-placeholder{visibility:hidden}

/* The real buttons, positioned to exactly overlay .nav-ctas' placeholders
   above — kept in sync by js/main.js (CtaFloat.sync / the resize handler),
   not hardcoded, since .nav-ctas' exact position depends on sidebar
   content height and Sidebar.scale(). z-index 65 is unconditionally above
   his portrait's explicit 60 (see the .hero-cta-row comment) — unlike
   .nav, this container never needs to duck behind him, so there's no
   toggle to replicate here. */
.cta-float{
  position:fixed;
  z-index:65;
  /* Same natural (pre-scale) width as .nav-inner — js/main.js applies
     the same Sidebar.scale() factor here via a matching transform, so at
     that width + that scale it renders at the exact size/position
     .nav-ctas' placeholders occupy. */
  width:calc(var(--nav-w) - 40px);
  display:flex;
  flex-direction:column;
  gap:7px;
  /* The container is a z-index:65 rectangle parked at the sidebar CTA
     slot. When Let's Work is ghost-absolutised out to the hero (centred
     on his torso), this empty box stays behind at the lower-left — and at
     hero scale the Work/Platforms link cluster renders right on top of it,
     so an auto container silently ate their hovers. none lets the empty
     area pass events through; only the real button (wherever it renders)
     stays interactive. */
  pointer-events:none;
}
.cta-float .slot{width:100%}
.cta-float .btn{pointer-events:auto}

/* A transition on anything the ghost engine tweens fights it: the browser
   eases toward every scrub frame, so the element lags the scroll and
   rubber-bands for the whole collapse. Ghost-driven nodes may only
   transition colour. Must stay after the rules it constrains. */
[data-ghost]{transition-property:background-color,border-color,color,opacity}

/* ══════════════════════════════════════════════════════════════════════
   PAGE + HERO
   ══════════════════════════════════════════════════════════════════════ */

.page{padding-left:var(--nav-w)}

/* Available to assistive tech, absent from the layout. clip-path over the
   old clip:rect() hack, and NOT display:none / visibility:hidden — those
   remove the element from the accessibility tree too, which would defeat
   the point. */
.sr-only{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
}

/* The collapse consumes 1150px (see ghost.js); the rest is the beat
   before the stage unpins and the page moves on. */
.hero{
  position:relative;
  height:260vh;
  /* Break out of the sidebar's gutter — at rest the sidebar is entirely
     flown out into this composition, so the hero owns the full viewport. */
  margin-left:calc(-1 * var(--nav-w));
  width:calc(100% + var(--nav-w));
}

/* Layered, not stacked: the wordmark, the nav and the portrait all overlap
   deliberately, so children are positioned rather than flowed.

   The z-index belongs HERE, not on .hero-portrait. position:sticky always
   creates a stacking context, so a z-index on a child is scoped inside this
   box and can never outrank .nav (z 50) at the root — the wordmark would
   paint over his face. Raising the stage itself puts him in front; a
   data-tl on the stage drops it back under the sidebar as that assembles. */
.hero-stage{
  position:sticky;
  top:0;
  height:100vh;
  overflow:hidden;
  z-index:60;
  /* This whole box sits above .nav (z-index 50) at rest by design (see
     comment above) — but it's pure staging: ghost targets + the portrait
     image, nothing in it is actually interactive. Left at the default
     auto, its full-viewport box silently caught every hover/click meant
     for the real sidebar content underneath (confirmed directly via
     Playwright: hovering the real nav-link at hero scale failed with
     ".hero-stage ... intercepts pointer events"). none lets everything
     fall through to the real, interactive elements in .nav below it. */
  pointer-events:none;
}

/* Mobile only — the hero's line of copy, un-hidden in the max-width:900px
   block where the reasoning lives. Off by default rather than on-and-
   hidden-later so it can never flash during a desktop load. */
.hero-mtag{display:none}

/* Lowered so the caps sit across the top HALF of the viewport (not hugging
   the very top edge), his head breaking up into the letterforms — the NESH
   composition. At 36vw the caps are ~40% of viewport height; top:6vh spans
   them from ~6% to ~46%, i.e. the top half. */
.hero-wordmark-row{
  position:absolute;
  /* var, not a literal — --mark-bottom is derived from this, and the nav
     row + stat cards are derived from that. See :root. */
  top:var(--mark-top);
  left:50%;
  /* A fixed px nudge here doesn't hold across viewport widths — measured
     ~8px right-of-center bias at 1440px but ~16px at 1710px, not a fixed
     amount or a clean multiple of font-size. Rather than chase an exact
     centering correction, .wordmark-ghost's font-size is sized with enough
     side margin to absorb this rendering bias at any desktop width instead. */
  transform:translateX(-50%);
  z-index:1;
}

/* Sized to match heynesh.com's PAINTED glyph height, not its bounding-box
   height. Their mark is an SVG whose box hugs the letters, so box == glyph.
   Ours is live text: a tall line-height pads the box with empty space and
   makes the letters look SMALL even when the box "matches" — which is
   exactly what went wrong at 25vw/.99. So: tight line-height (glyphs fill
   the box) + font-size dialled until our letters read as tall as their
   NESH. This bleeds off both edges horizontally, same as the reference. */
/* "All 6 visible" sizing: MERHEB (6 caps) spans ~the full viewport width,
   every letter on screen, edge to edge the way NESH shows all 4 of theirs.
   23vw is the value that fits all six at 1440 with the outer letters just
   kissing the edges. It's ~25% shorter than NESH's letter height — the
   unavoidable cost of a 6-letter word fitting the same width — but reads as
   big bold caps because every letter is full cap-height (unlike the old
   lowercase, where 4 of 6 letters were short x-height). */
.wordmark-ghost{
  /* var, not a literal — see the --mark-* block in :root. */
  font-size:var(--mark-size);
  font-weight:900;
  font-stretch:62.5%;
  /* Must match .wordmark's letter-spacing exactly (-.03em). ghost.js
     tweens only font-size, not letter-spacing, so at hero scale the
     REAL wordmark renders with .wordmark's -.03em while this invisible
     target keeps its own — any difference makes the target's measured B
     edge disagree with the painted one (was -.04em → ~25px short over 5
     letter gaps at 31vw), which then threw LinkAlign's right cluster off
     by that much. */
  letter-spacing:-.03em;
  line-height:var(--mark-lh);
  text-transform:uppercase;
  white-space:nowrap;
  color:var(--gold);
}

/* Two clusters, not an even spread: the middle has to stay clear or the
   links run straight across his face. top:58vh clears the wordmark's own
   box (bottom edge lands ~55vh at current sizing) — this row is above the
   wordmark in z-index, so any overlap paints link text straight over the
   gold letters. */
/* Each cluster is independently positioned (not a single flex row anymore)
   — placed via the hero layout editor artifact, dragged over the real
   portrait rather than guessed, so the two clusters no longer have to
   sit at a shared top or hug the row's own edges via space-between. */
.hero-links-row{
  position:absolute;
  inset:0;
  z-index:2;
  pointer-events:none;
}
.hero-link-group{
  position:absolute;
  display:flex;
  align-items:center;
  gap:clamp(10px,1.3vw,20px);
  /* NOT pointer-events:auto — this is the invisible ghost-target group
     (aria-hidden, measurement only), and at hero scale it sits exactly
     over the real .nav-link underneath. auto here was silently
     swallowing hover/click before it ever reached the real link — the
     nav-roll hover effect (js/nav-roll.js) only worked once scrolled
     into the sidebar, where the ghost group is no longer overlapping. */

  /* Wrap, capped to the space that actually exists beside his portrait.

     His portrait is sized from viewport HEIGHT (.hero-portrait is
     height:102vh), so on a narrow-but-tall window he occupies a much larger
     share of the width and the room either side of him shrinks. Pixel-
     scanning the cutout's alpha at this row measured his ink at ~61.5vh
     wide; (100vw − 61.5vh)/2 is therefore the clearance per side, less a
     20px breathing gap.

     At 1425x900 that's 415px a side and all seven labels sit on one line.
     At 965x900 it's 185px and they wrap to two or three — which is the
     point: seven labels need ~653px of row and only ~411px exists there.
     Wrapping also fixes a pre-existing bug, since the OLD five labels
     already overlapped his torso by ~140px at that size. */
  flex-wrap:wrap;
  row-gap:2px;
  max-width:calc((100vw - 61.5vh) / 2 - 20px);
}
/* The right cluster is anchored by its RIGHT edge (LinkAlign pins it to the
   wordmark's B), so its wrapped lines must stay right-aligned or a short
   final line would drift toward his body instead of away from it. */
.hero-link-group:last-child{justify-content:flex-end}
/* left/right are set in px at runtime by LinkAlign (js/main.js), which
   measures the wordmark's actual M/B glyph edges every boot+resize — the
   only way to track them exactly, since the letter edges sit at
   (viewport − wordmarkWidth)/2, not a fixed fraction of the viewport. The
   % values here are a static fallback for the ~2px range they'd otherwise
   be off if the JS ever fails to run; top stays CSS-driven. */
/* Offset from the wordmark's bottom rather than a vh value — see the
   --mark-* block in :root for why (the two used to scale on different
   axes and collided on any maximised window). At 1440x900 this resolves
   to the same 494px the old 54.9vh gave, so that size is unchanged. */
.hero-link-group:first-child{ top:var(--hero-navrow); left:4.09%; }
.hero-link-group:last-child{ top:calc(var(--hero-navrow) - 4px); right:2.3%; }
.hero-link{
  position:relative;
  /* Sized independently of .nav-link's 13px rest state — ghost.js DOES
     tween font-size (unlike letter-spacing, see the note below), so the
     landing-page row and the collapsed sidebar pill are free to differ;
     that gap is what makes the collapse read as a shrink, not just a
     reposition. Capped at 17px on purpose — the owner asked for this row
     specifically bigger. An earlier pass (pre-7-pill sidebar, pre
     hero-rhythm rebase) found 17px crept the right cluster's start ~26px
     into his torso and capped this at 15px; re-measured after those
     changes at 1920x903, 1920x760 and 1440x900 with 17px restored —
     78-198px of clearance on both clusters at every size checked, no
     overlap. Re-check clearance if this is ever revisited. */
  font-size:clamp(12px,.9vw,17px);
  font-weight:800;
  /* Must match .nav-link's .04em exactly. This is the ghost TARGET; the
     REAL, visible link is .nav-link, which ghost.js positions by LEFT edge
     and tweens font-size on — but NOT letter-spacing. So the real link
     keeps .04em while this target had .01em, making the real "Get In Touch"
     ~5px wider than the target it's measured against — the right cluster's
     end then overshot the B by that much. Matching them makes real width =
     target width, so the measured right edge is the painted one. */
  letter-spacing:.04em;
  line-height:1.5;
  white-space:nowrap;
  text-transform:uppercase;
}
.hero-sep{
  font-size:clamp(12px,1.12vw,17px);
  font-weight:500;
  opacity:.28;
  user-select:none;
}

/* Above .nav (z 50) so the wordmark and links pass behind his shoulders.
   By the time the sidebar assembles he has already faded out. */
/* Matches heynesh.com's character framing now that the cutout is a tall
   head-to-waist figure (not the old chest-up bust). Top-anchored so his
   hair sits near the top edge like Nenad's (~5vh), and tall enough that
   the torso runs OFF the bottom edge — no crop line, because the figure
   extends past the viewport. (The old top-anchor exposed a crop line only
   because that chest-up cutout was too short to reach the bottom; this one
   isn't.) */
.hero-portrait{
  position:absolute;
  z-index:60;
  top:5vh;
  left:50%;
  transform:translateX(-50%);
  height:102vh;
  pointer-events:none;
}
.hero-portrait img{height:100%;width:auto;max-width:none}

/* A second sticky layer, fixed below the sidebar rather than riding
   hero-stage's own toggling z-index — see the HTML comment above
   .hero-decor for why. Same top:0/height:100vh as hero-stage, so vh-based
   positioning of its children resolves identically.

   margin-top:-100vh matters: as the next sibling after the 100vh-tall
   hero-stage, this box's own normal-flow resting position would start
   100vh further down the document — it would only start sticking once
   scroll had already passed hero-stage's entire pinned range, never
   actually overlapping it. Pulling it back up by hero-stage's height
   realigns both siblings' resting position to the same point, so they
   stick over the identical scroll range. */
.hero-decor{
  position:sticky;
  top:0;
  height:100vh;
  margin-top:-100vh;
  z-index:10;
  pointer-events:none;
}

/* Tinted glass. Each card is independently positioned (placed via the hero
   layout editor artifact, not guessed) rather than stacked in a flex
   column with a relative offset — card 2 needed to land further DOWN and
   further LEFT than card 1, which a flex column + margin-left can't do. */
.hero-cards{
  position:absolute;
  inset:0;
  z-index:70;
  pointer-events:none;
}
/* Not pointer-events:auto — same ghost-target-swallows-events mistake as
   .hero-link-group above. These are aria-hidden measurement targets. */
.hero-cards .hero-card{position:absolute}
/* Sits BELOW the nav-link row now, not level with it. They used to share
   a row deliberately, which worked while the row was two labels wide and
   stopped short of the card. At seven nav buttons the left cluster runs
   ~175px further right and straight into this card, so the card drops
   clear instead. Both are offset from --mark-bottom so the gap between
   them holds at every viewport. */
.hero-cards .hero-card:nth-child(1){ top:var(--hero-card1); left:19.2vw; }
.hero-cards .hero-card:nth-child(2){ top:var(--hero-card2); left:16.6vw; }

/* Bigger box, pulled in from the right edge (was hugging it), and a real
   frosted-glass treatment — translucent, blurred, ink-tinted.

   Flat rgba(30,26,23,.62), same as .hero-card-bg / .stat-bg / .nav-panel-bg
   — one material across all three hero-panel types. Used to carry its own
   58%->30% gradient, tuned before the sidebar's WCAG-contrast pass landed
   the other panels on a flat .62; left un-synced, it read as a visibly
   different, lighter glass next to the stat cards. */
.hero-traits{
  position:absolute;
  z-index:70;
  left:73.3vw;
  /* Anchored to --hero-navrow (the wordmark-derived var the nav row itself
     uses), not a flat vh. A flat vh (tried 59.5, then 62.5) drifted ~90px
     relative to the nav row across viewport heights — the row's own
     position barely moves with height (dominated by --mark-size's vw
     term), so a vh-only offset either undershot or badly overshot
     depending on height. Same trap as the "Hero rhythm — the vw/vh bug"
     already documented for .hero-navrow itself; fixed the same way. */
  top:calc(var(--hero-navrow) + 55px);
  list-style:none;
  padding:clamp(20px,2.2vw,30px) clamp(24px,2.6vw,36px);
  border-radius:16px;
  background:rgba(30,26,23,.62);
  border:1px solid rgba(255,255,255,.26);
  box-shadow:0 10px 34px rgba(30,26,23,.30), inset 0 1px 0 rgba(255,255,255,.26);
  backdrop-filter:blur(18px);
  -webkit-backdrop-filter:blur(18px);
  display:flex;
  flex-direction:column;
  gap:clamp(8px,1.1vh,14px);
  color:#fff;
}

.hero-traits li{
  display:flex;
  align-items:center;
  gap:13px;
  font-size:clamp(16px,1.5vw,22px);
  font-weight:800;
  letter-spacing:-.01em;
  text-shadow:0 1px 4px rgba(30,26,23,.35);
}
/* Real glyphs per format, replacing a generic rotated-square diamond that
   said nothing about the item it marked. Same drawing convention as the
   sidebar nav icons (24-grid, stroked, no inline attrs), stroked gold and
   sized up for the panel. */
.hero-traits li svg{
  width:clamp(16px,1.35vw,21px);
  height:clamp(16px,1.35vw,21px);
  flex:none;
  fill:none;
  stroke:var(--gold);
  stroke-width:1.9;
  stroke-linecap:round;
  stroke-linejoin:round;
}



/* Ghost TARGET only (invisible, aria-hidden) for #cta-float's real
   buttons — see the long comment on .nav-ctas in index.html for the full
   picture. Centred over his torso on purpose: placed after
   .hero-portrait in the DOM, with a z-index above his own explicit 60,
   so this — and by extension the real buttons ghost.js measures against
   it — reads in front of him instead of hidden under his shirt. Bigger
   than the sidebar's own buttons, matching how oversized the rest of the
   hero-scale composition reads. */
.hero-cta-row{
  position:absolute;
  /* .hero-portrait carries its own explicit z-index:60 within this same
     .hero-stage stacking context (not just the stage's 60) — DOM order
     after it wasn't enough on its own; needs a higher explicit z-index
     to actually paint on top of him. Fine to do here, unlike the
     nav-vs-hero-stage case: both these are siblings inside the SAME
     local stacking context, so plain numeric z-index comparison applies. */
  z-index:61;
  left:50%;
  bottom:8vh;
  transform:translateX(-50%);
  display:flex;
  gap:clamp(14px,1.8vw,28px);
}
.hero-btn{
  display:flex;
  align-items:center;
  justify-content:center;
  height:clamp(64px,8vh,92px);
  padding:0 clamp(34px,3.6vw,56px);
  border-radius:16px;
  font-size:clamp(18px,1.8vw,26px);
  font-weight:800;
  white-space:nowrap;
  background:var(--gold);
  color:var(--ink);
}
/* .hero-btn-decor (the visible "About Me" twin) had no hover at all —
   .hero-stage is pointer-events:none (see its own comment), which this
   inherits by default, so the mouse never even reached it. pointer-events
   restored just here (it isn't a link, so it can't swallow a click meant
   for anything real) plus its own background-only transition — same
   reasoning as .btn's: this element's transform is under continuous
   scroll-linked GSAP control (data-tl), so a hover transition stays off
   transform to avoid fighting it.

   Background overridden to near-white, off the shared gold .hero-btn:
   both buttons read as equally primary at identical gold, so neither
   led. A bare outline was the other option and was rejected — it sits
   directly over his shirt at this position, and a transparent pill with
   a thin border has poor contrast against dark brown fabric. White is
   the sidebar's own secondary-surface material (.nav-item-bg's rest
   state), just brought up to full opacity for a much bigger element
   sitting on a photo instead of a small pill on flat cream. Scoped to
   -decor only — .hero-btn itself (Let's Work's ghost target) is
   untouched, so the ghost engine's measured colour is unaffected. */
.hero-btn-decor{
  background:rgba(255,255,255,.94);
  border:1px solid rgba(30,26,23,.12);
  pointer-events:auto;
  transition:background-color .25s var(--ease);
}
.hero-btn-decor:hover{background:#fff}

.hero-card{
  position:relative;
  width:clamp(140px,11.8vw,180px);
  min-height:clamp(110px,14.7vh,150px);
  padding:clamp(16px,1.7vw,24px);
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
  gap:clamp(2px,.5vh,6px);
  isolation:isolate;
}
/* This is the invisible GHOST TARGET (measurement + "from" colour source
   only — see ghost.js) for the REAL, always-visible .stat-bg below.

   MUST STAY A FLAT SOLID. ghost.js tweens backgroundColor as a plain
   colour; hand it a gradient and the tween reads it as transparent and the
   card disappears mid-scroll.

   A two-tone sheen used to ride along on .stat-bg::after. It was removed
   when the sidebar was unified: anything on the REAL element paints at
   BOTH scales, so the sheen that gave this card a second tone in the hero
   was also the reason the stat panel didn't match every other panel in the
   sidebar. It's the hero tint alone now.

   Ink-tinted, not white: white-on-white was why the numbers washed out.
   Only the HERO end darkens — .stat-bg keeps its light value, so the card
   still resolves to a pale chip once it lands in the sidebar. */
.hero-card-bg{
  position:absolute;
  inset:0;
  z-index:-1;
  border-radius:14px;
  /* Held equal to .stat-bg's resting value (see the contrast note on
     .nav-panel-bg). Equal ends are what make ghost.js skip the colour
     tween entirely, so the card keeps one tone the whole way from his
     torso into the sidebar. */
  background:rgba(30,26,23,.62);
  border:1px solid rgba(255,255,255,.26);
}
/* Gold, matching the reference's yellow figures. ghost.js only tweens
   colour when the two ends differ (ghost.js:179-182) — .stat-num is gold
   too, so this pair now tweens size/position only, no colour crossfade. */
.hero-card-num{
  color:var(--gold);
  font-size:clamp(34px,3.6vw,52px);
  font-weight:900;
  letter-spacing:-.045em;
  line-height:1;
  white-space:nowrap;
}
/* Ghost TARGET for .stat-ico — invisible, measured only.

   position:absolute is the whole point: the two hero cards sit at frozen
   offsets 18px apart, and .hero-card is sized by min-height, so an icon in
   normal flow would grow the first card straight into the second. Out of
   flow it sits in the card's empty upper corner and changes nothing.

   Gold at both ends, like the figures — ghost.js only tweens colour when
   the two ends differ, so this pair moves and scales without a crossfade.

   Top-RIGHT, not top-left above the figure the way the sidebar stacks it.
   The clear band above the figure depends on viewport HEIGHT (the card is
   min-height:14.7vh) while the figure itself is sized by viewport WIDTH,
   so on a short wide window — 1902x760 — that band goes negative and an
   icon placed there lands on top of the "50+". Measured at three sizes:
   17px of clearance at 1902x910, 1.6px at 1280x800, and −13px at
   1902x760.

   The right-hand side has no such dependency. The figures are short and
   left-aligned ("50+" is ~78px of a 132px content box at the widest), so
   the top-right corner stays clear at every size the card can take. The
   icon crossing the card diagonally as it collapses is invisible amongst
   everything else in flight. */
.hero-card-ico{
  position:absolute;
  top:clamp(16px,1.7vw,24px);
  right:clamp(16px,1.7vw,24px);
  /* 1.35vw/20px cap, down from 1.75vw/25px. Between roughly 1280 and 1440
     the card width has already hit its 180px ceiling while the figure is
     still growing with the viewport, and at 25px the icon closed to 0.5px
     of the "50+" — touching it at 1280x800. This holds ≥4.5px of clearance
     across every size tested from 1100x700 to 1902x910. */
  width:clamp(16px,1.35vw,20px);
  height:clamp(16px,1.35vw,20px);
  color:var(--gold);
}
.hero-card-label{
  color:#fff;
  font-size:clamp(9px,.8vw,12px);
  font-weight:800;
  letter-spacing:.14em;
  text-transform:uppercase;
  line-height:1;
  white-space:nowrap;
}

/* ══════════════════════════════════════════════════════════════════════
   SECTIONS
   ══════════════════════════════════════════════════════════════════════ */

.section{padding:clamp(80px,11vh,140px) var(--gutter)}

.section-eyebrow{
  font-size:10px;
  font-weight:800;
  letter-spacing:.22em;
  text-transform:uppercase;
  opacity:.4;
  margin-bottom:18px;
}

.section-head{margin-bottom:clamp(38px,6vh,64px);max-width:62ch}

.section-title{
  font-size:clamp(32px,4.4vw,64px);
  font-weight:900;
  letter-spacing:-.04em;
  line-height:.98;
}

.section-lead{
  margin-top:18px;
  max-width:46ch;
  font-size:15px;
  line-height:1.55;
  opacity:.62;
}

/* ---- Trusted by ---------------------------------------------------- */

.brands{
  padding:clamp(50px,7vh,84px) 0;
  border-top:1px solid rgba(30,26,23,.12);
  border-bottom:1px solid rgba(30,26,23,.12);
  overflow:hidden;
}
.brands .section-eyebrow{padding:0 var(--gutter);margin-bottom:26px}

.marquee{overflow:hidden;padding:5px 0}
.marquee-track{
  display:flex;
  gap:44px;
  width:max-content;
  will-change:transform;
}
/* .34 measured at 2.08:1 against the cream background — well under the
   3:1 WCAG AA floor for text this size (19-30px at weight 800 clears the
   "large text" threshold, so 3:1 is the bar, not 4.5:1). .58 measures
   ~4:1, comfortably past it with room for a slightly-off monitor, while
   still reading as a muted wall rather than full-ink shouting; hover
   already goes to opacity:1 as the "engaged" state, so the rest state
   only needed to become READABLE, not become the loudest thing in the
   section. Computed via the actual WCAG relative-luminance formula
   against blended ink-on-cream, not eyeballed. */
.brand{
  display:inline-flex;
  align-items:center;
  gap:.32em;
  font-size:clamp(19px,2.1vw,30px);
  font-weight:800;
  letter-spacing:-.03em;
  white-space:nowrap;
  opacity:.58;
  transition:opacity .3s var(--ease);
}
/* Same material as the sidebar's .nav-client-mark (grayscale + a contrast
   nudge so mismatched brand palettes read as one row of marks, not a
   confetti of logo colors) — sized up for this section's much larger
   type instead of inheriting the sidebar's fixed 22px. Only the 15 names
   with a real file in assets/img/brands/ carry this tag; the rest stay
   plain text until art exists for them. */
.brand-mark{
  flex:0 0 auto;
  width:clamp(24px,2.4vw,34px);
  height:clamp(24px,2.4vw,34px);
  border-radius:22%;
  object-fit:contain;
  background:var(--ink);
  filter:grayscale(1) contrast(1.12);
  transition:filter .3s var(--ease);
}
/* Both the opacity lift and the color reveal are gated behind real hover
   capability — matchMedia, not a viewport-width guess, matching js/work.js's
   own reasoning: pointer:fine is "can this input hover?", which a resized
   desktop window answers yes to and a touchscreen doesn't, regardless of
   width. Raw CSS :hover has no such gate on its own, and touch synthesizes
   a hover+active+click sequence on tap that STICKS until the visitor taps
   elsewhere on the page. Undetectable during a routine scroll-past on a
   real phone, since the marquee never stops moving — but a stray tap while
   scrolling would otherwise leave one mark stuck in full color, alone,
   against an otherwise-grayscale row until something else on the page was
   touched. Scoped to the row's own hover state so mobile behaves exactly
   as it did before this feature existed. */
@media (hover:hover) and (pointer:fine){
  .brand:hover{opacity:1}
  /* The row stays grayscale at rest for the reason above — mismatched brand
     palettes reading as one row, not a confetti of logo colors — but a
     single mark under the cursor is no longer "the row", so its own color
     is safe to reveal as the hover's answer to "who is this". */
  .brand:hover .brand-mark{filter:grayscale(0) contrast(1)}
}

/* ---- Stats band ---------------------------------------------------- */

.stats-band{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:clamp(20px,3vw,48px);
  padding:clamp(70px,10vh,120px) var(--gutter);
}
.stat-big{display:flex;flex-direction:column;gap:8px}
/* tabular-nums is load-bearing here, not a nicety: motion.js rolls a 0-9
   column through each digit window, so proportional digits would make the
   figure's width jump on every frame of the roll. */
.stat-big-num{
  display:flex;
  align-items:flex-start;
  font-size:clamp(52px,7.4vw,110px);
  font-weight:900;
  letter-spacing:-.055em;
  /* line-height 1, not the .9 this used to carry. Each digit now sits in a
     window exactly one line-box tall WITH overflow hidden, and at .9 the
     glyphs are taller than their own line box — which was invisible before
     (nothing clipped them) but would shave the tops and bottoms off every
     number now. */
  line-height:1;
  font-variant-numeric:tabular-nums;
}
/* One digit's window. Height comes from the cell inside it. */
.digit-mask{
  display:block;
  overflow:hidden;
  height:1em;
}
.digit-track{
  display:flex;
  flex-direction:column;
  will-change:transform;
}
.digit-cell{
  display:block;
  height:1em;
  line-height:1;
}
/* Static — only the digits roll. */
.stat-suffix{display:block;line-height:1}
.stat-big-label{
  font-size:11px;
  font-weight:800;
  letter-spacing:.17em;
  text-transform:uppercase;
  opacity:.5;
}

/* ---- The work ------------------------------------------------------ */

/* DEFAULT = the vertical grid, and that is deliberate. The horizontal
   gallery below only switches on when js/motion.js has actually built the
   pinned scroll and put .is-hscroll on <html>. Gating it on a media query
   instead would strand the layout in every case where the pin ISN'T built
   — reduced-motion, a GSAP CDN failure — leaving a max-content track
   clipped by its viewport with three cards permanently unreachable. */
.work-track{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:14px;
}

/* ---- Horizontal gallery (js/motion.js adds .is-hscroll) ------------- */

/* The section becomes exactly one viewport tall and hands all its leftover
   height to the viewport, so the cards size themselves to whatever is left
   after the heading — rather than a hardcoded card height that only suits
   one window. A pinned section taller than 100vh would have its bottom cut
   off for the whole scroll. */
.is-hscroll .work{
  min-height:100vh;
  display:flex;
  flex-direction:column;
  padding-top:clamp(60px,8vh,100px);
  padding-bottom:clamp(40px,6vh,80px);
}
.is-hscroll .work-viewport{
  flex:1 1 auto;
  /* min-height:0 lets a flex child shrink below its content size; without
     it the track's card height wins and the section grows past 100vh. */
  min-height:0;
  overflow:hidden;
  /* Cancels .section's horizontal padding on the right so the strip runs
     off the edge of the screen instead of stopping short of it. */
  margin-right:calc(var(--gutter) * -1);
}
.is-hscroll .work-track{
  display:flex;
  width:max-content;
  height:100%;
  gap:18px;
  grid-template-columns:none;
}
.is-hscroll .work-card{
  flex:0 0 auto;
  /* Two cards fill a 1902 window with the third just breaking the edge —
     the peek is what signals there's more to scroll to. */
  width:clamp(300px,46vw,760px);
  height:100%;
  min-height:0;
}
/* The lift reads as a glitch mid-travel — the card is already moving. */
.is-hscroll .work-card:hover{transform:none}
/* Three lines of reserved description height exist to keep four titles
   aligned across a ROW; in the gallery each card is its own column and the
   reserve just pushes the copy up the card. */
.is-hscroll .work-desc{min-height:0}
.work-card{
  position:relative;
  display:flex;
  flex-direction:column;
  min-height:290px;
  padding:22px;
  border-radius:18px;
  background:rgba(30,26,23,.05);
  border:1px solid rgba(30,26,23,.09);
  transition:background .35s var(--ease),transform .35s var(--ease);
  /* .work-card is a <button> (opens the archive), not a link — undo the
     UA button defaults so it still reads as the same card. */
  appearance:none;
  width:100%;
  font:inherit;
  color:inherit;
  text-align:left;
  cursor:pointer;
}
.work-card:hover{background:var(--gold);transform:translateY(-6px)}
.work-index{
  font-size:10px;
  font-weight:800;
  letter-spacing:.2em;
  opacity:.4;
}
.work-name{
  margin-top:auto;
  font-size:clamp(26px,2.6vw,38px);
  font-weight:900;
  letter-spacing:-.04em;
  line-height:1;
  margin-bottom:10px;
}
/* Reserve three lines so the four card titles sit on one line across the
   row regardless of how long each description runs. */
.work-desc{font-size:13px;line-height:1.5;opacity:.62;max-width:32ch;min-height:calc(3 * 1.5em)}
.work-go{
  margin-top:18px;
  font-size:11px;
  font-weight:800;
  letter-spacing:.14em;
  text-transform:uppercase;
  opacity:.5;
}
.work-card:hover .work-go,
.work-card:hover .work-desc{opacity:.8}

/* ---- Work cards carrying footage -----------------------------------
   These invert to dark-on-light because text has to stay legible over
   video. The gold hover fill is dropped for them (it would just be hidden
   behind the clip); the hover signal becomes the clip starting to play
   plus a slow push-in, with gold moved onto the "View on X" label so the
   brand accent survives.

   The Interviews card carries .has-media WITHOUT a .work-media layer — no
   footage was supplied for it. It takes the dark treatment anyway so the
   four cards read as one set. */
.work-card.has-media{
  overflow:hidden;
  isolation:isolate;
  background:var(--ink);
  border-color:rgba(30,26,23,.25);
  color:var(--cream);
}
/* Beats the shared .work-card:hover gold fill above. */
.work-card.has-media:hover{background:var(--ink)}

/* Every direct child EXCEPT the media layer is lifted above it. The media
   layer itself stays at the default z-index so it can't cover the text. */
.work-card.has-media > :not(.work-media){position:relative;z-index:1}

.work-media{
  position:absolute;
  inset:0;
  overflow:hidden;
  border-radius:inherit;
}
.work-video{
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
  /* Held under 1 so the poster/clip reads as a ground for the type rather
     than competing with it; lifts on hover as the clip starts. */
  opacity:.55;
  transform:scale(1.03);
  transition:opacity .45s var(--ease),transform .7s var(--ease);
}
.work-card.has-media:hover .work-video{opacity:.78;transform:scale(1.09)}

/* Bottom-weighted scrim: the card's type stacks toward the bottom
   (.work-name carries margin-top:auto), so that end needs the most cover.
   Sits above .work-video as a later sibling in paint order. */
.work-media::after{
  content:'';
  position:absolute;
  inset:0;
  background:linear-gradient(180deg,
    rgba(30,26,23,.30) 0%,
    rgba(30,26,23,.55) 45%,
    rgba(30,26,23,.90) 100%);
}

/* Opacities re-tuned for light-on-dark — the values above were picked for
   ink on cream and read as muddy when inverted. */
.work-card.has-media .work-index{opacity:.6}
.work-card.has-media .work-desc{opacity:.78}
.work-card.has-media .work-go{opacity:1;color:var(--gold)}
.work-card.has-media:hover .work-desc{opacity:.92}

@media (prefers-reduced-motion:reduce){
  .work-video{transition:none;transform:none}
  .work-card.has-media:hover .work-video{transform:none}
}

/* ---- Work archive ---------------------------------------------------
   Same fixed-overlay/scrim/panel bones as .pw (the partner dialog) —
   see that block's comments for why a second modal shares the
   contract instead of inventing its own. This one is wider, since a
   video grid needs the room a form doesn't. ---------------------- */
.wa{
  position:fixed;
  inset:0;
  z-index:200;
  display:none;
  align-items:center;
  justify-content:center;
  padding:24px;
}
.wa.is-open{display:flex}
.wa-scrim{
  position:absolute;
  inset:0;
  background:rgba(20,17,15,.6);
  backdrop-filter:blur(4px);
  -webkit-backdrop-filter:blur(4px);
}
.wa-panel{
  position:relative;
  width:min(1040px,100%);
  /* Two declarations, not one. 100vh on iOS Safari is the viewport with
     the URL bar RETRACTED, which is taller than what you can actually
     see while it is showing — so the panel's last row sat under the
     browser chrome with nothing to scroll it into view. dvh is the live
     value; the vh line stays as the fallback for anything that doesn't
     know the unit and would otherwise get no max-height at all. */
  max-height:calc(100vh - 48px);
  max-height:calc(100dvh - 48px);
  overflow-y:auto;
  padding:clamp(28px,3.4vw,44px);
  border-radius:22px;
  background:var(--ink);
  color:var(--cream);
  box-shadow:0 50px 120px -30px rgba(0,0,0,.7);
}
.wa-close{
  position:absolute;
  top:18px;right:18px;
  width:34px;height:34px;
  display:grid;place-items:center;
  border:none;border-radius:10px;
  background:rgba(240,230,220,.14);
  color:var(--cream);
  font-family:inherit;
  font-size:22px;line-height:1;
  cursor:pointer;
  transition:background-color .2s var(--ease),color .2s var(--ease);
}
.wa-close:hover{background:var(--gold);color:var(--ink)}
.wa-close:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
.wa-head{margin:0 44px 24px 0}
.wa-count{
  display:block;
  font-size:10px;
  font-weight:800;
  letter-spacing:.22em;
  text-transform:uppercase;
  opacity:.5;
  margin-bottom:8px;
}
.wa-title{
  font-size:clamp(26px,3vw,38px);
  font-weight:900;
  line-height:1;
  letter-spacing:-.03em;
  margin-bottom:10px;
}
.wa-sub{font-size:14px;line-height:1.5;opacity:.7;max-width:60ch}
/* One uniform square grid. The stills are a real mix of 16:9 and 9:16
   (UGC alone runs 8 landscape to 6 portrait), so a square tile does crop
   them — accepted deliberately: the grid is an index, and the tile links
   out to the full video on X. object-position:center keeps the subject,
   who is centred in essentially all of this footage. */
.wa-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(160px,1fr));
  gap:14px;
  align-items:start;
}
.wa-item{
  display:flex;
  flex-direction:column;
  color:inherit;
  text-decoration:none;
}
.wa-thumb{
  position:relative;
  display:block;
  aspect-ratio:1;
  border-radius:12px;
  overflow:hidden;
  background:rgba(240,230,220,.06);
}
.wa-thumb-img{
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:center;
  display:block;
  transition:transform .4s var(--ease);
}
.wa-play{
  position:absolute;
  inset:0;
  display:grid;place-items:center;
  background:rgba(20,17,15,.28);
  opacity:0;
  transition:opacity .25s var(--ease);
}
.wa-play svg{
  width:34px;height:34px;
  fill:var(--cream);
  filter:drop-shadow(0 2px 8px rgba(0,0,0,.5));
}
.wa-item:hover .wa-thumb-img{transform:scale(1.06)}
.wa-item:hover .wa-play{opacity:1}
/* Clamped to two lines so every tile is the same height — these captions
   are real tweet text and run anywhere from three words to a full
   sentence, which otherwise leaves the rows ragged. */
.wa-caption{
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
  margin-top:8px;
  min-height:calc(2 * 1.4em);
  font-size:12.5px;
  line-height:1.4;
  opacity:.68;
  transition:opacity .2s var(--ease);
}
.wa-item:hover .wa-caption{opacity:.95}

@media (max-width:900px), (max-height:560px) and (pointer:coarse){
  .wa-grid{grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}
  .wa-panel{padding:24px}
}

/* ---- Platforms ----------------------------------------------------- */

.plat-list{border-top:1px solid rgba(30,26,23,.14)}
.plat{
  display:grid;
  /* The note column is a FIXED 130px, not `auto`. Every .plat is its own
     grid container, so an auto-sized note is measured per row — and since
     the notes differ ("Shorts & stills" 116px vs "Live" 30px), each row
     handed a different amount of leftover width to the two 1fr columns.
     The handles landed anywhere from x=710 to x=753, a 43px rag that
     tracked note width exactly. A fixed track makes all six rows share
     identical geometry. 130px clears the longest current label with room
     to spare; .plat-note is right-aligned inside it so its right edge
     still sits flush against the arrow. */
  grid-template-columns:48px 1fr 1fr 130px 24px;
  align-items:center;
  gap:20px;
  padding:18px 14px;
  margin:0 -14px;
  border-radius:14px;
  border-bottom:1px solid rgba(30,26,23,.14);
  /* background/color-only additions alongside padding-left. Still no
     transform or opacity here — those are exactly the properties the
     data-tl reveal on this row writes inline every frame, and racing a
     CSS transition against that produces the lag the comment used to warn
     about (see .btn for the canonical writeup of that conflict). */
  transition:padding-left .35s var(--ease),background .3s var(--ease);
}
.plat:last-child{border-bottom:none}
.plat-ico{
  width:48px;height:48px;
  display:flex;align-items:center;justify-content:center;
  border-radius:12px;
  background:rgba(30,26,23,.06);
  color:var(--ink);
  transition:background .3s var(--ease),color .3s var(--ease);
}
/* FILLED, same as .nav-ico — these are real brand marks (see the comment
   above .plat-list in index.html), and every one of them is authored as a
   solid silhouette with detail knocked out via fill-rule rather than drawn
   as a line. Stroking them, which this used to do, rendered the X as an
   outlined double-chevron and Abstract's mark as three hollow slivers. */
.plat-ico svg{
  width:20px;height:20px;
  fill:currentColor;
  stroke:none;
}
.plat-name{
  font-size:clamp(20px,2.2vw,32px);
  font-weight:900;
  letter-spacing:-.035em;
}
.plat-handle{font-size:14px;font-weight:700;opacity:.55;transition:opacity .3s var(--ease)}
.plat-note{
  font-size:10px;
  font-weight:800;
  letter-spacing:.16em;
  text-transform:uppercase;
  text-align:right;
  opacity:.4;
  transition:opacity .3s var(--ease);
}
.plat-arrow{
  width:24px;height:24px;
  display:flex;align-items:center;justify-content:center;
  color:var(--ink);
  opacity:0;
  transform:translate(-6px,6px);
  transition:opacity .3s var(--ease),transform .3s var(--ease);
}
.plat-arrow svg{
  width:17px;height:17px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.75;
  stroke-linecap:round;
  stroke-linejoin:round;
}

/* Every Platforms hover behaviour, gathered into one hover-gated block
   rather than left scattered through the rules above.

   Gated because this row's hover is the loudest on the page — the whole
   row turns GOLD and slides 20px right. On a touchscreen, tapping a row
   synthesises hover and it STICKS until the visitor taps elsewhere, so an
   iPad user tapping through to a platform would come back to the page and
   find one row still lit gold with an arrow out, looking selected. Same
   class of bug the brand marks had; same `(hover:hover) and (pointer:fine)`
   gate work.js established.

   The rows stay real links, so tapping still navigates — only the
   decoration is withheld from inputs that cannot un-hover. */
@media (hover:hover) and (pointer:fine){
  .plat:hover{padding-left:20px;background:var(--gold)}
  .plat:hover .plat-ico{background:var(--ink);color:var(--gold)}
  .plat:hover .plat-handle{opacity:.7}
  .plat:hover .plat-note{opacity:.65}
  .plat:hover .plat-arrow{opacity:1;transform:translate(0,0)}
}

/* ---- Testimonials -------------------------------------------------- */

/* A fanned hand rather than a grid: each .quote-reveal is a flex item that
   overlaps its neighbour (negative margin) and carries the constant fan
   values — rotation, vertical lift, stacking order — as custom properties
   set inline per card in the HTML, since there's a fixed set of 5 and no
   generic count to derive them from.

   The rotation/lift/hover-pop transform lives on the INNER .quote, never on
   .quote-reveal itself, because .quote-reveal is the element the data-tl
   reveal tween writes an inline `transform` to (translateY 34->0 on
   scroll-in). GSAP's inline style always wins over CSS — the exact trap
   documented on .plat above and .btn elsewhere — so a fan transform on the
   same element would be wiped the instant the reveal fires. Splitting the
   reveal (outer) from the fan+hover (inner) keeps both transforms live at
   once with no fight. */
.quote-hand{
  display:flex;
  justify-content:center;
  align-items:flex-end;
  flex-wrap:wrap;
  padding:44px 0 64px;
}
.quote-reveal{
  flex:0 0 auto;
  width:250px;
  margin-left:var(--overlap,0);
  position:relative;
  z-index:var(--z,1);
}
/* Same ink-tinted frosted glass as .hero-traits / .stat-bg / .hero-card-bg
   — one material, flat rgba(30,26,23,.62) with a white glass rim and inset
   top-shine, rather than a bespoke flat-cream card. White text follows,
   same as every other panel built on this formula. */
.quote{
  display:flex;
  flex-direction:column;
  justify-content:space-between;
  gap:20px;
  /* 310, not 270: Alisha's quote (the longest of the five) needs 186px of
     text height, and 270 gave it 171 — the last line was clipped by the
     card's own overflow at every state, hover included. 310 = 186 quote +
     44 caption + 48 padding + 20 gap, plus a little slack for a webfont
     landing slightly wider than the fallback. */
  height:310px;
  padding:24px;
  border-radius:18px;
  background:rgba(30,26,23,.62);
  border:1px solid rgba(255,255,255,.26);
  box-shadow:0 10px 30px rgba(30,26,23,.35), inset 0 1px 0 rgba(255,255,255,.22);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  color:#fff;
  transform:rotate(var(--rot,0deg)) translateY(var(--lift,0));
  transform-origin:bottom center;
  transition:transform .45s var(--ease),box-shadow .45s var(--ease);
}
/* Hover-gated for the same reason as .plat and the brand marks: this is
   the biggest movement on the page — the card lifts 28px, scales 7% and
   straightens out of the fan. A touch device synthesises hover on tap and
   never releases it, so one card would stay standing out of the hand,
   un-rotated, until the visitor tapped something else. The fan is
   decorative and non-interactive, so withholding it on touch costs
   nothing at all there.

   z-index moves inside the gate too: it exists only to lift the hovered
   card above its neighbours, and a stuck z-index:20 on touch would leave
   one card permanently overlapping the two beside it. */
@media (hover:hover) and (pointer:fine){
  .quote-reveal:hover{z-index:20}
  .quote-reveal:hover .quote{
    transform:translateY(-28px) scale(1.07) rotate(0deg);
    box-shadow:0 34px 64px rgba(30,26,23,.45), inset 0 1px 0 rgba(255,255,255,.3);
  }
}
.quote blockquote{font-size:14px;line-height:1.48;letter-spacing:-.01em;overflow:hidden}
.quote blockquote::before{content:'“'}
.quote blockquote::after{content:'”'}
.quote figcaption{display:flex;flex-direction:column;gap:3px}
.quote figcaption strong{font-size:14px;font-weight:800}
.quote figcaption span{
  font-size:10px;
  font-weight:800;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:rgba(255,255,255,.55);
}

/* ---- About / Journey timeline -------------------------------------- */

/* cards slide in from beyond the section edge — clip so that off-screen
   travel never adds horizontal page overflow / a stray scrollbar. */
.about{overflow-x:clip}


/* --- the timeline column (no heading/intro — starts right after hero) --- */
/* 1400, up from 1000. At the owner's 1920 the section's content band is
   1569px wide, so a 1000px cap left 284px of dead margin on each side and
   the cards read as a narrow centred column. The reference runs its cards
   out to both extremes of its band, which is also what gives its connector
   a full-width traversal to make between every pair. */
.about-timeline{
  position:relative;
  max-width:1400px;
  margin:0 auto;
  padding-top:clamp(10px,3vh,40px);
  /* Open canvas below the last card for the connector's run-out and its
     trailing dashes to live in. Without it they paint past the timeline's
     own box and collide with the Trusted By band. */
  padding-bottom:clamp(96px,13vh,150px);
}
.about-path{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  z-index:0;
  pointer-events:none;
  overflow:visible;
}
.about-path-line,
.about-path-tail{
  stroke-width:2;
  stroke-linecap:round;
  fill:none;
  vector-effect:non-scaling-stroke;
}
/* The trailing dashes past the final dot — the reference's "story isn't
   over" cue. It cannot live on .about-path-line: that element's
   stroke-dasharray is the draw animation, so a decorative dash pattern
   there would fight it. Separate path, own dash pattern, revealed by class
   once the main line has finished drawing (a transition rather than a
   per-frame opacity write). */
.about-path-tail{
  /* Fallback only — about.js overwrites this inline, solving the pattern
     against the tail's measured length so it lands exactly three dashes. */
  stroke-dasharray:24 22;
  opacity:0;
  transition:opacity .5s var(--ease);
}
.about-path-tail.is-on{opacity:1}
/* round dot beside each card, positioned by about.js */
.about-dot{
  position:absolute;
  z-index:2;
  width:13px;height:13px;
  margin:-6.5px 0 0 -6.5px;          /* center on its point */
  border-radius:50%;
  background:var(--gold);
  border:1.5px solid var(--ink);
  opacity:0;
  transform:scale(.2);
  transition:opacity .3s var(--ease), transform .3s var(--ease);
  pointer-events:none;
}
.about-dot.is-lit{opacity:1;transform:scale(1)}

/* Thin rule up each card's outer edge, ending a little above that card's
   dot — the reference's card marker, and the detail that makes the connector
   read as deliberate rather than as a line that happens to pass by. Faded at
   both ends so it doesn't stop on a hard edge.

   A real element positioned by about.js, NOT an .about-card::before: the card
   carries a backdrop-filter, which gave the pseudo-element its own backdrop
   root and stopped it painting outside the card's box at all. The dots
   already prove this layer works at these coordinates. */
/* .42, not .26. The band is centred with a half-pixel offset at most widths,
   so a 1px line lands across two device pixels and anti-aliases to roughly
   half its alpha — at .26 that resolved to about .13 and was invisible on
   cream. Confirmed by painting it red as a diagnostic: the box was always
   there, it just had no contrast. Still lighter than the connector's .7. */
.about-rule{
  position:absolute;
  z-index:2;
  width:1px;
  background:linear-gradient(180deg,
    rgba(30,26,23,0), rgba(30,26,23,.42) 22%,
    rgba(30,26,23,.42) 78%, rgba(30,26,23,0));
  pointer-events:none;
}

/* --- cards --- */
/* z-index:1 against the connector's 0 is load-bearing, not tidiness: the
   line now traverses the full width between dots, so it passes BEHIND each
   card and emerges at the outer corner — which is exactly how the
   reference gets its long uninterrupted sweeps. */
.about-card{
  position:relative;
  z-index:1;
  /* 45% of a 1400px band (was 52% of 1000) keeps the card's own text
     measure close to what it was while pushing the pair out to both
     extremes of the wider band. */
  width:min(620px,45%);
  padding:clamp(24px,2.4vw,36px);
  border-radius:22px;
  /* .86, up from .5, and a heavier blur. Required by the routing above: at
     .5 the connector was clearly legible THROUGH the card as a stray
     diagonal scratch, where the reference has the card occlude it and the
     line simply reappear at the next dot. High enough to swallow a .7-ink
     hairline, low enough that the ambient silhouette still reads as a faint
     shape rather than being erased. */
  background:rgba(255,255,255,.86);
  border:1px solid rgba(30,26,23,.08);
  box-shadow:0 24px 60px -34px rgba(30,26,23,.5);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
}
.about-card.is-left{margin-right:auto}
.about-card.is-right{margin-left:auto}
.about-card + .about-card{margin-top:clamp(32px,5vh,72px)}
@media (max-width:1200px){
  .about-card + .about-card{margin-top:clamp(28px,4vh,52px)}
}

.about-card-year{
  font-weight:900;
  font-size:clamp(52px,6vw,84px);
  line-height:.9;
  letter-spacing:-.02em;
  color:var(--gold);
  margin-bottom:14px;
}
.about-card-title{
  font-size:clamp(20px,1.9vw,26px);
  font-weight:800;
  letter-spacing:-.02em;
  margin-bottom:12px;
}
.about-card-blurb{
  font-size:clamp(14px,1.15vw,16px);
  line-height:1.55;
  opacity:.74;
  margin-bottom:24px;
  max-width:44ch;
}
.about-card-meta{
  display:flex;
  align-items:center;
  gap:12px;
}
.about-chip{
  flex:none;
  width:40px;height:40px;
  display:grid;place-items:center;
  border-radius:50%;
  background:var(--ink);
  color:var(--gold);
  text-decoration:none;
  transition:transform .2s var(--ease);
}
/* The PFP fills the chip edge to edge — the ink disc behind it only shows
   if the image fails, so it stays as the fallback ground. */
.about-chip{overflow:hidden}
.about-chip img{
  width:100%;height:100%;
  object-fit:cover;
  display:block;
}
/* Wraps the chip + handle as one link to Merheb's X profile — flex:1
   takes over the job .about-handle's margin-right:auto used to do,
   pushing .about-readmore to the row's far end. */
.about-attrib{
  display:flex;
  align-items:center;
  gap:12px;
  flex:1;
  min-width:0;
  color:inherit;
  text-decoration:none;
}
.about-attrib:hover .about-chip{transform:scale(1.08)}
.about-attrib:hover .about-handle{opacity:1}
.about-handle{
  font-size:13px;
  opacity:.6;
  transition:opacity .2s var(--ease);
}
.about-readmore{
  flex:none;
  padding:9px 18px;
  border:1px solid rgba(30,26,23,.18);
  border-radius:999px;
  background:transparent;
  color:var(--ink);
  font-size:13px;font-weight:700;
  cursor:pointer;
  transition:background-color .2s var(--ease), color .2s var(--ease);
}
.about-readmore:hover{background:var(--ink);color:var(--cream)}

/* --- expanded overlay (about.js clones a card's <template> into this) --- */
.about-overlay{
  position:fixed;
  z-index:120;
  border-radius:24px;
  background:var(--ink);
  color:var(--cream);
  padding:clamp(30px,3vw,48px);
  overflow:auto;
  box-shadow:0 50px 120px -30px rgba(0,0,0,.7);
  will-change:top,left,width,height;
}
.about-overlay-scrim{
  position:fixed;
  inset:0;
  z-index:119;
  background:rgba(20,17,15,.55);
  backdrop-filter:blur(3px);
  -webkit-backdrop-filter:blur(3px);
  opacity:0;
}
.about-overlay-close{
  position:absolute;
  top:18px;right:18px;
  width:34px;height:34px;
  display:grid;place-items:center;
  border:none;border-radius:10px;
  background:rgba(240,230,220,.14);
  color:var(--cream);
  font-size:22px;line-height:1;
  cursor:pointer;
  transition:background-color .2s var(--ease), color .2s var(--ease);
}
.about-overlay-close:hover{background:var(--gold);color:var(--ink)}
.about-full-year{
  font-weight:900;
  font-size:clamp(64px,8vw,120px);
  line-height:.85;
  letter-spacing:-.02em;
  color:var(--gold);
  margin-bottom:18px;
}
.about-full-title{
  font-size:clamp(24px,2.6vw,38px);
  font-weight:800;
  letter-spacing:-.02em;
  margin-bottom:18px;
}
.about-overlay p{
  font-size:clamp(15px,1.3vw,18px);
  line-height:1.65;
  opacity:.82;
  max-width:52ch;
}
/* The closing line each card ends on when expanded. The collapsed card
   carries the main text only; this is the one thing opening it adds. */
.about-full-tagline{
  margin-top:18px;
  color:var(--gold);
  font-weight:700;
  font-style:italic;
  opacity:1;
}
.about-full-stats{
  display:flex;
  flex-wrap:wrap;
  gap:clamp(20px,3vw,44px);
  list-style:none;
  margin:28px 0;
}
.about-full-stats li{font-size:14px;opacity:.7}
.about-full-stats b{
  display:block;
  font-size:clamp(30px,3.4vw,46px);
  font-weight:900;
  color:var(--gold);
  opacity:1;
  letter-spacing:-.02em;
}
.about-full-cta{
  display:inline-block;
  margin-top:8px;
  padding:14px 30px;
  border-radius:12px;
  background:var(--gold);
  color:var(--ink);
  font-weight:800;
  text-decoration:none;
  transition:transform .2s var(--ease);
}
.about-full-cta:hover{transform:translateY(-2px)}

@media (prefers-reduced-motion:reduce){
  .about-dot{opacity:1;transform:scale(1);transition:none}
}

/* ---- Contact ------------------------------------------------------- */

/* ---- FAQ ------------------------------------------------------------
   Native <details>. The height transition uses grid-template-rows 0fr→1fr
   rather than max-height: transitioning `height:auto` still isn't possible,
   and a fixed max-height would clip a long answer or leave dead space under
   a short one. The grid row resolves to the answer's real height, so every
   item animates to exactly its own size.

   <details> only removes [open] AFTER a close, so the closing direction
   can't be transitioned by CSS alone — accepted: opening is the motion
   people notice, closing reads fine as an instant collapse. */
/* Two columns of three. The rule that used to top the whole list moves to
   each column, so both start on a line — a single top border on the grid
   would only draw above the first column's first item. */
.faq-list{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:0 clamp(32px,4.5vw,72px);
  align-items:start;
  max-width:1240px;
}
.faq-col{border-top:1px solid rgba(30,26,23,.14)}
.faq-item{
  position:relative;
  padding-left:22px;
  border-bottom:1px solid rgba(30,26,23,.14);
}
/* Gold accent that grows in from the top on hover/open — the same "this
   one's active" cue as the nav's active pill, just borrowed for a list
   rather than a menu. transform, not height, so it doesn't nudge layout. */
.faq-item::before{
  content:"";
  position:absolute;
  left:0; top:22px; bottom:22px;
  width:3px;
  border-radius:2px;
  background:var(--gold);
  transform:scaleY(0);
  transform-origin:top;
  transition:transform .4s var(--ease);
}
.faq-item:hover::before,
.faq-item[open]::before{transform:scaleY(1)}
.faq-item[open]{padding-bottom:4px}

.faq-q{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:20px;
  padding:22px 0;
  font-size:clamp(16px,1.5vw,20px);
  font-weight:800;
  letter-spacing:-.02em;
  line-height:1.3;
  cursor:pointer;
  list-style:none;
  transition:color .2s var(--ease);
}
/* Safari still paints its own disclosure triangle without this. */
.faq-q::-webkit-details-marker{display:none}
.faq-q:hover{color:rgba(30,26,23,.62)}
.faq-q:focus-visible{outline:2px solid var(--gold);outline-offset:3px;border-radius:4px}

/* A chip, not a bare glyph — same ink/gold invert the sidebar and the
   Platforms rows already use on hover/open, so the FAQ reads as one more
   surface in the same material language rather than a different pattern.
   Two bars forming a +, one of which rotates away to leave a − when open. */
.faq-mark{
  position:relative;
  flex:0 0 auto;
  width:32px;height:32px;
  border-radius:50%;
  background:rgba(30,26,23,.06);
  transition:background .3s var(--ease);
}
.faq-mark::before,
.faq-mark::after{
  content:"";
  position:absolute;
  top:50%;left:50%;
  width:13px;height:1.8px;
  background:var(--ink);
  border-radius:2px;
  transform:translate(-50%,-50%);
  /* A slight overshoot on the turning bar only — the fixed one has nothing
     to spring, so it stays on the plain ease to avoid a mismatched wobble. */
  transition:transform .4s cubic-bezier(.34,1.56,.64,1),background .3s var(--ease);
}
.faq-mark::after{transform:translate(-50%,-50%) rotate(90deg)}
.faq-item[open] .faq-mark::after{transform:translate(-50%,-50%) rotate(0deg)}
.faq-item:hover .faq-mark,
.faq-item[open] .faq-mark{background:var(--ink)}
.faq-item:hover .faq-mark::before,
.faq-item:hover .faq-mark::after,
.faq-item[open] .faq-mark::before,
.faq-item[open] .faq-mark::after{background:var(--gold)}

.faq-a-wrap{
  display:grid;
  grid-template-rows:0fr;
  transition:grid-template-rows .34s var(--ease);
}
.faq-item[open] .faq-a-wrap{grid-template-rows:1fr}
.faq-a{overflow:hidden}
.faq-a p{
  max-width:60ch;
  padding-bottom:24px;
  font-size:15px;
  line-height:1.6;
  opacity:.72;
}

@media (prefers-reduced-motion:reduce){
  .faq-item::before,.faq-a-wrap,.faq-mark,.faq-mark::before,.faq-mark::after{transition:none}
}

.contact{
  border-top:1px solid rgba(30,26,23,.12);
  text-align:left;
}
.contact-title{
  font-size:clamp(42px,7.4vw,110px);
  font-weight:900;
  letter-spacing:-.05em;
  line-height:.92;
}
.contact-note{
  margin-top:22px;
  font-size:11px;
  font-weight:800;
  letter-spacing:.18em;
  text-transform:uppercase;
  opacity:.45;
}
/* ---- Contact — the chat thread -------------------------------------- */

.chat{
  margin-top:clamp(34px,5vh,54px);
  display:flex;
  align-items:flex-start;
  gap:14px;
}

/* The image is already the square crop (head down through both shoulders —
   deliberately not a tight head crop, because the cutout's background is
   transparent and a window stopping at his neck would put the circle's own
   fill under a floating head). So this just sizes it: no offset multipliers
   any more, those existed only to frame the full-body cutout by CSS. */
.chat-avatar{
  --av:58px;
  flex:0 0 auto;
  width:var(--av);
  height:var(--av);
  border-radius:50%;
  overflow:hidden;
  background:rgba(30,26,23,.10);
}
.chat-avatar img{
  width:100%;
  height:100%;
  object-fit:cover;
  display:block;
}

.chat-stream{
  display:flex;
  flex-direction:column;
  align-items:flex-start;
  gap:14px;
}
.chat-slot{display:grid}
/* Both children in the SAME cell: the cell takes the taller one's height
   (the message), so swapping dots for message moves nothing below it. */
.chat-slot > *{grid-area:1/1;justify-self:start;align-self:start;margin:0}

/* The received side of a thread — light chip, ink text, and the one
   squared corner (bottom-left, pointing back at the avatar) that reads as
   a tail without drawing one. */
.chat-bubble,
.chat-typing{
  padding:15px 22px;
  border-radius:22px 22px 22px 6px;
  background:rgba(30,26,23,.07);
  border:1px solid rgba(30,26,23,.07);
}
.chat-bubble{
  font-size:clamp(16px,1.6vw,21px);
  font-weight:700;
  letter-spacing:-.02em;
  line-height:1.25;
}

/* Hidden at rest — the opposite of every other element here. This is a
   transient the timeline brings in and then takes away, so "untouched"
   has to mean "already gone": under reduced motion, where motion.js
   never runs, the thread must read as a finished conversation rather
   than leaving him typing forever. */
.chat-typing{
  display:flex;
  align-items:center;
  gap:5px;
  opacity:0;
  pointer-events:none;
}
.chat-typing span{
  width:7px;height:7px;
  border-radius:50%;
  background:var(--ink);
  opacity:.28;
  animation:chat-dot 1.3s var(--ease) infinite;
}
.chat-typing span:nth-child(2){animation-delay:.16s}
.chat-typing span:nth-child(3){animation-delay:.32s}
@keyframes chat-dot{
  0%,60%,100%{transform:translateY(0);opacity:.28}
  30%{transform:translateY(-5px);opacity:.85}
}

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

/* One is a <button> and one an <a>, so the font stack, border, decoration
   and cursor all have to be said out loud — none of them inherit across
   both element types. */
.chat-send,
.chat-call{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  padding:16px 30px;
  border-radius:100px;
  font-family:inherit;
  font-size:16px;
  font-weight:900;
  letter-spacing:-.01em;
  text-decoration:none;
  cursor:pointer;
  /* NO transform transition. These carry [data-roll-host] labels whose
     inner .roll-line spans are under GSAP control on hover, and their own
     transform is written by motion.js's chatThread entrance — a CSS
     transform transition here would be a third writer on one property.
     The lift is dropped rather than fought for; the roll IS the hover
     feedback, same as the sidebar's Let's Work. */
  transition:box-shadow .3s var(--ease),background-color .25s var(--ease),
             border-color .25s var(--ease);
}
/* Transparent border, NOT `border:none`. The outlined sibling below carries
   a real 2px border, and with none here the two pills measured 46px and
   50px — visibly unequal sitting side by side. Matching the border width
   and hiding it is what keeps one shared box height. */
.chat-send{
  border:2px solid transparent;
  background:var(--gold);
  color:var(--ink);
}
.chat-send:hover{
  background:#ffe45c;
  box-shadow:0 14px 30px rgba(30,26,23,.22);
}
/* Outlined, not filled — the form is the primary ask, the call is the
   alternative. Border is 2px so it holds its weight next to a solid gold
   pill of the same height. */
.chat-call{
  border:2px solid rgba(30,26,23,.28);
  background:none;
  color:var(--ink);
}
.chat-call:hover{
  border-color:var(--ink);
  background:rgba(30,26,23,.05);
}
.chat-send:focus-visible,
.chat-call:focus-visible{outline:2px solid var(--ink);outline-offset:3px}

@media (prefers-reduced-motion:reduce){
  .chat-typing span{animation:none}
  .chat-send,.chat-call{transition:none}
}

/* ---- Partner dialog ------------------------------------------------- */

.pw{
  position:fixed;
  inset:0;
  z-index:200;
  display:none;
  align-items:center;
  justify-content:center;
  padding:24px;
}
.pw.is-open{display:flex}
.pw-scrim{
  position:absolute;
  inset:0;
  background:rgba(20,17,15,.6);
  backdrop-filter:blur(4px);
  -webkit-backdrop-filter:blur(4px);
}
.pw-panel{
  position:relative;
  width:min(620px,100%);
  /* dvh + vh fallback — see .wa-panel for why both. */
  max-height:calc(100vh - 48px);
  max-height:calc(100dvh - 48px);
  overflow-y:auto;
  padding:clamp(28px,3.4vw,44px);
  border-radius:22px;
  background:var(--ink);
  color:var(--cream);
  box-shadow:0 50px 120px -30px rgba(0,0,0,.7);
}
.pw-close{
  position:absolute;
  top:18px;right:18px;
  width:34px;height:34px;
  display:grid;place-items:center;
  border:none;border-radius:10px;
  background:rgba(240,230,220,.14);
  color:var(--cream);
  font-family:inherit;
  font-size:22px;line-height:1;
  cursor:pointer;
  transition:background-color .2s var(--ease),color .2s var(--ease);
}
.pw-close:hover{background:var(--gold);color:var(--ink)}
.pw-title{
  margin:0 44px 24px 0;
  font-size:clamp(28px,3.4vw,44px);
  font-weight:900;
  line-height:1;
  letter-spacing:-.03em;
  text-transform:uppercase;
}
/* Only ever rendered inside <noscript>, replacing the mailto form action
   that used to trip Chrome's mixed-content warning on every load. */
.pw-noscript{
  margin-bottom:18px;
  font-size:14px;
  line-height:1.6;
  opacity:.8;
}
.pw-noscript a{color:var(--gold)}
.pw-form{display:flex;flex-direction:column;gap:10px}

/* Label and input share ONE bordered box, label above input, which is why
   .pw-field is the <label> itself — the whole box is then the input's
   click target with no `for`/`id` pairing to keep in sync. */
.pw-field{
  display:flex;
  flex-direction:column;
  gap:4px;
  padding:12px 16px;
  border:1px solid rgba(240,230,220,.28);
  border-radius:10px;
  cursor:text;
  transition:border-color .2s var(--ease),background-color .2s var(--ease);
}
.pw-field:focus-within{
  border-color:var(--gold);
  background:rgba(240,230,220,.04);
}
.pw-field[data-invalid]{border-color:#ff8f6b}
.pw-label{
  font-size:11px;
  font-weight:800;
  letter-spacing:.1em;
  text-transform:uppercase;
  opacity:.5;
}
.pw-field input,
.pw-field textarea{
  width:100%;
  border:none;
  background:none;
  padding:0;
  color:var(--cream);
  font-family:inherit;
  font-size:15px;
  font-weight:600;
  line-height:1.45;
}
.pw-field input:focus,
.pw-field textarea:focus{outline:none}
.pw-field textarea{resize:vertical;min-height:88px}
.pw-field ::placeholder{color:rgba(240,230,220,.3)}

.pw-submit{
  margin-top:8px;
  padding:16px 20px;
  border:none;
  border-radius:10px;
  background:var(--gold);
  color:var(--ink);
  font-family:inherit;
  font-size:12px;
  font-weight:900;
  letter-spacing:.18em;
  text-transform:uppercase;
  cursor:pointer;
  transition:background-color .25s var(--ease);
}
.pw-submit:hover{background:#ffe45c}
.pw-submit:focus-visible,
.pw-close:focus-visible{outline:2px solid var(--gold);outline-offset:3px}

/* Empty at rest and only filled by partner.js, so it must not reserve a
   line — :empty keeps the gap out of the layout until there's something
   to say. */
.pw-status{
  margin:0;
  font-size:13px;
  line-height:1.5;
  color:var(--gold);
}
.pw-status:empty{display:none}

/* ---- Footer -------------------------------------------------------- */

.footer{
  display:flex;
  justify-content:space-between;
  gap:20px;
  flex-wrap:wrap;
  /* Left padding clears the fixed sidebar. This lives here rather than
     coming from .page because the footer is deliberately OUTSIDE <main>
     (see the landmark comment in index.html) and so never inherited
     .page's padding-left — it spanned the full viewport and ran underneath
     the sidebar, where "…reserved to merheb." showed through in the gap
     beside the floating Let's Work button. Reset at <900px, where the
     sidebar is a normal top bar and nothing is fixed over this. */
  padding:26px var(--gutter) 34px calc(var(--nav-w) + var(--gutter));
  border-top:1px solid rgba(30,26,23,.12);
  font-size:11px;
  font-weight:700;
  opacity:.45;
}
.footer a:hover{opacity:1;text-decoration:underline}

/* ══════════════════════════════════════════════════════════════════════
   SHORT-AND-WIDE DESKTOP WINDOWS — the last fold overflow.

   The hero stack is derived from --mark-bottom, which is
   `6vh + 31vw * .78`. The vw term grows with WIDTH while the space
   available grows with HEIGHT, so a window that is wide relative to its
   height runs the stack past the fold. Measured, worst element first:

     1400x500   card2 +163   traits +143
     1920x700   card2 +105   traits  +90
     1920x800   card2  +20

   `card2` is the second stat card's ghost TARGET. Invisible itself, but
   ghost.js positions the sidebar's real "70+ GUESTS" card onto its rect
   at scroll 0 — so past the fold means the visible card starts off-screen
   and flies up out of nowhere. That is the part worth fixing; .hero-traits
   is the visible decorative panel painting over the next section.

   ── WHY THE SCOPE IS THIS NARROW ──────────────────────────────────────
   `min(31vw, 54vh)` was already tried as a global cap on --mark-size and
   REJECTED: it shrank the wordmark ~17% on the owner's maximised
   1902x910, and both the mark's size and the hero's positions are settled
   (see the --mark-size comment in :root, and CURRENT_STATE.md's frozen-
   positions warning). So this correction may not be global.

   The two conditions together are provably inert everywhere that works
   today. Every desktop geometry below was measured, before and after:

     BROKEN, caught:  1400x500 (2.80)  1920x700 (2.74)  2100x821 (2.56)
                      1920x760 (2.53)  1920x800 (2.40)
     FINE, excluded:  1902x910 (2.09)  2560x1440 (1.78)  1536x864 (1.78)
                      1366x768 (1.78)  1280x720 (1.78)  1024x768 (1.33)
                      1180x820 (1.44)  1366x1024 (1.33) 1920x1080 (1.78)
                      3440x1440 (2.39 — over the ratio cut is close, but
                      1440 is far over the height bound)

   BOTH bounds are load-bearing, and each one alone is wrong:
     · ratio alone catches 3440x1440, which fits comfortably (149px of
       clearance) because --hero-cardh reaches its 150px ceiling on tall
       viewports while the fixed px offsets stay constant.
     · height alone catches 1366x768 and 1280x720 — ordinary laptops that
       have never overflowed.
   The pair is what isolates "wide relative to its height".

   NOT covered, deliberately: a window both narrow AND very short, e.g.
   901x500 (ratio 1.80, one pixel above the mobile breakpoint). It
   overflows for the same reason but fails the ratio test. Catching it
   needs a per-height ratio curve that a media query cannot express, and
   a 901px-wide desktop window 500px tall is not a real reading position.

   ── WHAT IT DOES ──────────────────────────────────────────────────────
   Inside the band the whole composition shrinks together rather than any
   one piece being nudged: the mark takes a height-derived cap, the stat
   cards get shorter, and .hero-traits moves up under the nav row. The
   wordmark does shrink here — unavoidable, since fitting a 500px-tall
   window needs the stack ~160px shorter — but nobody's settled
   composition lives at these proportions.
   ══════════════════════════════════════════════════════════════════════ */
/* Three corrections, one condition. They were briefly split into a mild
   band and a severe one, on the theory that shrinking the cards alone was
   enough down to 720px — but 1920x760 fell between the two and stayed
   27px over. The real boundary is ~786px, and the mark cap below is
   already a natural no-op above that (at 1920x800 it returns 605 against
   a 595 mark), so one band is both simpler and correct.

   --hero-cardh and .hero-card's min-height MUST move together and must
   stay >= the card's real content height. The variable is what spaces
   card2 below card1, so setting it UNDER the real height shrinks nothing
   — it just slides the second card up into the first. (Tried it: cardh 84
   against a 112px card overlapped them by 10px.) 100px clears the
   measured content+padding at these sizes, so the card resolves to
   exactly the variable and the two stay consistent. */
@media (min-aspect-ratio: 12/5) and (max-height: 900px){
  :root{
    --hero-cardh:clamp(100px,12vh,120px);
    /* Solved, not guessed. With the cards pinned at 100, card2's bottom
       is 6vh + .78M + 55 + 100 + 18 + 100, so fitting needs
       M <= (0.94vh - 273) / .78 — M <= 252 at vh=500, M <= 517 at 720.
       The line through those points is 120vh - 350px; 355 buys slack.
       Above ~790 it exceeds 31vw and stops applying by itself.

       This is the shrink that was rejected as a GLOBAL cap (see
       --mark-size in :root). Scoped this narrowly it never reaches the
       owner's 1902x910 or any standard geometry — 1902x910 is ratio 2.09
       and fails the aspect condition before height is even considered. */
    --mark-size:min(31vw, calc(120vh - 355px));
  }
  .hero-card{
    min-height:clamp(100px,12vh,120px);
    padding:clamp(12px,1.3vw,18px);
  }
  /* 20px, down from 55: the panel is ~204px tall and its own height does
     not shrink with the viewport, so the offset is where the room has to
     come from. Safe against the stat cards — they sit at the left edge
     and this panel at the right, so they never share an x-range. */
  .hero-traits{top:calc(var(--hero-navrow) + 20px)}
}

/* ══════════════════════════════════════════════════════════════════════
   MOBILE — the ghost engine is off below 900px (see js/ghost.js).
   The sidebar becomes a menu sheet behind a fixed bar, and the hero is
   static: the wordmark that is a measuring stick on desktop is un-hidden
   and simply IS the hero mark here.

   ── THE CONDITION ────────────────────────────────────────────────────
   Every mobile block in this file reads:

     (max-width:900px), (max-height:560px) and (pointer:coarse)

   and every desktop block reads its exact inverse:

     (min-width:901px) and (min-height:561px),
     (min-width:901px) and (pointer:fine)

   These five queries and js/viewport.js's two strings are ONE definition
   written in two languages. They must be edited together — a gap between
   them is a viewport where the stylesheet and the engines disagree about
   which layout is live, which is not a cosmetic bug: ghost.js would spend
   the collapse absolutely positioning hero elements into a layout the CSS
   has already stacked.

   The second clause exists because width alone answers the wrong
   question. An iPhone 16 Pro Max held sideways is 932x430 — it cleared
   the old 900px bar and was handed the full desktop experience on a
   430px-tall touchscreen, hero running 51px past the fold. `pointer:
   coarse` keeps the height test aimed only at touch devices, so a desktop
   window dragged short stays a desktop. js/viewport.js has the full
   reasoning, including why the threshold is 560.
   ══════════════════════════════════════════════════════════════════════ */

/* Desktop has a permanent sidebar and needs none of this. display:none
   rather than a mobile-only stylesheet: it is ~60 lines, and splitting the
   file for it would cost a request. */
.mbar{display:none}

@media (max-width:900px), (max-height:560px) and (pointer:coarse){
  .mbar{
    position:fixed;
    top:0;left:0;right:0;
    z-index:120;                        /* over the sheet's 110 */
    height:54px;
    display:flex;
    align-items:center;
    justify-content:space-between;
    padding:0 12px 0 20px;
    /* Translucent + blur so the page reads as passing UNDER the bar
       rather than stopping at it. Opaque enough that ink type crossing
       beneath it never shows through as a smear. */
    background:rgba(240,230,220,.86);
    backdrop-filter:blur(14px);
    -webkit-backdrop-filter:blur(14px);
    border-bottom:1px solid rgba(30,26,23,.10);
  }
  /* Solid once the sheet is open — the blur would be sampling the sheet's
     flat cream anyway, and the seam between the two reads as a bug. */
  html.menu-open .mbar{background:var(--cream);border-bottom-color:transparent}

  /* Held back until the hero's giant mark has scrolled under the bar
     (js/menu.js's IntersectionObserver adds .is-branded), so the two are
     never on screen at once. visibility, not just opacity: an invisible
     link that still takes Tab is worse than no link. */
  .mbar-mark{
    font-size:19px;
    font-weight:900;
    font-stretch:62.5%;
    letter-spacing:-.03em;
    line-height:1;
    text-transform:uppercase;
    color:var(--ink);
    background:var(--gold);
    padding:5px 8px 4px;
    border-radius:7px;
    opacity:0;
    visibility:hidden;
    transition:opacity .3s var(--ease),visibility .3s;
  }
  .mbar.is-branded .mbar-mark{opacity:1;visibility:visible}
  /* The sheet carries its own wordmark 20px lower — two would stack. */
  html.menu-open .mbar-mark{opacity:0;visibility:hidden}

  .mbar-toggle{
    width:42px;height:42px;
    /* margin-left:auto keeps the button hard right on the first screen,
       where .mbar-mark is invisible AND out of layout, so space-between
       has only one child to place. */
    margin-left:auto;
    display:flex;
    align-items:center;
    justify-content:center;
    border:none;
    border-radius:11px;
    background:var(--ink);
    color:var(--cream);
    font:inherit;
    cursor:pointer;
    transition:background-color .25s var(--ease),color .25s var(--ease);
  }
  html.menu-open .mbar-toggle{background:var(--gold);color:var(--ink)}
  .mbar-toggle:focus-visible{outline:2px solid var(--ink);outline-offset:3px}

  /* Two bars that cross into an X. Both animate `top` and `transform`
     together, so the halves meet in the middle as they rotate rather than
     rotating first and sliding after. */
  .mbar-bars{position:relative;display:block;width:18px;height:12px}
  .mbar-bars i{
    position:absolute;
    left:0;
    width:100%;height:2px;
    border-radius:2px;
    background:currentColor;
    transition:top .3s var(--ease),transform .3s var(--ease);
  }
  .mbar-bars i:nth-child(1){top:1px}
  .mbar-bars i:nth-child(2){top:9px}
  html.menu-open .mbar-bars i:nth-child(1){top:5px;transform:rotate(45deg)}
  html.menu-open .mbar-bars i:nth-child(2){top:5px;transform:rotate(-45deg)}

  /* ---- .nav becomes the menu sheet -----------------------------------
     Not a top block any more. It used to render the entire sidebar
     vertically at the head of the document: 497px on a 375x812 phone —
     61% of the first screen — and then it scrolled away for good, leaving
     ~7500px of page with no navigation at all.

     As a sheet it costs the first screen nothing and is one tap away from
     anywhere. visibility (not just opacity) is what takes it out of the
     tab order while closed; both transition, so the fade still plays.

     z-index 110: above the page and .cta-float's 65, below .mbar's 120 so
     the X that closes it stays on top of what it closes, and below the
     modals' 200 — a Work archive opened from a menu link must cover it. */
  .nav{
    position:fixed;
    inset:0;
    width:100%;
    height:100%;
    height:100dvh;                       /* iOS URL bar — see .wa-panel */
    z-index:110;
    padding:74px 20px 104px;             /* 74 clears .mbar, 104 the CTA */
    overflow-y:auto;
    -webkit-overflow-scrolling:touch;
    background:var(--cream);
    opacity:0;
    visibility:hidden;
    transform:translateY(-6px);
    transition:opacity .28s var(--ease),transform .28s var(--ease),visibility .28s;
  }
  /* The open state drops `visibility` from its transition list, and that
     is not a tidy-up — it is what makes the sheet focusable.

     Transitioned visibility is discrete: intermediate values resolve to
     `visible`, but at progress 0 the value is still the FROM value. So
     with visibility transitioned in both directions, js/menu.js's
     first.focus() — one statement after the class is added — ran against
     a sheet the engine still considered hidden, and hidden elements
     cannot take focus. The call silently no-opped and focus stayed on
     the hamburger, so a keyboard user opened the menu into nothing.

     Removing it here makes visibility:visible apply on the same tick.
     The base rule above keeps it, which is what holds the sheet on screen
     for the full 280ms of the closing fade instead of blanking it
     instantly. Direction-specific by construction. */
  html.menu-open .nav{
    opacity:1;
    visibility:visible;
    transform:none;
    transition:opacity .28s var(--ease),transform .28s var(--ease);
  }

  /* height:auto is the important one. The desktop rule is
     height:calc(100vh - 40px) so the sidebar fills the viewport and
     Sidebar.scale() can shrink it to fit; unreset here it made the header
     a full screen tall regardless of content, so ~390px of empty panel sat
     between the email row and the CTA below it. */
  .nav-inner{width:100%;height:auto;transform:none!important;gap:14px}
  /* Full-width row: space-between would fling the © to the far edge. */
  .nav-brandrow{justify-content:flex-start;gap:7px;align-items:baseline}
  .nav-logo-slot{flex:none}
  /* The tagline moves to the hero on mobile (.hero-mtag in index.html).
     Hidden here rather than deleted because desktop's sidebar still wants
     it — and a menu does not need a mission statement. */
  .nav-tagline-panel{display:none}
  /* ---- Header weight -------------------------------------------------
     The top bar was rendering the ENTIRE sidebar vertically — wordmark,
     socials, tagline, stat cards, seven nav pills, the client marquee,
     the email row, the CTA. Measured 840px on a 390x844 phone, so the
     whole first screen was navigation and his portrait didn't begin
     until screen two.

     The two panels hidden here are not losses: both are duplicated by a
     real section further down (the stat cards by .stats-band, the client
     marquee by the Trusted By .brands section), so on mobile they were
     furniture shown twice. Everything that is NOT duplicated — tagline,
     nav, email, CTA — stays. */
  .nav-stats,
  .nav-clients{display:none}

  /* A column of full-width rows, not the wrapped pills this used to be.
     Wrapping existed to keep seven pills inside a 497px header; the sheet
     has the whole screen, and a menu whose destinations are one per line
     is both easier to read and easier to hit.

     flex:0 0 auto and align-content:flex-start stay load-bearing. On
     desktop .nav-links is `flex:1` so the pill column fills the sidebar's
     height; kept here it grew to 535px and distributed the rows across
     all that leftover space, leaving ~90px gaps between them.
     justify-content:space-between (also inherited) did the same
     vertically. */
  .nav-links{
    flex:0 0 auto;
    flex-direction:column;
    align-items:stretch;
    align-content:flex-start;
    justify-content:flex-start;
    gap:6px;
  }
  /* 52px rows. .nav-item is what takes the tap (it gets pointer-events
     back under .no-ghost), so the pill — not the <a> inside it — is the
     target that has to be thumb-sized. Comfortably past the ~44px floor. */
  .nav-item{padding:16px 16px 14px;gap:12px;border-radius:12px}
  .nav-link{font-size:15px}
  .nav-ico{width:22px;height:22px}
  .nav-ico svg{width:17px;height:17px}

  /* The placeholder goes and the real button (#cta-float, a SIBLING of
     .nav in the DOM — see its comment in the desktop rules) is pinned to
     the bottom of the sheet instead of flowing inside it. It cannot flow
     inside it: it is not a descendant. Fixed positioning is what lets it
     sit there anyway, and is also the better result — the primary action
     stays put while the destinations above it scroll.

     Gated on menu-open, so it is not a button floating over the page
     while the menu is shut. js/main.js's CtaFloat.sync clears the inline
     top/left/transform below 900px, which is what lets these rules win. */
  .nav-ctas{display:none}
  .cta-float{
    position:fixed;
    left:20px;right:20px;bottom:20px;
    width:auto;
    z-index:115;
    transform:none;
    opacity:0;
    visibility:hidden;
    pointer-events:none;
    transition:opacity .28s var(--ease),visibility .28s;
  }
  /* Drops visibility from the transition list for the same reason .nav
     does — this button is the last stop in the sheet's focus trap. */
  html.menu-open .cta-float{
    opacity:1;
    visibility:visible;
    pointer-events:auto;
    transition:opacity .28s var(--ease);
  }
  .btn{flex:1}
  .page{padding-left:0}

  /* Anchor destinations clear the fixed bar. Doing it here keeps
     js/menu.js out of the business of computing scroll offsets — the
     browser's own anchor jump lands correctly, and so does a deep link
     pasted straight into the address bar. */
  .hero,.section,.brands,.stats-band{scroll-margin-top:64px}

  /* ---- Touch targets --------------------------------------------------
     Measured on a real 390px viewport: the email button was 15px tall,
     the social links 24x24, Read more 35px, the CTA 38px. All are
     comfortably tappable with a mouse and none of them are on a phone,
     where ~44px is the floor. Padding rather than height so the type
     inside keeps its own vertical rhythm. */
  .nav-socials a{width:40px;height:40px}
  .nav-socials svg{width:16px;height:16px}
  .nav-mail{padding:12px 0;min-height:44px}
  .nav-mail-panel{padding:6px 16px}
  .about-readmore{min-height:44px;padding-left:18px;padding-right:18px}
  .btn{height:44px}
  /* The layered composition needs width it doesn't have here, so the hero
     becomes a plain stack: wordmark, copy, then him.

     overflow-x:clip is a guard on the wordmark, which is sized by
     dividing the available width by 3 — a ratio measured against Archivo
     at 62.5% stretch (it actually lands at 2.87, so there is ~4% of
     slack). If that face ever fails to load, the fallback sets MERHEB
     far wider and the mark would push the page sideways. Clipping the
     overshoot is much the lesser failure: a scrollbar on a phone makes
     every section on the site look broken. Same device .about already
     uses, same reason. */
  .hero{height:auto;margin-left:0;width:100%;overflow-x:clip}
  /* DOM order puts him mid-composition, which only makes sense layered.
     Stacked, he reads as the closer — so re-order rather than re-nest. */
  .hero-stage{
    position:relative;height:auto;overflow:visible;padding:66px 20px 0;
    display:flex;flex-direction:column;
  }
  .hero-portrait{order:4}

  /* display:none on .hero-decor itself, not just its children — display:none
     removes it from layout entirely, so its sticky/height:100vh/negative
     margin (tuned for the desktop-only two-sticky-layer stack) never
     applies here. Without this it would reserve a spurious 100vh gap. */
  .hero-links-row,.hero-cta-row,.hero-decor,
  .hero-cards,.hero-traits{display:none}

  /* ---- The mobile hero -----------------------------------------------
     .wordmark-ghost is normally an invisible measuring stick — .ghost sets
     opacity:0 and ghost.js reads its rect to know where the sidebar's real
     wordmark should start out. Below 900px ghost.js declines to run, so
     nothing is measuring it and nothing else is going to render a mark:
     un-hiding it turns the stick into the actual hero wordmark, with no
     new markup and no second copy of the word.

     Sized by the same solved rule the desktop no-ghost block uses: MERHEB
     renders almost exactly 3x its font-size wide in this face at this
     weight and stretch, so dividing the available width by 3 fills the
     measure at every phone width rather than at one. */
  .hero-wordmark-row{
    position:static;
    left:auto;top:auto;
    transform:none;
    width:100%;
  }
  .wordmark-ghost{
    opacity:1;
    font-size:calc((100vw - 40px) / 3);
    line-height:.82;
  }
  .hero-mtag{
    display:block;
    margin-top:14px;
    max-width:30ch;
    font-size:15px;
    line-height:1.45;
    font-weight:500;
    opacity:.72;
  }
  .hero-portrait{
    position:relative;
    left:auto;top:auto;bottom:auto;
    transform:none;
    height:auto;
    width:min(100%,340px);
    margin:26px auto -1px;
  }
  .hero-portrait img{width:100%;height:auto}

  .section{padding:60px 20px}
  .brands .section-eyebrow{padding:0 20px}
  .stats-band{grid-template-columns:1fr;gap:26px;padding:52px 20px}
  .plat{grid-template-columns:40px 1fr auto;gap:6px 14px;margin:0;padding:16px 4px}
  .plat-ico{width:40px;height:40px}
  .plat-note,.plat-arrow{display:none}
  /* Fan flattens to a stack — no room to swipe a hand of overlapping cards
     on a touch width, and the hover-pop has no touch equivalent anyway.
     Same specificity as the desktop rules above (one class, no pseudo
     beyond :hover), so simply appearing later in the file is what wins. */
  /* gap:14px is the fix for "these space weirdly". The fan overlaps its
     cards by design (margin-left:-70px), and flattening it to a column
     zeroed that margin without putting anything back — so at rest the
     five cards sat flush against each other, edge to edge, reading as one
     ridged slab rather than five quotes. */
  /* max-width caps the MEASURE, and it is the one thing in this section
     that genuinely needed a tablet pass. Stacked full-width, the quote
     text ran 83ch at 744px, 94ch at 834 and 103ch at 900 — and worst of
     all 107ch on a landscape phone (932px wide, which is mobile mode by
     height now), against the ~45-75ch the eye tracks comfortably. 560
     lands it at ~64ch on every width in that band.

     On the container, not the card: one rule then covers the whole
     600-932px range in one place. Below ~600 the viewport is already
     narrower than the cap so phones are untouched — 393px still measures
     38ch, exactly as before.

     Centred rather than left-aligned: the cards keep .quote-hand's own
     justify-content:center from the desktop fan, and a 560px column
     pinned left in a 932px viewport reads as a layout bug rather than a
     choice. Measured, not guessed — the other four text blocks in this
     band (about blurb 44ch, section lead 46ch, FAQ answer 60ch, work desc
     32ch) were all already inside the comfortable range and are
     deliberately left alone. */
  .quote-hand{
    flex-direction:column;
    align-items:stretch;
    padding:20px 0;
    gap:14px;
    max-width:560px;
    margin-left:auto;
    margin-right:auto;
  }
  .quote-reveal{width:100%;margin-left:0}
  /* min-height:150px removed. Three of the five quotes are shorter than
     that, so all three rendered at exactly 150 with the caption pushed to
     the bottom by .quote's own justify-content:space-between — a visible
     hole between the words and the name that got bigger the shorter the
     quote was. Content height, with the new gap providing the rhythm the
     uniform height was standing in for. */
  .quote{height:auto;transform:none}
  /* No hover lift on touch: there is no pointer to leave, so the tapped
     card stays translated until something else takes focus. */
  .quote-reveal:hover .quote{transform:none}
  /* The stack shows every quote in full already, so "hover a card" is both
     untrue (no hover surface) and pointing at a problem that doesn't exist
     here. Scoped to .testimonials specifically — .section-lead is shared
     by Work and FAQ, which still want theirs on mobile. */
  .testimonials .section-lead{display:none}
  /* 16px exactly, and it is not a taste call. iOS Safari zooms the whole
     page in when a focused input's text is under 16px, and it does NOT
     zoom back out on blur — so tapping Name left the visitor stranded on
     a page 1.3x too wide for the rest of the session, with the panel's
     right edge off-screen. Any value >=16 prevents it; 16 is the smallest
     that does, so the field still matches the panel's type scale. */
  .pw-field input,
  .pw-field textarea{font-size:16px}
  .chat-avatar{--av:44px}
  .chat-bubble{font-size:16px;padding:13px 18px}
  /* Full-width stacked rather than side by side: two 15px pills wrap to
     two rows at this width anyway, and ragged wrapped pills read worse
     than a deliberate stack. */
  /* Back to one column — two 3-question stacks at this width would leave
     each question wrapping to three lines. */
  .faq-list{grid-template-columns:1fr}
  .faq-col + .faq-col{border-top:none}
  .chat-actions{flex-direction:column;align-self:stretch}
  .chat-send,.chat-call{padding:14px 26px;font-size:15px}
  .about-path{display:none}
  .about-dot{display:none}
  /* padding-bottom reserves room for the connector's run-out and dashes;
     both are display:none here, so it would only be dead space. The card
     rules go with them — same marker. They're hidden rather than skipped
     because a desktop->mobile resize leaves already-built ones in the DOM. */
  .about-timeline{max-width:520px;padding-bottom:0}
  .about-rule{display:none}
  .about-card{width:100%}
  .about-card.is-left,.about-card.is-right{margin-left:0;margin-right:0}
  .about-card + .about-card{margin-top:26px}
  .footer{padding:22px 20px 30px}
}

/* ghost.js sets .no-ghost when it declines to run (narrow viewport or
   reduced motion). The sidebar then renders as itself, and the hero has to
   stand up without a single scroll-driven element.

   This block used to hand that job to a static centre headline
   (.hero-title / .hero-mid — both since deleted, see git history). That
   headline was removed from the markup at the owner's request, and these
   rules were not revisited at the time — so
   desktop-with-motion-off was left hiding the wordmark row, the nav row,
   the CTA row, the cards AND the portrait, and pointing what remained at
   an element that no longer exists. The result was a 100vh cream void with
   one aria-hidden glass panel floating in it. Anyone browsing with
   "reduce motion" on — a system default for a lot of people — got that as
   the entire first screen.

   The fix needs no new markup. .wordmark-ghost is already a full-size gold
   MERHEB (it is the measuring stick ghost.js reads); it is invisible only
   because .ghost sets opacity:0. Un-hiding it plus the portrait rebuilds
   the hero's identity out of what is already on the page, and it stays
   aria-hidden so it never double-announces against the sidebar's real
   wordmark. */
/* min-width:901px is load-bearing, not tidiness. This rule is written for
   desktop-with-motion-off, but `html.no-ghost .hero` (0,2,1) outranks the
   mobile block's `.hero{height:auto}` (0,1,0) AND sits later in the file,
   so it also won on every phone — where .no-ghost is always set, because
   ghost.js declines below 900px. The mobile hero's content measured 508px
   inside the 812px box that bought it, leaving ~300px of empty cream
   between his portrait and the first About card. That gap was the top
   complaint in the mobile review; this media query is the whole fix. */
@media (min-width:901px) and (min-height:561px), (min-width:901px) and (pointer:fine){
  html.no-ghost .hero{height:100vh}
}
/* Still hidden: these are pure measuring sticks whose content is already
   legible in the assembled sidebar, and the giant nav labels in particular
   would just duplicate the nav two inches to their left. */
html.no-ghost .hero-links-row,
html.no-ghost .hero-cta-row,
html.no-ghost .hero-cards{display:none}
/* No mobile counterpart to this block any more. It used to hide the
   wordmark row below 900px, on the reasoning that the sidebar stacked
   above the hero already showed one. The sidebar is a menu sheet now and
   shows nothing until it is opened, so the mark IS the mobile hero — see
   .hero-wordmark-row in the max-width:900px block. */
@media (min-width:901px) and (min-height:561px), (min-width:901px) and (pointer:fine){
  /* Re-aimed at the CONTENT area, because this mode changes what the
     wordmark is sharing the screen with. In the animated hero it is
     centred on the viewport and deliberately bleeds off both edges — safe,
     because at scroll 0 the sidebar's panels are still faded out and it
     has the whole width to itself. Under .no-ghost the sidebar is fully
     assembled from the first paint, and .hero-stage sits above it at
     z-index 60, so the same full-bleed mark simply painted over the
     socials and the top nav pills.

     Centre shifts by half the sidebar (50% + nav-w/2), and the size is
     solved rather than guessed: MERHEB renders almost exactly 3x its
     font-size wide in this face at this weight and stretch, so dividing
     the free width by 3 makes it fill the gap between sidebar and right
     edge at every desktop width instead of only at one. */
  html.no-ghost .hero-wordmark-row{left:calc(50% + var(--nav-w) / 2)}
  html.no-ghost .wordmark-ghost{
    opacity:1;
    font-size:calc((100vw - var(--nav-w) - var(--gutter) * 2) / 3);
  }
}

@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms!important;transition-duration:.01ms!important}
}
