/* ============================================================
   Portfolio hero — production styles
   Single source of truth for index.html.
   Relative url()s resolve from this file's location (repo root), so the
   ./assets/... paths match the HTML.
   ============================================================ */

/* ----- Futura LT (display) — web-licensed, shipped in assets/fonts ----- */
@font-face {
  font-family: "Futura LT";
  src: url("./assets/fonts/FuturaLT-Bold.woff2") format("woff2"),
       url("./assets/fonts/FuturaLT-Bold.woff") format("woff");
  font-weight: 700; font-style: normal; font-display: swap;
}

:root {
  /* --- semantic color tokens (mirrors the Figma "Semantic" collection) --- */
  --surface-default: #fbfdfe;   /* neutral/10 — page background */
  --surface-raised:  #f9fcff;   /* neutral/20 — alternating case-band tint */
  --surface-card:    var(--neutral-50);  /* light card/sticker on a dark surface (About) */
  --text-primary:    #252626;
  --text-secondary:  #55585b;   /* Figma var is misspelled "text/seconday" */
  --text-accent:     #4549e7;
  --accent-default:  #4549e7;
  --text-on-accent:  #ffffff;   /* decoupled from surface → its intended value, white */
  --indigo-overlay:  rgba(38, 33, 92, 0.5);
  --neutral-200:     #e0e5ea;                         /* new neutral shade (cool light grey) */
  --neutral-50:      #f3f9ff;                         /* neutral/50 — coolest light neutral tint */
  --border-default:  var(--neutral-200);              /* hairline frame/divider */
  --logo-tint:       #55585b;                         /* solid monochrome fill for client logos */
  --surface-placeholder: #d9dbe6;                     /* case image placeholder (cool grey) */
  --surface-deep:    #26215c;                         /* indigo/900 — deep closing surface (About) */
  --accent-subtle:   #cecbf6;                         /* indigo/100 — light accent for dark surfaces */
  --indigo-10:       #f3f3ff;                         /* indigo/10 — lightest indigo tint */

  --font-display: "Futura LT", "Futura", "Century Gothic", "Trebuchet MS", sans-serif;
  --font-body: "Source Sans 3", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --gutter: clamp(24px, 8.33vw, 120px);   /* 120px at 1440 */
  --stage-max: 1800px;                     /* strategy B: cap & center, image bleeds past it */
  --content-max: 678px;

  /* --- DEFAULT (Figma-frame) type & spacing ---
     Base values. Also used <961px (mobile, where the clamps scale down).
     Overridden by the viewport tiers further below. */
  --h1-size:     clamp(42px, 11vw, 112px);
  --role-size:   clamp(26px, 4vw, 40px);
  --body-size:   clamp(16px, 1.4vw, 18px);
  --gap-section: clamp(32px, 4.4vw, 64px);   /* name (H1) ↔ "Designer & Researcher" */
  --gap-desc:    40px;                         /* "Designer & Researcher" ↔ body */
  --gap-cta:     40px;                         /* body ↔ button group */
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  font-family: var(--font-body);
  background: var(--surface-default);
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;   /* guards the full-bleed image/footer */
}
img { display: block; max-width: 100%; }
.accent { color: var(--text-accent); }

/* ===== Frame (XL) ===== */
.xl {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(360px, 46vw, 660px);
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "topbar  topbar"
    "content image"
    "footer  footer";
  min-height: 100vh;
  background: var(--surface-default);
  overflow: hidden;
}

.top-bar {
  grid-area: topbar;
  display: flex; align-items: flex-end;
  padding: clamp(20px, 5vw, 40px) var(--gutter) clamp(14px, 2.5vw, 28px);
}
.monogram {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(24px, 3vw, 32px); line-height: 1; color: var(--text-primary);
}

/* Sticky monogram bar — fixed at the top, solid, with a bottom hairline that
   matches the client-logos borders. The monogram sits with 24px above and below.
   It stays in place (no slide) and a tiny fade-in (`--visible`) reveals it once
   the user scrolls past the logos — animations.js fires this exactly when the
   logo bar's bottom hairline reaches this bar's bottom hairline, so they overlap. */
.monogram-bar {
  position: fixed; inset: 0 0 auto 0; z-index: 20;
  display: flex; align-items: center;
  padding: 24px var(--gutter);
  background: var(--surface-default);
  border-bottom: 1px solid var(--border-default);
  pointer-events: none;            /* the bar passes clicks through; only the mark is clickable */
  opacity: 0;                      /* sits in place; the fade-in reveals it (no slide) */
  visibility: hidden;              /* keep the button out of the tab order while hidden */
  transition: opacity 0.15s ease, visibility 0.15s ease;
}
.monogram-bar--visible { opacity: 1; visibility: visible; }
.monogram-bar__mark {
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(24px, 3vw, 32px); color: var(--text-primary);
  /* bare button → looks like the wordmark, but clicks to scroll back to top */
  appearance: none; -webkit-appearance: none;
  margin: 0; padding: 0; border: 0; background: none;
  pointer-events: auto; cursor: pointer;
}
.monogram-bar__mark:focus-visible {
  outline: 2px solid var(--accent-default); outline-offset: 4px; border-radius: 2px;
}
/* The mark must line up with the page content (the `.xl` stage) at every width.
   The bar is fixed + full-width, so (like the case top bar) it has to mirror the
   `.xl` gutter on phone/tablet, and inset to the centred 1800px stage on desktop
   (the same padding the client-logo bar uses to align with the stage). */
@media (max-width: 699.98px) { .monogram-bar { --gutter: 24px; } }
@media (min-width: 700px) and (max-width: 960px) {
  .monogram-bar { --gutter: clamp(40px, calc(40px + (100vw - 700px) * 0.2154), 96px); }
}
@media (min-width: 961px) {
  .monogram-bar { padding-inline: max(var(--gutter), calc((100vw - var(--stage-max)) / 2 + var(--gutter))); }
}
@media (prefers-reduced-motion: reduce) { .monogram-bar { transition: none; } }

.content {
  grid-area: content; align-self: center;
  display: flex; flex-direction: column; gap: var(--gap-cta);
  padding-left: var(--gutter); padding-right: clamp(16px, 3vw, 40px);
  max-width: var(--content-max);
}
.text { display: flex; flex-direction: column; gap: var(--gap-section); }
.h1 {
  display: flex; flex-direction: column;
  font-family: var(--font-display); font-weight: 700; line-height: 1; letter-spacing: -0.0125em;
  font-size: var(--h1-size);
}
.h1 .line { white-space: nowrap; }
.h1 .line--accent { color: var(--text-accent); }
.h1 .line--primary { color: var(--text-primary); }

.description { display: flex; flex-direction: column; gap: var(--gap-desc); }
.role {
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--role-size); line-height: 1.05; letter-spacing: -0.005em; color: var(--text-primary);
}
/* Tagline roll (cycling roles) — armed by animations.js (`role--roll`) only when
   JS+GSAP run and motion is allowed; otherwise the static `.role` above shows.
   The container is a fixed-height mask; each phrase is a `.role__line` of
   `.role__word`s that GSAP rolls vertically (clipped by the mask). Font-size is
   fitted in JS so the widest phrase never wraps. */
/* Height leaves room for Futura's descenders (g, p, j, y) above and below the
   centred line so `overflow: hidden` doesn't clip them; the roll distance tracks
   this height (read live in JS), so the constant-gap guarantee is unaffected. */
.role--roll { position: relative; overflow: hidden; height: 1.45em; }
.role--roll .role__line {
  position: absolute; inset: 0; margin: 0;
  display: flex; align-items: center; gap: 0.25em;
  line-height: 1; white-space: nowrap;
}
.role--roll .role__word { display: inline-block; will-change: transform; }
.role--roll .role__word--accent { color: var(--text-accent); }
.body {
  font-family: var(--font-body); font-weight: 400;
  font-size: var(--body-size); line-height: 1.5; color: var(--text-secondary);
  max-width: 518px;
}

.button-group { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.button {
  font-family: var(--font-body); font-weight: 700; font-size: 16px; line-height: 1;
  padding: 20px 24px; border-radius: 80px; border: none; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5em;
  white-space: nowrap; cursor: pointer; transition: opacity 0.15s ease;
}
/* hover/press affordance is the GSAP "Lift" (see animations.js) */
.button--primary { background: var(--accent-default); color: var(--text-on-accent); }
.button--secondary { background: transparent; color: var(--text-accent); }

.hero-image {
  grid-area: image; position: relative; min-height: 320px;
  border-top-left-radius: clamp(120px, 25vw, 368px);
  overflow: hidden; background: #d9dbe6;
}
.hero-image__photo { width: 100%; height: 100%; object-fit: cover; object-position: center 22%; }
.hero-image__overlay {
  position: absolute; inset: 0;
  background: var(--indigo-overlay); mix-blend-mode: plus-lighter; pointer-events: none;
}

.logo-ticker {
  grid-area: footer; background: var(--surface-default); min-height: 157px;
  border-top: 1px solid var(--border-default);
  border-bottom: 1px solid var(--border-default);
  display: flex; align-items: center; justify-content: center;
  padding: 24px var(--gutter);
}
/* marquee viewport — clips the moving track; when the marquee runs (is-marquee,
   added by animations.js) it fades the track at both edges so logos fade in on
   the right and out on the left. */
.logos-viewport { width: 100%; overflow: hidden; }
.logos-viewport.is-marquee {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.logos {
  list-style: none; display: flex; align-items: center;
  gap: 120px;  /* spacing between logos on mobile; 160px on large (below) */
  /* no-JS / reduced-motion fallback: wrap & centre so every logo is visible */
  flex-wrap: wrap; justify-content: center;
}
@media (min-width: 961px) { .logos { gap: 160px; } }
@media (max-width: 549.98px) { .logos { gap: 64px; } }   /* tighter on small phones */
/* With JS active the strip is a single row from page load, so the reserved
   logo-area height never changes when the marquee starts (no layout jump). */
html.js .logos { flex-wrap: nowrap; justify-content: flex-start; }
/* marquee track (JS): sized to content so GSAP can slide it left */
.logos--marquee { width: max-content; }
.logos li { display: flex; align-items: center; flex: 0 0 auto; }

/* client logos: each SVG's shape recoloured to a solid fill via CSS mask (the
   mask uses the alpha/shape, so the SVG's own colours are ignored). aspect-ratio
   keeps proportions; height is tuned per logo for equal visual weight. */
.logo {
  display: block; height: 28px; background-color: var(--logo-tint);
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-size: contain;     mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.logo--ns            { aspect-ratio: 165 / 64;  height: 30px;
  -webkit-mask-image: url(./assets/ns.svg);            mask-image: url(./assets/ns.svg); }
.logo--backbase      { aspect-ratio: 301 / 52;  height: 22px;
  -webkit-mask-image: url(./assets/backbase.svg);      mask-image: url(./assets/backbase.svg); }
.logo--ziggo         { aspect-ratio: 184 / 64;  height: 29px;
  -webkit-mask-image: url(./assets/ziggo.svg);         mask-image: url(./assets/ziggo.svg); }
.logo--essent        { aspect-ratio: 291 / 64;  height: 22.8px;
  -webkit-mask-image: url(./assets/essent.svg);        mask-image: url(./assets/essent.svg); }
.logo--postnl        { aspect-ratio: 1 / 1;     height: 42px;
  -webkit-mask-image: url(./assets/postnl.svg);        mask-image: url(./assets/postnl.svg); }
.logo--timing        { aspect-ratio: 225 / 71;  height: 28.67px; transform: translateY(4.6px);  /* drop so its baseline aligns with essent's */
  -webkit-mask-image: url(./assets/timing.svg);        mask-image: url(./assets/timing.svg); }
.logo--libertyglobal { aspect-ratio: 89 / 99;   height: 48px;
  -webkit-mask-image: url(./assets/libertyglobal.svg); mask-image: url(./assets/libertyglobal.svg); }
.logo--toogethr      { aspect-ratio: 267 / 119; height: 30px;
  -webkit-mask-image: url(./assets/toogethr.svg);      mask-image: url(./assets/toogethr.svg); }

/* ============================================================
   DESKTOP (≥961px) — Strategy B: cap & center the stage at 1800,
   let the image + footer bleed to the viewport edges beyond it.
   ============================================================ */
@media (min-width: 961px) {
  .xl { max-width: var(--stage-max); margin-inline: auto; overflow: visible; }
  .hero-image {
    margin-right: min(0px, calc((var(--stage-max) - 100vw) / 2));
  }
  .logo-ticker {
    margin-inline: min(0px, calc((var(--stage-max) - 100vw) / 2));
    padding-inline: max(var(--gutter), calc((100vw - var(--stage-max)) / 2 + var(--gutter)));
  }
}

/* ===== type & spacing tiers — width AND height aware =====
   Shorter viewports get smaller type/spacing so the hero keeps fitting.
   Cascade order matters: LARGER first, then SMALLER (overrides on short
   screens), then EXTRA-SMALL (overrides on very short screens). */

/* ----- LARGER · only when wide (≥1561) AND genuinely tall (≥1080) -----
   The big 136px type needs real vertical room; below 1080 tall it crams,
   so wide-but-short viewports fall back to the Default tier. */
@media (min-width: 1561px) and (min-height: 1080px) {
  .xl {
    --h1-size:     clamp(112px, 8vw, 136px);
    --role-size:   clamp(40px, 2.8vw, 48px);
    --body-size:   clamp(18px, 1.2vw, 20px);
    --gap-section: clamp(64px, 5vw, 92px);
    --gap-desc:    clamp(32px, 2.4vw, 40px);
    --gap-cta:     clamp(64px, 5vw, 92px);
    --content-max: 760px;
  }
}

/* ----- 1431–1560px wide & tall: base = Figma-frame defaults ----- */

/* ----- SMALLER · narrow widths (961–1430) OR short viewports (≤860 tall) ----- */
@media (min-width: 961px) and (max-width: 1430px),
       (min-width: 961px) and (max-height: 860px) {
  .xl {
    --h1-size:     clamp(72px, 7vw, 96px);
    --role-size:   clamp(28px, 2.6vw, 34px);
    --body-size:   16px;
    --gap-section: clamp(28px, 3.4vw, 48px);
    --gap-desc:    clamp(16px, 1.8vw, 24px);
    --gap-cta:     clamp(28px, 3.4vw, 48px);
    --content-max: 600px;
  }
}

/* ----- EXTRA-SMALL · very short viewports (≤680 tall) ----- */
@media (min-width: 961px) and (max-height: 680px) {
  .xl {
    --h1-size:     clamp(56px, 5.2vw, 76px);
    --role-size:   clamp(24px, 2vw, 30px);
    --body-size:   15px;
    --gap-section: clamp(20px, 2.4vw, 32px);
    --gap-desc:    18px;
    --gap-cta:     18px;
    --content-max: 560px;
  }
}

/* ----- short viewports also trim the chrome (top bar + footer) so the hero fits ----- */
@media (min-width: 961px) and (max-height: 860px) {
  .top-bar { padding-top: clamp(16px, 3vw, 28px); padding-bottom: clamp(10px, 2vw, 18px); }
  .logo-ticker { min-height: 116px; }
}
@media (min-width: 961px) and (max-height: 680px) {
  .logo-ticker { min-height: 92px; }
}

/* ===== phones (<700px) — centered hero with a circular avatar above the name
   (Figma node 36:264): top-bar → avatar → H1 → tagline → body → buttons, each
   32px apart; avatar/H1/tagline/body centered, buttons fill the width (capped at
   240px, group centred). Tablets ≥700px keep the original stacked layout (next
   block). 699.98 (not 700) leaves no sub-pixel gap before that block's 700px. ===== */
@media (max-width: 699.98px) {
  .xl {
    --gutter: 24px;          /* tighter side margins on phones */
    --body-size: 16px;       /* body copy fixed at 16px */
    --h1-size: clamp(40px, 12vw, 48px);   /* 48px at the 402px design width */
    /* Tagline capped at the 28px design size, shrinking on narrow phones so the
       static "Designer & Researcher" never wraps (the roll fits the longest
       phrase in JS on top of this). 11.9 = Futura width per 1px; 52 = gutters + safety. */
    --role-size: min(28px, calc((100vw - 52px) / 11.9));
    --gap-section: 32px;     /* H1 ↔ tagline */
    --gap-desc:    32px;     /* tagline ↔ body */
    --gap-cta:     32px;     /* body ↔ buttons */
    grid-template-columns: minmax(0, 1fr);   /* clamp min-content so the wide marquee track can't blow the grid out */
    grid-template-rows: auto auto auto auto;
    grid-template-areas: "topbar" "image" "content" "footer";
    align-content: start;    /* stack from the top: the logo bar flows under the content, not pinned to the viewport bottom */
    min-height: 0;           /* content-height hero (no 100vh floor) so the gap from the logo bar to the first case stays constant instead of growing with viewport height */
  }
  .top-bar { padding: 32px var(--gutter) 0; }
  /* The hero "M." is hidden under JS (it lives in the sticky scroll bar), so on
     phones the top bar is just empty space above the avatar. Collapse it under JS
     so the avatar sits the same distance from the top as the gap below it (the
     space above = the space below = the avatar's 32px margin). No-JS keeps the bar
     so the brand mark still shows. */
  html.js .top-bar { display: none; }
  .monogram { font-size: 32px; }
  .logo-ticker { min-height: 105px; margin-top: 64px; }   /* ~33% shorter bar, sitting 64px below the buttons */

  /* profile photo → centered 180px circle (keeps the indigo overlay,
     clipped to the circle by the figure's overflow:hidden + 50% radius) */
  .hero-image {
    width: 180px; height: 180px; min-height: 0;
    margin: 32px auto;
    border-radius: 50%;
  }

  .content {
    align-self: start;
    padding: 0 var(--gutter);   /* gap to the logo bar comes from its 64px margin-top */
    max-width: none;
  }

  /* centre the name, tagline, and description */
  .h1   { align-items: center; text-align: center; }
  .role { text-align: center; }
  .role--roll .role__line { justify-content: center; }
  .body { text-align: center; margin-inline: auto; }

  /* CTAs: equal halves, capped at 240px each, group centred */
  .button-group { width: 100%; flex-wrap: nowrap; justify-content: center; }
  .button { flex: 1 1 0; min-width: 0; max-width: 240px; }
}

/* ===== tablet / large landscape (700–960px) — keep the ORIGINAL stacked layout
   (left-aligned content, then the banner image below). The phone block above
   doesn't apply here, so base styles + these few overrides reproduce it. ===== */
@media (min-width: 700px) and (max-width: 960px) {
  .xl {
    --gutter: clamp(40px, calc(40px + (100vw - 700px) * 0.2154), 96px);   /* side margins ramp 40px (700) → 96px (960) */
    --body-size: 16px;       /* body copy fixed at 16px */
    /* "Designer & Researcher" sized to fill the content width on ONE line.
       11.9 = string width per 1px in Futura LT Bold; 54px = 2×24 margins + safety. */
    --role-size: min(calc((100vw - 54px) / 11.9), 44px);
    grid-template-columns: minmax(0, 1fr);   /* clamp min-content so the wide marquee track can't blow the grid out */
    grid-template-rows: auto auto auto auto;
    grid-template-areas: "topbar" "content" "image" "footer";
  }
  .content {
    align-self: start;
    padding: clamp(16px,6vw,48px) var(--gutter) clamp(32px,8vw,56px);
    max-width: none;
  }
  .hero-image { min-height: clamp(300px, 62vw, 480px); border-top-left-radius: clamp(80px, 24vw, 220px); }
}

/* ===== Case studies =====
   Full-bleed alternating bands (sibling of .xl, so the band background spans the
   viewport edge-to-edge). Each band is a two-column row — text + image placeholder
   — capped and centred. Desktop (≥961px) alternates the image side and the surface
   tint band-to-band; below 961px the row stacks with the image on top (mobile design). */
.case {
  padding: clamp(64px, 12vw, 128px) var(--gutter);
  background: var(--surface-default);
}
.case:nth-child(even) { background: var(--surface-raised); }

.case__row {
  display: flex; flex-direction: column;
  gap: clamp(32px, 6vw, 56px);
  max-width: 1560px; margin-inline: auto;
}
.case__media {
  order: -1;                       /* stacked: image sits on top */
  width: 100%; height: clamp(220px, 56vw, 391px);
  background: var(--surface-placeholder);
  border-radius: clamp(24px, 5vw, 40px);
}
.case__text {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: clamp(32px, 4.4vw, 64px);
}
.case__heading {
  display: flex; flex-direction: column;
  gap: clamp(16px, 2.2vw, 32px);
  font-family: var(--font-display); font-weight: 700; line-height: 1.2;
  color: var(--text-primary);
}
.case__client { font-size: clamp(20px, 2.4vw, 24px); letter-spacing: -0.0025em; }
.case__title  { font-size: clamp(28px, 3.6vw, 40px); font-weight: 700; letter-spacing: -0.005em; }

@media (min-width: 961px) {
  .case__row { flex-direction: row; align-items: center; gap: clamp(48px, 5.5vw, 80px); }
  .case__text,
  .case__media { flex: 1 1 0; min-width: 0; }            /* equal halves */
  .case__media { order: 0; height: clamp(320px, 27vw, 391px); }
  .case:nth-child(even) .case__media { order: -1; }      /* image on the left */
}

/* ============================================================
   ABOUT / CONTACT — closing landing band (after .cases)
   Built from Figma "About" (node 319:8568). Deep-indigo surface so it reads
   as a deliberate, distinct close — not another case band. Desktop: a
   scattered photo/note collage LEFT (the last case's image sits right, so
   this alternates) beside a light card holding the copy.

   The collage is a proportional canvas: every piece is positioned with
   percentages of the design's 517x689 frame, so the whole composition scales
   as one. Text inside it is sized in `cqw` (the collage is the query
   container) with a px floor — and below 700px the notes leave the canvas for
   normal flow, so their text is never scaled down to an unreadable size.

   Deviations from the Figma, per DESIGN_SYSTEM.md (docs win over the file):
   heading is H3/60px (Figma drew 68px, which is off the type scale); note
   text is Source Sans 3 Label + Bold (Figma used Futura Std Medium — only
   Futura Bold ships); body colour is --text-secondary (Figma #3c3f42).
   ============================================================ */
.about {
  background: var(--surface-deep);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  padding: clamp(72px, 12vw, 144px) var(--gutter) clamp(104px, 17vw, 200px);  /* generous bottom = page close */
}
.about__inner {
  max-width: 1200px; margin-inline: auto;      /* = the Figma frame's 1200 content width at 1440 */
  display: flex; flex-direction: column;       /* stacked by default (tablet + phone) */
  gap: clamp(40px, 7vw, 72px);
}

/* ---- the collage ------------------------------------------------
   `container-type: inline-size` makes this the cqw reference for every
   piece inside, so the composition scales with the column, not the viewport
   (it stays capped at 517 on tablet while the viewport keeps growing). */
.about__collage {
  position: relative; margin-inline: auto;
  width: 100%; max-width: 517px;
  container-type: inline-size;
  display: flex; flex-direction: column; gap: 20px;   /* unpacked: photos above notes */
}
/* Unpacked default: the photos keep their scatter (they scale to any size),
   the notes sit below in normal flow at full size. The @container rule below
   folds the notes back onto the canvas once there's room for them. */
.about__photos { position: relative; width: 100%; aspect-ratio: 495 / 484; }
.about__notes {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; align-items: center; gap: 20px;
}

/* Photo "polaroids" — white card, wider bottom lip, image inset and rounded */
.about__photo {
  position: absolute; margin: 0;
  background: var(--surface-default);
  border-radius: clamp(8px, 3.1cqw, 16px);
  padding: clamp(6px, 2.32cqw, 12px);
  padding-bottom: clamp(12px, 4.64cqw, 24px);
  box-shadow: 0 25px 25px rgba(0, 0, 0, 0.25);
  /* --float is driven by buildAboutAmbient; rotation stays with CSS so it can
     keep following --rot across the collage's two layouts (see below) */
  --float: 0px;
  transform: translateY(var(--float)) rotate(var(--rot, 0deg));
}
.about__photo img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; border-radius: clamp(4px, 1.55cqw, 8px);
}
/* left/width are % of the 517-wide canvas; top is % of .about__photos' own
   box (495x484 — the photos' bounding box), which the desktop rules re-base
   onto the full 689-tall canvas. */
.about__photo--stage    { --rot: -5deg;    left: 2.32%;  top: 29.35%; width: 48.36%; aspect-ratio: 250 / 286; }
.about__photo--guitar   { --rot: 5deg;     left: 50.73%; top: 1.86%;  width: 42.55%; aspect-ratio: 220 / 306; }
.about__photo--portrait { --rot: -7.35deg; left: 40.72%; top: 54.94%; width: 41.35%; aspect-ratio: 214 / 221; }

/* Note stickers — a lighter tint card, softer double shadow */
.about__note {
  background: var(--surface-card);
  border-radius: clamp(10px, 3.1cqw, 16px);
  padding: clamp(10px, 2.32cqw, 12px) clamp(14px, 3.09cqw, 16px);
  box-shadow: 0 20px 12.5px rgba(0, 0, 0, 0.1), 0 8px 5px rgba(0, 0, 0, 0.1);
  display: flex; flex-direction: column; gap: 4px;
  --float: 0px;
  transform: translateY(var(--float)) rotate(var(--rot, 0deg));
}
/* Plain body text in ink, as drawn — the weight + colour of the value line
   below carry the hierarchy, so the key stays quiet and sentence-cased. Same
   size as the value, matching the Figma's relationship between the two. */
.about__note-key {
  display: flex; align-items: baseline; gap: clamp(8px, 2.32cqw, 12px);
  font-family: var(--font-body); font-weight: 400;
  font-size: 15px; line-height: 1.5;
  color: var(--text-primary);
}
.about__note-emoji { font-size: 1.15em; letter-spacing: normal; }
/* the disc is spun by buildNowPlaying — inline-block so the transform applies */
.about__note-disc { display: inline-block; transform-origin: 50% 50%; }

/* "Now playing" equalizer — sits in the sticker's top-right corner. `margin-left:
   auto` pushes it there against the full sticker width (the key row stretches, so
   it lands on the card's edge, not just past the label). Sized in `em` so it
   tracks the note type as the collage scales. Bars carry staggered CSS heights,
   which is the resting state under reduced motion / no JS; GSAP animates scaleY
   from there. */
.about__eq {
  display: flex; align-items: flex-end; gap: 0.13em;
  height: 0.95em; margin-left: auto; align-self: center;
}
.about__eq span {
  display: block; width: 0.17em; height: 100%;
  border-radius: 0.09em; background: var(--text-accent);
  transform-origin: 50% 100%;
}
.about__eq span:nth-child(1) { height: 55%; }
.about__eq span:nth-child(2) { height: 100%; }
.about__eq span:nth-child(3) { height: 72%; }
.about__eq span:nth-child(4) { height: 40%; }
.about__note-value {
  font-family: var(--font-body); font-weight: 700;
  font-size: 15px; line-height: 1.5;
  color: var(--text-accent);
}
/* The track title links out. It inherits the value line's bold accent, so it's
   already distinguished from the note's regular-weight ink label by weight AND
   colour, not colour alone; the underline on hover/focus is the extra cue that
   it's clickable. Not a `.button` (so it takes no GSAP Lift) — same call as the
   case page's plain text links. */
.about__note-link {
  color: inherit; text-decoration: none;
  border-radius: 2px;
}
.about__note-link:hover,
.about__note-link:focus-visible { text-decoration: underline; text-underline-offset: 0.18em; }
.about__note-link:focus-visible { outline: 2px solid var(--accent-default); outline-offset: 3px; }

.about__note--playing { --rot: 3deg;    max-width: 320px; }
.about__note--talent  { --rot: -1.5deg; max-width: 340px; }
.about__note--read    { --rot: 2.5deg;  max-width: 320px; }

/* ---- the copy card ---------------------------------------------- */
.about__card {
  background: var(--surface-card);
  border-radius: clamp(20px, 2.8vw, 40px);
  padding: clamp(28px, 5.56vw, 80px);          /* = 80px at the 1440 design width */
  display: flex; flex-direction: column; gap: clamp(28px, 3.4vw, 48px);
}
.about__heading {
  font-family: var(--font-display); font-weight: 700; line-height: 1.15;
  font-size: clamp(34px, 5.5vw, 60px); letter-spacing: -0.0075em;   /* H3 */
  color: var(--text-primary);
}
.about__heading .accent { color: var(--text-accent); }   /* the two-tone dot */
.about__body { display: flex; flex-direction: column; gap: 1.55em; }
.about__body p,
.about__connect p {
  font-family: var(--font-body);
  font-size: clamp(16px, 1.4vw, 18px); line-height: 1.6;
  color: var(--text-secondary);
}
.about__divider { border: 0; height: 1px; background: var(--border-default); margin: 32px 0; }
.about__connect { display: flex; flex-direction: column; gap: clamp(24px, 2.8vw, 40px); align-items: flex-start; }
.about__actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* ---- Desktop — two columns, collage left, card right -------------
   Bases of 517 : 623 (+ the 60px gap) are exactly the Figma's 1200 content
   width; below that both shrink in proportion, since flex-shrink is weighted
   by the basis. Basis, not `flex-grow` on a 0 basis — a 0 basis is clamped up
   to the card's own padding, which would hand the card the extra width. */
@media (min-width: 961px) {
  .about__inner { flex-direction: row; align-items: center; gap: clamp(32px, 4.2vw, 60px); }
  /* order -1 pulls the collage to the left, as drawn — the card comes first in
     the DOM so the stacked layout reads copy-then-photos with no reordering */
  .about__collage { flex: 0 1 517px; order: -1; }
  .about__card    { flex: 0 1 623px; min-width: 0; }
}

/* ---- The full collage — notes folded back onto the canvas --------
   A CONTAINER query, not a media query: what decides this is how wide the
   collage itself is, not the viewport. Below 440px of canvas the note text
   would have to shrink past its floor to keep its designed slot, and the
   stickers start colliding — so at any viewport where the column is that
   narrow (a 961px desktop two-column as much as a phone) the notes stay
   unpacked in flow instead. The photo `top`s are re-based from the
   photos-only box onto the full 689-tall frame. */
@container (min-width: 440px) {
  .about__photos { aspect-ratio: 517 / 689; }
  .about__photo--stage    { top: 20.62%; }
  .about__photo--guitar   { top: 1.31%; }
  .about__photo--portrait { top: 38.59%; }
  .about__notes { position: absolute; inset: 0; display: block; }
  .about__note { position: absolute; width: max-content; }
  .about__note-key   { font-size: clamp(13px, 2.9cqw, 15px); }
  .about__note-value { font-size: clamp(13px, 2.9cqw, 15px); }
  .about__note--playing { --rot: 8deg;     left: 1.19%;  top: 3.91%;  max-width: 46%; }
  .about__note--talent  { --rot: -2.19deg; left: 3.04%;  top: 67.73%; max-width: 62%; }
  .about__note--read    { --rot: 7.3deg;   left: 38.45%; top: 85.78%; max-width: 50%; }
}

/* ---- Stacked (<961px) — FULL-BLEED OVERLAP ------------------------
   Wherever the two-column row can't hold, the collage stops being a column
   below the copy and becomes the band's backdrop: the photos run edge to edge
   and off both sides, and the card slides up over them. Chosen from the five
   explorations in prototypes/about-mobile-variants.html (variant 02).

   `display: contents` dissolves the collage box so the photos and notes become
   siblings of the card and can sit on either side of it — the card is still
   FIRST in the DOM, so the copy is what a screen reader meets first; only the
   photos move visually. It also removes the `cqw` container, which is what
   keeps the `@container` block above from firing here (the notes must stay
   unpacked in flow); the cqw paddings inside the photos then resolve against
   the viewport and still land inside their clamps at every width.

   Sizes are clamped rather than pure percentages: the composition has to widen
   and flatten as the viewport grows, not scale uniformly, or by 960px the
   photos would be ~570px wide. Each clamp's fluid term reproduces the drawn
   390px values exactly, then caps.
   ------------------------------------------------------------------ */
@media (max-width: 960.98px) {
  .about { padding-top: 0; padding-left: 0; padding-right: 0; }
  .about__inner { gap: 0; max-width: none; }
  .about__collage { display: contents; }

  .about__photos {
    order: 1; width: 100%; max-width: none;
    aspect-ratio: auto; height: clamp(320px, 91vw, 470px);
    /* clip, NOT hidden — hidden breaks position:sticky on iOS (see css.md).
       Without it the photos hanging past both edges widen the document and
       give the whole page a horizontal scrollbar. x-only, so the portrait
       card is still free to hang below the band and under the card. */
    overflow-x: clip;
  }
  .about__photo--stage {
    --rot: -6deg; z-index: 1;
    left: clamp(-72px, -13vw, -40px); top: 13%;
    width: clamp(200px, 60vw, 330px);
  }
  .about__photo--guitar {
    --rot: 5deg; z-index: 2;
    left: auto; right: clamp(-40px, -3vw, -10px); top: -5%;
    width: clamp(195px, 58vw, 320px);
  }
  .about__photo--portrait {
    --rot: -3deg; z-index: 3;
    left: 0; right: 32px; margin-inline: auto;   /* centred, nudged 16px left */
    top: 45%;
    width: clamp(180px, 54vw, 300px);
  }

  /* the card rides up over the band; capped so the measure stays readable
     once the viewport is far wider than the phone this was drawn for */
  .about__card {
    order: 2; position: relative; z-index: 4;
    width: min(100% - 32px, 620px); margin-inline: auto;
    margin-top: clamp(-76px, -13.3vw, -46px);
    box-shadow: 0 -10px 44px rgba(0, 0, 0, 0.3);
  }
  .about__notes {
    order: 3; width: min(100% - 32px, 620px);
    margin-inline: auto; margin-top: 26px;
  }
}

/* ---- Phone — capped button. Copy stays LEFT-aligned: long-form paragraphs
   are much harder to read centred, and it matches the hero's phone treatment
   (left-aligned body under a centred name). ---- */
@media (max-width: 699.98px) {
  .about__actions { width: 100%; }
  .about__actions .button { flex: 1 1 0; min-width: 0; max-width: 240px; }
}

/* ============================================================
   CASE STUDY PAGE (case-ov-pay.html — Nederlandse Spoorwegen / OV-Pas)
   Built 1:1 from the Figma frame "Case study - NS" (1440px). The top bar,
   dividers and full-bleed image span the viewport; the hero + section content
   use the SAME stage as the home page — capped at --stage-max (1800px) and
   centred, with --gutter sides — so the side margins line up exactly with the
   home hero/cases (content is 1200 at the 1440 design width, growing toward
   1560 on wider screens). Type follows the documented scale (H2/H4/H5/H6 + Label /
   Label Large / Body / Caption); colours reuse the semantic tokens. Reveals are
   in animations.js (buildCasePage) — PE-safe + scroll-driven, like the home page.
   ============================================================ */
/* height:auto overrides the global `html,body{height:100%}` — otherwise the body
   is only one viewport tall, which caps the sticky top bar's range to the first
   screen (it un-sticks after that). overflow-x:clip (not hidden) keeps sticky
   working — `overflow:hidden` on the body breaks position:sticky on iOS. */
.cs-body { background: var(--surface-default); overflow-x: clip; height: auto; min-height: 100vh; }

/* Page margins — mirror the home page's side gutter at every breakpoint. The home
   sets these on `.xl` (phone 24px; tablet a 40→96px ramp); desktop (≥961px) uses
   the `:root` base clamp(24px, 8.33vw, 120px), which the case page already
   inherits. Keep in sync with the `.xl` --gutter overrides further below. */
@media (max-width: 699.98px) { .cs-body { --gutter: 24px; } }
@media (min-width: 700px) and (max-width: 960px) {
  .cs-body { --gutter: clamp(40px, calc(40px + (100vw - 700px) * 0.2154), 96px); }
}

/* hero + sections: same centred stage as the home `.xl` (cap + gutter), so the
   side margins match the home hero exactly; full-bleed siblings stay full width */
.cs-hero,
.cs-section { max-width: var(--stage-max); margin-inline: auto; padding-inline: var(--gutter); }

/* ----- sticky top bar (monogram home link + Back to home) ----- */
.cs-topbar {
  position: sticky; top: 0; z-index: 20;
  background: var(--surface-default);
  border-bottom: 1px solid var(--border-default);
  transition: transform 0.3s ease;
}
/* Auto-hidden on phones while scrolling down (toggled by animations.js); slides
   back the instant the user scrolls up, reaches the top, or hits the bottom.
   Larger viewports never get this class — the bar is always visible there. */
.cs-topbar--hidden { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) { .cs-topbar { transition: none; } }
.cs-topbar__inner {
  max-width: var(--stage-max); margin-inline: auto;
  display: flex; align-items: center; gap: clamp(20px, 4vw, 32px);
  padding: 22px var(--gutter);
}
.cs-monogram {
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(24px, 3vw, 28px); color: var(--text-primary); text-decoration: none;
}
.cs-monogram:focus-visible { outline: 2px solid var(--accent-default); outline-offset: 4px; border-radius: 2px; }
/* Back-to-home — a plain text link (no border, no fill), accent-coloured, with a
   leading arrow that nudges left on hover. Padding is tap-target only (invisible). */
.cs-back {
  display: inline-flex; align-items: center; gap: 0.4em;
  font-family: var(--font-body); font-weight: 700; font-size: 16px; line-height: 1;
  color: var(--text-accent); text-decoration: none; white-space: nowrap;
  padding: 6px 8px;
}
.cs-back__arrow { display: inline-block; transition: transform 0.2s ease; }
.cs-back:focus-visible { outline: 2px solid var(--accent-default); outline-offset: 4px; border-radius: 4px; }
@media (hover: hover) and (pointer: fine) {
  .cs-back:hover .cs-back__arrow { transform: translateX(-3px); }
}

/* ----- hero ----- */
.cs-hero {
  display: flex; flex-direction: column; gap: clamp(36px, 4.4vw, 64px);
  padding-top: clamp(56px, 8.3vw, 120px); padding-bottom: clamp(28px, 4vw, 40px);
}
.cs-hero__header { display: flex; flex-direction: column; gap: clamp(18px, 2.2vw, 32px); }
.cs-hero__client {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(28px, 3.06vw, 44px); line-height: 1.205; letter-spacing: -0.005em;
  color: var(--text-primary);
}
.cs-hero__title {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(40px, 5.56vw, 80px); line-height: 1.1; letter-spacing: -0.01em;
  color: var(--text-primary);
}
.cs-hero__content { display: flex; flex-direction: column; gap: clamp(32px, 5.5vw, 80px); }
.cs-hero__media {
  width: 100%; height: clamp(240px, 25vw, 360px);
  background: #f5f5f5; border-radius: 24px;     /* Figma hero placeholder (lighter than the section placeholders) */
}
.cs-panel { display: flex; flex-direction: column; gap: clamp(24px, 2.8vw, 40px); }
.cs-panel__title {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(24px, 2.22vw, 32px); line-height: 1.1875; letter-spacing: -0.0025em;
  color: var(--text-primary);
}
.cs-panel__list {
  list-style: none; display: flex; flex-direction: column; gap: 20px;
  font-family: var(--font-body); font-weight: 400;
  font-size: clamp(16px, 1.25vw, 18px); line-height: 1.611; color: var(--text-secondary);
}
.cs-panel__list li { position: relative; padding-left: 27px; }
.cs-panel__list li::before { content: "•"; position: absolute; left: 8px; top: -0.02em; }

@media (min-width: 961px) {
  .cs-hero__content { flex-direction: row; align-items: center; }
  .cs-hero__media { flex: 1 1 0; min-width: 0; height: clamp(320px, 25vw, 360px); }
  .cs-panel { flex: 1 1 0; min-width: 0; }
}

/* ----- full-bleed hairline divider (40px above + below the line) ----- */
.cs-divider { height: 1px; border: 0; background: var(--border-default); margin-block: clamp(28px, 4vw, 40px); }

/* ----- generic section ----- */
.cs-section {
  display: flex; flex-direction: column; gap: clamp(28px, 3.3vw, 40px);
  padding-block: clamp(28px, 4vw, 40px);
}
.cs-section:last-child { padding-bottom: clamp(64px, 10vw, 120px); }   /* breathing room at the page foot */
.cs-section--outcome { padding-top: clamp(48px, 6.5vw, 80px); }        /* follows the full-bleed image (no divider) */
.cs-section__head { display: flex; flex-direction: column; gap: 16px; }
.cs-section__title {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(28px, 3.06vw, 44px); line-height: 1.205; letter-spacing: -0.005em;
  color: var(--text-primary);
}

/* eyebrow — numbered pill + label, both accent uppercase */
.cs-eyebrow { display: flex; align-items: center; gap: 12px; }
.cs-eyebrow__num {
  display: inline-flex; align-items: center; justify-content: center;
  height: 30px; padding-inline: 8px; border: 1px solid var(--border-default); border-radius: 999px;
  font-family: var(--font-body); font-weight: 700; font-size: 13px; line-height: 1;
  letter-spacing: 0.12em; color: var(--text-accent);
}
.cs-eyebrow__label {
  font-family: var(--font-body); font-weight: 700; font-size: 13px; line-height: 1.385;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-accent);
}

/* body prose — blank-line spacing between paragraphs matches the Figma */
.cs-prose {
  font-family: var(--font-body); font-weight: 400;
  font-size: clamp(16px, 1.25vw, 18px); line-height: 1.611; color: var(--text-secondary);
  max-width: 720px;
}
.cs-prose p + p { margin-top: 1.611em; }

/* stacked content (text then full-width figure) */
.cs-stack { display: flex; flex-direction: column; gap: clamp(28px, 3.3vw, 40px); }

/* split content (text beside a supporting image) */
.cs-split { display: flex; flex-direction: column; gap: clamp(32px, 4vw, 40px); }
.cs-split__media {
  width: 100%; min-height: clamp(240px, 56vw, 435px);
  background: var(--surface-placeholder); border-radius: 24px;
}
@media (min-width: 961px) {
  .cs-split { flex-direction: row; align-items: stretch; gap: clamp(48px, 5.5vw, 80px); }
  .cs-split__text { flex: 543 1 0; max-width: 543px; }
  .cs-split__media { flex: 577 1 0; min-height: 435px; }
}

/* full-width figures (define / align) */
.cs-figure { width: 100%; background: var(--surface-placeholder); border-radius: 24px; }
.cs-figure--define { height: clamp(240px, 35.5vw, 512px); }
.cs-figure--align  { height: clamp(280px, 40.3vw, 580px); }

/* full-bleed image — edge to edge, no radius */
.cs-fullbleed { width: 100%; height: clamp(300px, 41vw, 590px); background: var(--surface-placeholder); }

/* outcome stats — content-width blocks spread edge-to-edge (space-between), so the
   first sits flush against the container's left edge and the last against the right
   (Figma). Stack centred on phones. */
.cs-stats { display: flex; justify-content: space-between; align-items: flex-start; padding-block: 24px; text-align: center; }
.cs-stat { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.cs-stat__num {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(52px, 5.56vw, 80px); line-height: 1.1; letter-spacing: -0.01em; color: var(--text-accent);
}
.cs-stat__label { font-family: var(--font-body); font-weight: 400; font-size: 15px; line-height: 1.533; color: var(--text-secondary); }
@media (max-width: 699.98px) { .cs-stats { flex-direction: column; align-items: center; gap: 32px; } }

/* more-work tiles — image-topped cards, two-up on tablet+ */
.cs-tiles { display: flex; flex-direction: column; gap: clamp(20px, 3vw, 36px); }
.cs-tile {
  display: flex; flex-direction: column; overflow: clip; text-decoration: none; color: inherit;
  background: var(--surface-default); border: 1px solid var(--border-default);
  border-radius: clamp(28px, 3vw, 40px);
}
.cs-tile:focus-visible { outline: 2px solid var(--accent-default); outline-offset: 4px; }
.cs-tile__media { width: 100%; height: clamp(200px, 20vw, 281px); background: var(--surface-placeholder); }
.cs-tile__body { display: flex; flex-direction: column; gap: clamp(20px, 2.2vw, 32px); padding: 32px 32px 48px; }
.cs-tile__titles { display: flex; flex-direction: column; gap: 8px; }
.cs-tile__client {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(20px, 1.67vw, 24px); line-height: 1.25; letter-spacing: -0.0025em; color: var(--text-primary);
}
.cs-tile__name {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(24px, 2.22vw, 32px); line-height: 1.1875; letter-spacing: -0.0025em; color: var(--text-primary);
}
.cs-tile__link { display: inline-flex; align-items: center; gap: 0.4em; font-family: var(--font-body); font-weight: 700; font-size: 16px; line-height: 1; color: var(--text-accent); }
.cs-tile__arrow { transition: transform 0.2s ease; }
@media (min-width: 700px) {
  .cs-tiles { flex-direction: row; }
  .cs-tile { flex: 1 1 0; min-width: 0; }
}
/* clickable-card affordance (mirrors the site's hover language; hover-fine only) */
@media (hover: hover) and (pointer: fine) {
  .cs-tile { transition: transform 0.25s ease, box-shadow 0.25s ease; }
  .cs-tile:hover { transform: translateY(-4px); box-shadow: 0 18px 40px -18px rgba(37, 38, 38, 0.22); }
  .cs-tile:hover .cs-tile__arrow { transform: translateX(4px); }
}

/* ============================================================
   PAGE-LOAD REVEAL (GSAP) — experiment, branch hero-flyin-reveal.
   Pre-hide animated elements, and ONLY when JS is active (`html.js`).
   With JS off, nothing here matches → everything renders normally.
   animations.js reveals them; the reduced-motion block is the CSS
   safety net (logos settle dimmed at .62, not full opacity).
   ============================================================ */
html.js .monogram,
html.js .h1,
html.js .role,
html.js .body,
html.js .button,
html.js .logo-ticker,
html.js .hero-image { opacity: 0; }

html.js .monogram,
html.js .h1,
html.js .role,
html.js .body,
html.js .button,
html.js .logo-ticker,
html.js .hero-image { will-change: transform, opacity; }

@media (prefers-reduced-motion: reduce) {
  /* .monogram stays hidden under JS — it's shown only in the sticky scroll bar */
  html.js .h1,
  html.js .role,
  html.js .body,
  html.js .button,
  html.js .logo-ticker,
  html.js .hero-image { opacity: 1; }
  /* no marquee under reduced motion → wrap so every logo stays visible */
  html.js .logos { flex-wrap: wrap; justify-content: center; }
  html.js .monogram,
  html.js .h1,
  html.js .role,
  html.js .body,
  html.js .button,
  html.js .logo-ticker,
  html.js .hero-image { will-change: auto; }
}

/* Case study page pre-hide (only that page has .cs-reveal). buildCasePage
   reveals these — the hero/top-bar on load, each section as it scrolls in.
   Reduced motion / no-JS leave everything visible. */
html.js .cs-reveal { opacity: 0; will-change: transform, opacity; }
@media (prefers-reduced-motion: reduce) {
  html.js .cs-reveal { opacity: 1; will-change: auto; }
}

/* ============================================================
   OV-pas case — "Minimap" interaction (built by ov-minimap.js).
   The chart (left) + a minimap & iPhone mockup (right) scroll in
   lockstep on one rounded canvas. Ported from the interaction lab;
   the lab stays the tuning workshop, this is the production styling.
   ============================================================ */
.ov-minimap { width: 100%; }

/* caption + route-dot row */
.ov__caption { text-align: center; font-size: 14px; color: var(--text-secondary); min-height: 2.6em; max-width: 34ch; }
.ov__caption--left { text-align: left; max-width: none; min-height: 0; }
.ov__caption strong { color: var(--text-accent); }

/* the chart's clipping frame (a fixed camera pans/zooms inside it) */
.ov-viewport {
  position: relative; width: 100%; height: clamp(360px, 52vh, 560px);
  border: 1px solid var(--border-default); border-radius: 20px; overflow: hidden;
  background:
    radial-gradient(circle at 1px 1px, rgba(37, 38, 38, 0.06) 1px, transparent 0) 0 0 / 26px 26px,
    var(--surface-raised);
}
.ov-canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; will-change: transform; }
/* end cards sit flush at the SVG's right edge, so their accent outline overhangs
   the viewBox — let it paint (the .ov-viewport frame still clips the panned chart) */
.ov-minimap .ov-canvas svg { overflow: visible; }

/* play/pause button + route dots */
.ov-btn {
  appearance: none; border: 1px solid var(--border-default); background: var(--surface-default);
  width: 38px; height: 38px; border-radius: 12px; font-size: 18px; cursor: pointer; color: var(--text-primary);
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
  box-shadow: 0 4px 12px -6px rgba(37, 38, 38, 0.3);
}
.ov-btn:hover { border-color: var(--accent-default); color: var(--text-accent); }
.ov-tourbar { position: absolute; left: 12px; bottom: 12px; display: flex; gap: 8px; align-items: center; z-index: 5; }
.ov-pip {
  width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--border-default);
  background: var(--surface-default); cursor: pointer; padding: 0;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.ov-pip.is-active { background: var(--accent-default); border-color: var(--accent-default); }

/* layout: chart left, minimap + phone right, on one rounded canvas */
.ns-wrap { display: flex; flex-direction: column; gap: 12px; }
.ns-main { display: flex; gap: 24px; align-items: stretch; }
.ns-main .ov-viewport { flex: 1 1 auto; min-width: 0; }
.ov-viewport--tall { height: clamp(440px, 66vh, 660px); }
.ns-main--mm {
  align-items: stretch;
  height: clamp(600px, 62vw, 780px); min-height: 480px; max-height: 828px;
  /* ^ the cap keeps the phone screen ≤360 CSS px so its @2x form screenshot
     never upsizes past the raster's resolution (no pixelation on hi-dpi). */
  padding: 24px; border: 1px solid var(--border-default); border-radius: 26px;
  background:
    radial-gradient(circle at 1px 1px, rgba(37, 38, 38, 0.06) 1px, transparent 0) 0 0 / 26px 26px,
    var(--surface-raised);
}
/* the diagram floats directly on the canvas — no frame of its own (still clipped) */
.ns-main--mm .ov-viewport--tall {
  flex: 1 1 46%; height: auto; margin: 0;
  border: 0; border-radius: 0; background: transparent;
}
/* filled rounded backdrop the minimap + phone sit on */
.ns-main--mm .ns-mmframe {
  flex: 1 1 54%; height: auto;
  display: flex; align-items: center; justify-content: center; gap: 48px; padding: 24px;
  border: 0; border-radius: 20px;
  background: color-mix(in srgb, var(--surface-placeholder) 42%, var(--surface-raised));
}
/* minimap box hugs the UI (no letterbox); clip-path also clips the composited
   .ns-mmimg (will-change), which overflow + border-radius alone would not */
.ns-minimap {
  position: relative; flex: 0 0 auto; align-self: center; overflow: hidden;
  border-radius: 8px; clip-path: inset(0 round 8px);
}
.ns-mmimg { position: absolute; left: 0; top: 50%; display: block; height: auto; will-change: transform, opacity, filter; }
/* viewport rect; its spread box-shadow dims outside it, following the rounded
   corners (clipped to the minimap so the repaint stays cheap) */
.ns-mmrect {
  position: absolute; top: 0; left: 0; width: 40px; height: 40px;
  border: 2px solid var(--accent-default); border-radius: 6px;
  pointer-events: none; z-index: 3;
  box-shadow: 0 0 0 600px rgba(17, 18, 24, 0.22);
}
/* iPhone 17 mockup: the screenshot shows through the PNG frame's transparent cut-out */
.ns-phone { flex: 0 0 auto; height: 100%; aspect-ratio: 1350 / 2760; max-height: 574px; position: relative; }
.ns-phone__body { position: absolute; inset: 0; z-index: 1; will-change: transform; }
.ns-phone__shadow {
  position: absolute; z-index: 0; pointer-events: none;
  left: 10%; right: 10%; bottom: -5.5%; height: 5.5%; border-radius: 50%;
  background: radial-gradient(ellipse at center,
    rgba(17, 18, 24, 0.30) 0%, rgba(17, 18, 24, 0.15) 46%, rgba(17, 18, 24, 0) 72%);
  filter: blur(10px); will-change: transform, opacity;
}
.ns-phone__frame {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  pointer-events: none; z-index: 2;
  filter: drop-shadow(0 9px 20px rgba(17, 18, 24, 0.14));
}
.ns-phone__screen {
  position: absolute; z-index: 1; overflow: hidden; background: #fff;
  top: 2.9%; bottom: 2.9%; left: 5.33%; right: 5.33%;
  border-radius: 16.9% / 7.84%;
}
.ns-phimg { position: absolute; top: 0; left: 0; width: 100%; height: auto; will-change: transform; }
.ns-caprow { display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap; }
.ns-caprow--dotsonly .ov-pip.is-active { transform: scale(1.25); }
.ns-pips { display: flex; gap: 8px; }

/* chart route highlights: accent clones of the chart's own nodes + arrows */
.ns-arrow {
  fill: var(--accent-default); stroke: var(--accent-default); stroke-width: 2;
  vector-effect: non-scaling-stroke; pointer-events: none;
  filter: drop-shadow(0 0 4px rgba(69, 73, 231, 0.35));
}
.ns-spot { fill: none; stroke: var(--accent-default); stroke-width: 3.5; vector-effect: non-scaling-stroke; pointer-events: none; }

@media (max-width: 760px) {
  .ns-main { flex-direction: column; }
  .ns-main--mm { height: auto; max-height: none; }
  .ns-main--mm .ov-viewport--tall { flex: 0 0 auto; height: 50vh; margin: 0; }
  .ns-main--mm .ns-mmframe { flex: 0 0 auto; height: 60vh; }
}
