/* =========================================================
   PORTFOLIO — STYLE
   Dark editorial portfolio. Inter (body/UI) + Space Grotesk (display).
   All content is placeholder; layout/proportions are final.
   ========================================================= */

/* ---------- Design tokens ---------- */
:root {
  --bg:        #0a0a0a;   /* dark overall background */
  --bg-soft:   #1a1a1a;   /* warm dark — media placeholders */
  --text:      #f0ede8;   /* off-white */
  --accent:    #c8b89a;   /* warm muted — highlights, tags, hover */

  /* Hero accent — an elegant orange, scoped to the hero only (CTAs, hover,
     the silk's warm threads). Kept separate from --accent so the FMIS
     warm-gold palette and the city-scrub title are left untouched. */
  --hero-accent:       #ff7800;
  --hero-accent-hover: #ff8c24;
  --hero-accent-ink:   #1a1102;   /* dark text that sits on the bright orange */

  --text-dim:  rgba(240, 237, 232, 0.55);
  --text-faint:rgba(240, 237, 232, 0.32);
  --rule:      rgba(240, 237, 232, 0.10);

  /* One family across the page — Bricolage Grotesque for both body and display;
     hierarchy comes from weight + size, not a second face. */
  --font-body: 'Bricolage Grotesque', system-ui, sans-serif;
  --font-disp: 'Bricolage Grotesque', system-ui, sans-serif;

  /* Horizontal rhythm: generous gutters that breathe */
  --gutter: clamp(1.5rem, 6vw, 7rem);
  --maxw:   1600px;
}

/* ---------- Reset ---------- */
*,
*::before,
*::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, h3 {
  margin: 0;
  font-family: var(--font-disp);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.02em;
}

p { margin: 0; }

a { color: inherit; text-decoration: none; }

/* Keyboard focus — a visible worklight ring on the dark page. Mouse / touch
   activations stay ring-free via :focus-visible; keyboard users always get it. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

img { display: block; max-width: 100%; }

/* ---------- Thin full-width section rules ---------- */
.rule {
  border: 0;
  height: 1px;
  margin: 0;
  background: var(--rule);
}

/* No divider into the FMIS product section — the city scrub hands straight off
   into it, so the two dark sections meet without a line. */
.rule:has(+ .fmis) { display: none; }

/* =========================================================
   AMBIENT BACKGROUND — drifting haze + film grain
   Fixed layer behind all content. The .bg sits as a negative
   z-index child so it paints above the body base colour but
   below every section. Cloud transforms are driven in main.js
   (slow ambient drift + scroll parallax). Decorative only.
   ========================================================= */
.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;          /* clip the oversized, moving clouds */
  pointer-events: none;
  background: transparent;    /* body provides the #0a0a0a base */
}

.bg__cloud {
  position: absolute;
  inset: -30%;               /* oversized so drift never reveals an edge */
  will-change: transform;
  /* default (reduced-motion / no-JS) resting scale */
  transform: scale(1.1);
}

/* Warm haze — leans into the accent tone */
.bg__cloud--warm {
  background:
    radial-gradient(38vw 34vw at 22% 24%, rgba(200, 184, 154, 0.060), transparent 60%),
    radial-gradient(32vw 30vw at 78% 72%, rgba(200, 184, 154, 0.042), transparent 62%);
}

/* Cool haze + a faint off-white lift for "lighter than background" texture */
.bg__cloud--cool {
  background:
    radial-gradient(42vw 38vw at 80% 28%, rgba(120, 140, 165, 0.050), transparent 60%),
    radial-gradient(36vw 34vw at 28% 82%, rgba(245, 237, 232, 0.022), transparent 64%);
}

/* Very subtle film grain so dark areas read as textured, not flat */
.bg__grain {
  position: absolute;
  inset: 0;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) {
  .bg__cloud { transform: scale(1.1) !important; }
}

/* Lite: the cloud loop never starts (js/main.js), so these layers are static.
   Drop the compositing promotion with it — will-change on two layers ~1.6x the
   viewport reserves GPU memory and forces them onto their own composited
   surfaces permanently, which is only worth paying for while they move. */
.lite .bg__cloud {
  will-change: auto;
  transform: scale(1.1);
}

/* =========================================================
   SCROLL REVEALS — replaces AOS
   Scoped to html.js (set by an inline script in <head>) so the hidden state
   can only ever exist where JS is running to undo it. Without JS the
   selector never matches and every element renders normally — the failure
   mode AOS had, where a stalled CDN left whole sections invisible, is
   structurally impossible here.
   Opacity + transform only: the element keeps its box, stays in the
   accessibility tree, and remains findable by in-page search before reveal.
   ========================================================= */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 0.8s cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: opacity, transform;
}

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Reduced motion: no travel, no fade-in delay — content is simply present.
   main.js also reveals everything up front in this case, so this is belt
   and braces against a flash of hidden content before the JS runs. */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* =========================================================
   NAVIGATION — fixed, transparent until .scrolled
   ========================================================= */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 0.4s ease, border-color 0.4s ease, backdrop-filter 0.4s ease;
}

.nav.scrolled {
  background: rgba(10, 10, 10, 0.72);
  border-bottom: 1px solid var(--rule);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

/* A backdrop-filter is a full-width read-back-and-blur of everything painted
   beneath the bar, redone on every frame the content under it moves. .scrolled
   is on within 40px of the top, so on any real page position that is always —
   including for the entire length of the city scrub, where it blurs a canvas
   that is being repainted from a video decode. It is the single most expensive
   always-on effect on the page and it costs GPU the scrub needs.

   Lite devices get an opaque bar instead: same silhouette, same contrast for
   the links, no read-back. */
.lite .nav.scrolled {
  background: rgba(10, 10, 10, 0.94);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.nav__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 1.5rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
}

.nav__wordmark {
  font-family: var(--font-disp);
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}

.nav__links {
  display: flex;
  gap: clamp(1.5rem, 3vw, 3rem);
}

.nav__links a {
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-dim);
  transition: color 0.25s ease;
}

.nav__links a:hover { color: var(--accent); }

/* =========================================================
   1. HERO — full viewport, typography only
   ========================================================= */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Top clears the fixed nav. The bottom accents are in flow (see .hero__footer),
     so the bottom only carries their breathing room, not a hand-guessed reserve. */
  padding: 7rem var(--gutter) clamp(1.75rem, 4vh, 2.5rem);
  max-width: var(--maxw);
  margin: 0 auto;
}

/* ---------- Hero background — animated silk field ----------
   Ported from a React/canvas component to vanilla JS. A flowing interference
   pattern is painted to <canvas.hero__silk> in js/main.js — into a small,
   downscaled buffer that is stretched to fill the layer under a soft CSS blur,
   so the per-pixel loop stays cheap enough to run full-bleed. `.hero__bg` is a
   full-bleed layer pinned to the hero that extends past its bottom and fades
   out (mask) so it bleeds into the top of the FMIS section below. It sits at
   z-index:-1 — above the ambient haze, behind the hero copy (z-index:1). JS
   scrubs its opacity to 0 as the hero scrolls away, and only runs the live loop
   on wide, motion-allowed viewports (phones / reduced-motion get one static
   frame). */
.hero__bg {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);   /* full-bleed regardless of the hero's max-width */
  width: 100vw;
  height: 140vh;                 /* extends well past the hero so its lower edge
                                    never scrolls into view as a line; JS fades
                                    the whole layer out on scroll */
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  opacity: 0.7;                  /* JS scrubs this to 0 as the hero scrolls away */
  background: #0a0a0a;           /* base under the silk before the canvas paints */
  /* Soft bottom dissolve for the resting state; the scroll-driven opacity fade
     (js/main.js) is what actually clears the layer, before its edge could show. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 66%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 66%, transparent 100%);
}

/* The silk canvas. The backing store is a fraction of display size (set in JS)
   and stretched to fill here; the blur hides the upscale and gives the threads
   their soft, silky falloff. Slightly over-sized so the blur never reveals an
   edge. */
.hero__silk {
  position: absolute;
  inset: -3%;
  width: 106%;
  height: 106%;
  filter: blur(8px) saturate(1.15);
}

/* Vignette + top/bottom darkening so the white headline keeps full contrast
   over the busiest part of the silk. */
.hero__bg-overlay {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(125% 100% at 50% 42%, transparent 0%, rgba(10,10,10,0.22) 70%, rgba(10,10,10,0.55) 100%),
    linear-gradient(to bottom, rgba(10,10,10,0.45) 0%, transparent 28%, transparent 62%, rgba(10,10,10,0.85) 100%);
}

/* Wide enough for the two-line headline to each sit on one line; the statement
   below keeps its own narrow, readable measure. */
/* margin-block:auto centers the copy in whatever space is left ABOVE the bottom
   accents, instead of in the whole hero box. Equal auto margins split the free
   space, so the block reads as centered when there is room and simply stops
   short of the accents when there is not — it can never sit on top of them. */
.hero__content { position: relative; z-index: 1; max-width: 64rem; margin-block: auto; }

/* The primary, large hero statement — held WHITE (not the warm headline tone
   used elsewhere), per the brief. */
.hero__lead {
  /* Sized by width AND height. On a short viewport (laptops at 1536x864 and
     below) the width-only size wrapped this into three ~100px lines, which is
     most of the screen before the statement, CTA and accents even start. The
     11vh term only ever binds when the screen is short — on tall displays the
     7rem cap still wins, so nothing changes there. */
  font-size: clamp(2.6rem, min(6.6vw, 11vh), 7rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1.0;
  color: var(--text);
  max-width: 20ch;
}

.hero__lead-sep { color: var(--hero-accent); font-weight: 500; }

/* Statement — one sentence, medium weight, held WHITE so it reads cleanly over
   the silk (the warm field washed out the old dim grey). */
.hero__statement {
  margin-top: 1.9rem;
  max-width: 52ch;
  font-size: clamp(1rem, 1.45vw, 1.3rem);
  line-height: 1.5;
  font-weight: 500;
  color: var(--text);
}

/* Keeps a hyphenated compound ("real-world") whole — browsers otherwise treat the
   hyphen as a break opportunity and split it across two lines. */
.nowrap { white-space: nowrap; }

/* Credential line — a step down in size for the reader who slows down, but kept
   near-white so it stays legible on the dark field. */
.hero__credentials {
  margin-top: clamp(1rem, 3vh, 2rem);
  max-width: 52ch;
  font-size: clamp(0.85rem, 1.05vw, 0.95rem);
  line-height: 1.55;
  color: rgba(240, 237, 232, 0.82);
}

/* Role line — smallest, and the one element held a touch dimmer (answers "what
   am I looking for"). Still clears legibility, just recedes in the hierarchy. */
.hero__role {
  margin-top: 0.6rem;
  max-width: 52ch;
  font-size: clamp(0.8rem, 0.95vw, 0.875rem);
  line-height: 1.55;
  color: rgba(240, 237, 232, 0.62);
}

/* CTA row — primary filled in the hero accent, secondary an opaque ghost. */
.hero__cta {
  margin-top: clamp(2rem, 4vh, 2.75rem);
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
}

.hero__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1.6rem;
  border-radius: 999px;
  font-family: var(--font-disp);
  font-size: clamp(0.95rem, 1.1vw, 1.05rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1;
  border: 1px solid transparent;
  transition: background-color 0.25s ease, color 0.25s ease,
              border-color 0.25s ease, transform 0.25s ease;
}

.hero__btn--primary {
  background: var(--hero-accent);
  color: var(--hero-accent-ink);
}
.hero__btn--primary:hover {
  background: var(--hero-accent-hover);
  transform: translateY(-2px);
}

.hero__btn--ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--rule);
}
.hero__btn--ghost:hover {
  border-color: var(--hero-accent);
  color: var(--hero-accent);
  transform: translateY(-2px);
}

.hero__btn-icon { font-size: 1.05em; line-height: 1; }

/* Bottom accents — skill-tag pills + a quiet location line under a hairline
   rule, sitting at the foot of the hero.
   In flow, not absolutely positioned: pinning them meant the hero had to
   reserve their height as bottom padding, and that guess was 20px short of
   what they actually occupy, so on shorter viewports (1536x864 and down) the
   centred copy grew straight through them and the CTA row landed on top of
   "Targeting junior roles in". Keeping them in flow makes the reserve exact
   by construction — the hero grows instead of overlapping. */
.hero__footer {
  position: relative;
  z-index: 1;
  margin-top: clamp(1.75rem, 4vh, 3rem);   /* floor on the gap when space is tight */
}

/* Small eyebrow that frames the pills as the roles being sought. */
.hero__seeking {
  margin: 0 0 0.75rem;
  font-size: clamp(0.78rem, 0.95vw, 0.85rem);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--hero-accent);
  padding-left: 0.3em;
}

.hero__tags {
  list-style: none;
  margin: 0 0 1.1rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.hero__tags li {
  padding: 0.55rem 1.1rem;
  border: 1px solid rgba(240, 237, 232, 0.28);
  border-radius: 999px;
  background: rgba(240, 237, 232, 0.05);
  font-size: clamp(0.85rem, 1.05vw, 0.95rem);
  font-weight: 500;
  color: rgba(240, 237, 232, 0.96);
}

.hero__location {
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
  font-size: clamp(0.9rem, 1.1vw, 1rem);
  color: rgba(240, 237, 232, 0.55);
}

@media (max-width: 640px) {
  .hero__credentials,
  .hero__role { font-size: 13px; }
  .hero__location { font-size: 13px; }
  /* On phones the whole hero — headline through "Open to remote work" — is tuned
     to land inside the first screen (390x844 is the reference), so nothing about
     the pitch needs a scroll to be seen. Type and rhythm are stepped down here
     rather than letting the fluid clamps run at their floor, which overflowed by
     roughly a hundred pixels. The hero still only fills the screen (never
     exceeds it): min-height + the auto margins on .hero__content push the
     accents to the foot, and the block grows past 100svh if a narrower phone
     wraps a line more. */
  .hero {
    justify-content: flex-start;
    min-height: 100svh;
    padding-top: 5.25rem;
    padding-bottom: clamp(1.25rem, 3vh, 2rem);
  }
  /* pretty keeps the second sentence off a lone "it." on the last line */
  .hero__lead { font-size: 2.15rem; line-height: 1.05; text-wrap: pretty; }
  .hero__statement {
    margin-top: 1.15rem;
    font-size: 14.5px;
    line-height: 1.5;
  }
  .hero__credentials { margin-top: 0.85rem; line-height: 1.5; }
  .hero__cta { margin-top: 1.5rem; }
  .hero__btn { padding: 0.7rem 1.35rem; font-size: 0.9rem; }
  .hero__footer { margin-top: 1.6rem; }
  .hero__seeking { margin-bottom: 0.6rem; }
  .hero__tags { gap: 0.45rem; margin-bottom: 0.85rem; }
  .hero__tags li { padding: 0.4rem 0.85rem; font-size: 12.5px; }
  .hero__location { padding-top: 0.85rem; }
}

/* Scroll indicator — bottom-right corner */
.hero__scroll {
  position: absolute;
  right: var(--gutter);
  bottom: 2.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
}

.hero__scroll-label {
  font-size: 0.7rem;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: var(--text-faint);
  writing-mode: vertical-rl;
}

.hero__scroll-line {
  width: 1px;
  height: 56px;
  background: linear-gradient(to bottom, var(--accent), transparent);
  animation: scrollPulse 2.2s ease-in-out infinite;
  transform-origin: top;
}

@keyframes scrollPulse {
  0%, 100% { transform: scaleY(0.4); opacity: 0.4; }
  50%      { transform: scaleY(1);   opacity: 1; }
}

/* =========================================================
   CITY — scroll-scrubbed aerial cityscape (hero → FMIS bridge)
   A pinned full-viewport <canvas> paints one frame of the City clip per scroll
   step (main.js). The overlay tints the bright footage down to a night key; a
   scroll-driven black layer (.cityscroll__fade) dissolves the clip UP out of the
   hero dark and back DOWN to black into FMIS, so both seams disappear. Mobile /
   reduced motion / no-JS rest on the static darkened poster (no canvas, no pin).
   ========================================================= */
.cityscroll {
  position: relative;
  min-height: 100vh;
  background: #0a0a0a;
}

.cityscroll__viewport {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  /* The poster still lives on ::before (see below) so it can be blurred without
     dragging the title and ring with it. This element only carries the base
     colour that shows through the blur at the edges. */
  background: #0a0a0a;
}

/* Opacity, not display: the canvas has to be able to fade in over the poster
   once a tier has loaded and painted, and `display` cannot be transitioned. */
.cityscroll__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.5s ease;
}

.cityscroll.is-scrub .cityscroll__canvas { opacity: 1; }

/* There was a sharp-still overlay layer here: on scroll-stop it faded a 2560px
   still of the current frame over the canvas. It was measured and removed.

   It existed because the motion layer was 1280 and visibly soft at rest. Now
   that the motion layer is 1920, a still buys about 10% on Laplacian variance
   (2229 vs ~2446 at display resolution) — below the point the eye resolves,
   and the user had already reported the previous, larger 1.5x step as "barely
   noticeable". Against that it cost 27MB of assets, ~47MB of resident bitmaps,
   and a large amount of state.

   Worse, it re-introduced the very artefact this section was being fixed for:
   a sharpness difference between scrolling and resting, i.e. an oscillation
   every time the user started and stopped. Uniform beats occasionally-crisper.
   Do not reinstate it without measuring the gap against the motion layer. */

/* While only the low-resolution proxy is available, the picture is blurred.
   At 480px stretched across a viewport it would otherwise look broken; blurred,
   it reads as depth of field behind the section title — a deliberate treatment
   rather than a degraded one. When the full-resolution file lands it is swapped
   in at the identical frame and this class comes off, so the picture appears to
   resolve into focus rather than cut.

   The blur must hit ONLY the picture layers. The poster therefore lives on a
   ::before rather than on the viewport's own background — blurring the viewport
   would drag the section title and the progress ring with it. ::before paints
   as the first child, so the stack stays poster → canvas → overlay → fade →
   ring → title.

   No scale/inset trick is needed to hide the blurred canvas edges: what shows
   through underneath is the poster, which is the same scene. */
/* The poster is the SLOT 0 frame at 1920 — the exact picture the scrub shows on
   entry, not a decorative approximation. It is a CSS background, so it is
   fetched by the preload scanner and is on screen in ~300ms, long before any
   video has downloaded, let alone baked.

   That makes it the first sharp fullscreen frame, and it is measurably the
   sharpest single frame in the whole pipeline (Laplacian variance 2446 at
   display resolution, against 1336 for a baked 1280 video frame). The scrub
   canvas is therefore revealed on top of it silently at slot 0 — MAE 5.72
   between the two, which is the noise floor, so the handover is invisible.

   The previous poster was a mid-clip frame (MAE 34.47 against slot 0). It had
   to be blurred to hide that it was the wrong picture. Do not swap this file
   for "a nicer looking frame" — it must stay frame-exact with slot 0. */
.cityscroll__viewport::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../assets/images/city-poster-v2.webp") center / cover no-repeat;
}

/* The blur is applied ONLY to the poster, never to the canvas.

   A CSS filter has to be re-run every time the layer it sits on repaints. The
   canvas repaints on every scroll step, and the full-resolution file takes tens
   of seconds to arrive on an ordinary connection — so blurring the canvas meant
   re-running a full-viewport gaussian on every scroll step for that whole time.
   That is what made scrolling crawl, and why the blur looked permanent: it was
   not stuck, just on far longer than it was designed for.

   The poster is static, so its blur is composited once and cached — free. The
   proxy's softness is baked into the encode instead (gblur at 480px), so the
   canvas needs no filter at all, and the focus-pull at the swap is done as an
   alpha cross-fade inside the canvas (see main.js). */
.cityscroll__viewport::before {
  transition: opacity 0.5s ease, filter 0.6s ease;
}

.cityscroll.is-softening .cityscroll__viewport::before {
  filter: blur(10px);
}

/* Full-resolution download progress — a hollow ring in the top-right. Sits
   below the fixed nav band (~4.5rem) so it never collides with the nav links. */
.cityscroll__ring {
  position: absolute;
  top: 5.5rem;
  right: var(--gutter);
  width: 22px;
  height: 22px;
  opacity: 0;
  transition: opacity 0.5s ease;
  pointer-events: none;
  z-index: 3;
}

.cityscroll__ring.is-visible { opacity: 1; }

.cityscroll__ring svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);   /* start the sweep at 12 o'clock */
}

.cityscroll__ring circle {
  fill: none;
  stroke-width: 1.5;
}

.cityscroll__ring-track { stroke: rgba(240, 237, 232, 0.18); }

.cityscroll__ring-fill {
  stroke: var(--accent);
  stroke-linecap: round;
  stroke-dasharray: 62.83;     /* 2*pi*r, r=10 */
  stroke-dashoffset: 62.83;    /* JS counts this down to 0 */
  transition: stroke-dashoffset 0.2s linear;
}

/* Footage treatment, light-handed: a top-weighted gradient does most of the
   work — the top stays clear so the bright skyline in the upper third pops,
   then it eases into a thin night scrim through the middle (where the title
   sits, kept legible), and darkens again at the bottom to hand off into the
   product section. A faint cool tint underneath settles the overall brightness
   + matches the page; it too lifts at the top so it doesn't mute the skyline.
   Always on, including the static poster. */
.cityscroll__overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(10, 10, 10, 0.20) 0%,
      rgba(10, 10, 10, 0.25) 18%,
      rgba(10, 10, 10, 0.35) 33%,
      rgba(10, 10, 10, 0.30) 48%,
      rgba(10, 10, 10, 0.32) 68%,
      rgba(10, 10, 10, 0.62) 87%,
      rgba(10, 10, 10, 0.95) 100%),
    linear-gradient(to bottom,
      rgba(12, 15, 24, 0.00) 0%,
      rgba(12, 15, 24, 0.10) 30%,
      rgba(12, 15, 24, 0.18) 60%,
      rgba(12, 15, 24, 0.18) 100%);
}

/* Scroll-driven black layer: 1 at the section ends (= the hero / FMIS dark),
   0 through the middle, so the cityscape dissolves up out of black and back
   into it. JS sets the opacity; in the static fallback it stays 0. */
.cityscroll__fade {
  position: absolute;
  inset: 0;
  background: #0a0a0a;
  opacity: 0;
  pointer-events: none;
}

/* Title moment: the full Fleet Management lockup, centred over the clip. It
   holds while the city scrubs and clears just before the black-out (opacity is
   driven in main.js). Reuses the FMIS intro type — kicker, gradient wordmark,
   lead and meta — so the brand lockup is identical to the rest of the work. */
.cityscroll__title {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.2rem, 3vh, 2.1rem);
  padding: clamp(2rem, 7vh, 5.5rem) var(--gutter);
  text-align: center;
  /* No will-change here. This element's opacity is set once, at load, and never
     animated (main.js pins it to 1 and the scroll-driven fade was removed), so
     the hint bought nothing — it just held a full-viewport layer permanently
     promoted directly on top of the scrubbing canvas. */
  /* Localised scrim — darkens only behind the centred lockup so the lead copy
     stays readable, while the footage at the edges keeps its (reduced) tint. */
  background: radial-gradient(58% 48% at 50% 50%,
    rgba(8, 9, 14, 0.52) 0%,
    rgba(8, 9, 14, 0.24) 54%,
    transparent 78%);
}

/* Sweep the wordmark's gradient once when the section first comes into view. */
@media (prefers-reduced-motion: no-preference) {
  .cityscroll__title.is-revealed .fmis__wordmark--hero {
    animation: fmis-gradient-sweep 1.7s cubic-bezier(0.22, 1, 0.36, 1) both;
  }
}

/* City-scrub title — warm-gold treatment. The shared FMIS wordmark renders here
   in worklight gold (filament → deep amber) instead of the blue→violet sweep, so
   the title matches the rest of the page; the lead + meta are lifted to full
   worklight white with a scrim so they stay readable over the bright footage.
   (background-size / -position come from .fmis__wordmark, so the reveal sweep
   still runs — now in gold.) */
.cityscroll__title .fmis__intro-lead {
  color: #f0ede8;
  text-shadow: 0 1px 3px rgba(10, 10, 10, 0.6), 0 2px 24px rgba(10, 10, 10, 0.92);
}
.cityscroll__title .fmis__intro-meta {
  color: rgba(240, 237, 232, 0.92);
  text-shadow: 0 1px 3px rgba(10, 10, 10, 0.6), 0 1px 16px rgba(10, 10, 10, 0.88);
}
.cityscroll__title .fmis__intro-meta li + li::before {
  background: linear-gradient(120deg, #ecd29a, #92601b);
}
.cityscroll__title .fmis__wordmark {
  background-image: linear-gradient(100deg, #ebcd8d 0%, #E4DFD9 50%, #ecd29a 100%);
  -webkit-text-fill-color: transparent;
  filter:
    brightness(1.05) saturate(1.12)
    drop-shadow(0 0 11px rgba(236, 210, 154, 0.275))
    drop-shadow(0 0 27px rgba(194, 144, 74, 0.2))
    drop-shadow(0 2px 26px rgba(10, 10, 10, 0.6));
}

/* Three stacked drop-shadows are three separate blur passes over the same
   clamp(...,5.7vw,5rem) text, re-composited over the canvas underneath as it
   scrubs. Lite collapses them to the one that does the real work — the dark
   halo that keeps the wordmark legible over bright footage. The gold glow is
   what is lost; the legibility is not. */
.lite .cityscroll__title .fmis__wordmark {
  filter:
    brightness(1.05) saturate(1.12)
    drop-shadow(0 2px 20px rgba(10, 10, 10, 0.72));
}

/* =========================================================
   SPLIT SECTIONS (FMIS, 3D & Motion) — 55/45 alternating
   ========================================================= */
/* 3D & Motion intro — 2×2 grid: three wide showcase windows plus the text
   block pinned to the top-right cell. Spans the full content width. */
.motion__intro {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: clamp(4rem, 9vw, 9rem) var(--gutter);
}

.motion__grid {
  display: grid;
  /* minmax(0, 1fr) keeps both columns exactly equal and lets them shrink —
     a plain 1fr floors at min-content and would overflow the viewport. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 2.2vw, 2rem);
  align-items: stretch;
}

/* DOM order (fight, text, M4, rain) auto-flows into TL, TR, BL, BR — the text
   block lands in the top-right cell with no explicit placement needed. */
.motion__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(0.5rem, 2vw, 2rem);
}

/* Eyebrow kicker above the title (mirrors the section-label treatment). */
.motion__kicker {
  font-family: var(--font-disp);
  font-size: 0.85rem;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 1.25rem;
}

/* Title is scaled to the grid cell (the shared .split__title clamp tops out
   at 5rem, far too large for this narrower column). */
.motion__text .split__title {
  font-size: clamp(1.85rem, 2.8vw, 2.9rem);
  max-width: 18ch;
}

/* Keep the description from filling the full cell width, so it reads as copy
   rather than competing with the media tiles for density. */
.motion__text .split__desc {
  max-width: 90%;
  /* Brightened to match the pipeline copy above (0.85), rather than the
     dimmer 0.55 body default — easier to read on near-black. */
  color: rgba(240, 237, 232, 0.85);
}

.split__title {
  font-size: clamp(2.5rem, 5vw, 5rem);
  font-weight: 600;
  max-width: 14ch;

  align-self: flex-start;
  margin-top: -1.78rem; /* tweak as needed */
}

.split__desc {
  margin-top: 1.75rem;
  max-width: 46ch;
  font-size: clamp(1rem, 1.2vw, 1.1rem);
  color: var(--text-dim);
}

/* Each tile = a 16:9 video window plus an always-visible caption below it. */
.motion__tile {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

/* Wide showcase window — 16:9, filling the cell width. The compound selector
   beats .media-placeholder's flex:1 / min-height:320px (defined later). It now
   carries the frame (border + warm dark fill) and is the positioning context
   for the loading skeleton stacked over the clip. */
.motion__tile .motion__window,
.motion__tile .motion__media {
  flex: none;
  aspect-ratio: 16 / 9;
  width: 100%;
  min-height: 0;
}

.motion__tile .motion__window {
  position: relative;
  overflow: hidden;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
}

/* Showcase videos: fill the 16:9 window; the poster frame carries the tile
   until JS starts playback in-view (or stays put under reduced motion). */
.motion__video {
  display: block;
  object-fit: cover;
  background: var(--bg-soft);
}

/* Inside a window the frame belongs to the wrapper, so the clip sits flush. */
.motion__window .motion__video {
  height: 100%;
  border: 0;
}

/* ---------- Loading skeleton (YouTube-style) ----------
   An opaque placeholder with a light sweep across it, raised over a tile whose
   clip has not buffered enough to run smoothly. Hidden by default and only
   raised by JS (.is-loading), so a tile that is already buffered when it
   scrolls into view simply plays — the skeleton never flashes. */
.motion__skeleton {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--bg-soft);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.32s ease, visibility 0s linear 0.32s;
}

.motion__window.is-loading .motion__skeleton {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.18s ease, visibility 0s;
}

/* The sweep. Animated only while loading so an idle tile costs nothing. */
.motion__skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
      rgba(240, 237, 232, 0) 20%,
      rgba(240, 237, 232, 0.055) 40%,
      rgba(240, 237, 232, 0.11) 50%,
      rgba(240, 237, 232, 0.055) 60%,
      rgba(240, 237, 232, 0) 80%);
  transform: translateX(-100%);
}

.motion__window.is-loading .motion__skeleton::after {
  animation: motion-skeleton-sweep 1.5s ease-in-out infinite;
}

@keyframes motion-skeleton-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
  .motion__window.is-loading .motion__skeleton::after { animation: none; }
}

/* Caption: a title that sets expectation + a quiet supporting line. */
.motion__cap {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.motion__cap-title {
  font-size: clamp(0.95rem, 1.1vw, 1.05rem);
  font-weight: 500;
  color: var(--text);
}

.motion__cap-note {
  max-width: 52ch;
  font-size: clamp(0.8rem, 0.95vw, 0.875rem);
  line-height: 1.5;
  color: rgba(240, 237, 232, 0.65);
}

/* =========================================================
   TAGS
   ========================================================= */
.tags {
  list-style: none;
  margin: 2.25rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem;
}

.tag {
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--accent);
  padding: 0.4rem 0.9rem;
  border: 1px solid rgba(200, 184, 154, 0.28);
  border-radius: 999px;
  transition: background 0.25s ease, border-color 0.25s ease;
}

.tag:hover {
  background: rgba(200, 184, 154, 0.08);
  border-color: rgba(200, 184, 154, 0.55);
}

/* =========================================================
   LINK WITH ARROW
   ========================================================= */
.link-arrow {
  margin-top: 2.5rem;
  align-self: flex-start;
  font-size: 0.95rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--text-faint);
  transition: color 0.25s ease, border-color 0.25s ease;
}

.link-arrow span { transition: transform 0.25s ease; display: inline-block; }
.link-arrow:hover { color: var(--accent); border-color: var(--accent); }
.link-arrow:hover span { transform: translateX(5px); }

/* =========================================================
   MEDIA PLACEHOLDERS — warm dark so layout reads correctly
   ========================================================= */
.media-placeholder {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 320px;
  padding: 2rem;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
  color: var(--text-faint);
  font-family: var(--font-disp);
  font-size: 0.95rem;
  letter-spacing: 0.04em;
}

.media-placeholder--wide {
  width: 100%;
  min-height: 420px;
  margin: 3.5rem 0;
}

/* Real media in placeholder slots — center-crop wider assets */
.media-frame {
  flex: 1;
  align-self: stretch;
  position: relative;
  min-height: 320px;
  overflow: hidden;
  border: 1px solid var(--rule);
  background: var(--bg-soft);
}

.media-frame__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 35% center;
}

/* =========================================================
   3. PIPELINE — full width, centered (NOT a split)
   ========================================================= */
.pipeline {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: clamp(5rem, 12vh, 11rem) var(--gutter);
  text-align: center;
}

.pipeline__inner {
  max-width: 1100px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.pipeline__title {
  font-size: clamp(2rem, 4.5vw, 4rem);
  font-weight: 600;
}

.pipeline__desc {
  max-width: 60ch;
  font-size: clamp(1.05rem, 1.3vw, 1.18rem);     /* ~17–19px */
  line-height: 1.6;
  color: rgba(240, 237, 232, 0.85);              /* lighter than the 0.55 dim — easier to read on near-black */
}

/* Impact line — quiet footnote below the description; subordinate to the
   main copy via smaller type, but still comfortably legible. */
.pipeline__impact {
  margin-top: 1rem;
  max-width: 60ch;
  font-size: clamp(0.9rem, 1.05vw, 0.95rem);     /* ~14–15px */
  line-height: 1.6;
  color: rgba(240, 237, 232, 0.72);
}

/* Looping pipeline animation — replaces the old wide media placeholder.
   Embedded via iframe so the animation's own styles stay isolated. */
.pipeline__animation {
  width: 100%;
  margin: 3.5rem 0;        /* same rhythm as media-placeholder--wide */
  background: transparent; /* let the page's gradient + grain show through (iframe is transparent) */
  overflow: hidden;
}

.pipeline__animation-frame {
  display: block;
  width: 100%;
  aspect-ratio: 1010 / 430;                  /* matches the animation's SVG viewBox */
  border: 0;
  background: transparent;
}

/* =========================================================
   5. TRANSLATION — narrow centered prose, ~60vh
   ========================================================= */
.prose {
  min-height: 60vh;
  display: flex;
  align-items: center;
  padding: clamp(4rem, 10vh, 8rem) var(--gutter);
}

.prose__inner {
  max-width: 62ch;
  margin: 0 auto;
}

.prose__title {
  font-size: clamp(1.75rem, 3.5vw, 3rem);
  font-weight: 600;
  margin-bottom: 2.25rem;
}

.prose__para {
  font-size: clamp(1rem, 1.3vw, 1.15rem);
  /* Brightened to 0.85 to match the motion copy, rather than the dimmer
     0.55 body default — easier to read on near-black. */
  color: rgba(240, 237, 232, 0.85);
}

.prose__para + .prose__para { margin-top: 1.5rem; }

/* ---------- TRANSLATION: intro column + page fan-out band ----------
   The section stacks a centred text column over a full-width, transparent
   animation band, so the copy never overlaps the animation. The band sits on
   the page's ambient background (silk/haze shows through the frames' alpha). */
.translation {
  max-width: var(--maxw);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1.2fr; /* text left ~45%, animation panel right ~55% */
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
  padding: clamp(4rem, 10vh, 8rem) var(--gutter);
}

.translation__intro {
  max-width: 62ch;
  margin: 0;
}

/* Contained transparent panel beside the copy (text left, animation right).
   aspect-ratio 16:9 both matches the 1920×1080 source AND supplies the box height
   (the canvas/poster are position:absolute, so without it the box would collapse).
   The JS draws "contain", so the whole horizontal fan sits inside with transparent
   margins. The half-width column already keeps it to ~38vh; max-height is a safety cap. */
.pagefan {
  position: relative;
  width: 100%;
  max-width: 100%;
  margin: 0;
  aspect-ratio: 16 / 9;
  max-height: 55vh;
  /* no background — the panel is deliberately transparent */
}

.pagefan__canvas,
.pagefan__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Poster carries the still fallback + alt text. Visible until the canvas takes
   over (.is-playing), then faded out — kept at opacity:0 (not display:none) so
   its alt text stays in the accessibility tree. object-fit matches the canvas. */
.pagefan__poster {
  object-fit: contain;
  transition: opacity 0.4s ease;
}

.pagefan.is-playing .pagefan__poster { opacity: 0; }

/* Hint the rewind-scrub affordance on hover-capable pointers only. */
@media (hover: hover) and (pointer: fine) {
  .pagefan { cursor: ew-resize; }
}

/* Reduced motion: never run the canvas; hold the static poster. */
@media (prefers-reduced-motion: reduce) {
  .pagefan.is-playing .pagefan__poster { opacity: 1; }
}

/* ---------- Scrub affordance ----------
   A quiet cue for the cursor scrub — no loud chevrons. A single dot glides along
   a hairline to imply the horizontal flip; the label names it. Hidden by default;
   JS reveals it only on hover/fine pointers once the fan is armed (.is-armed), then
   retires it for good after the first scrub (.has-scrubbed). The soft shadow keeps
   it legible over both the light pages and the dark transparent margins. */
.pagefan__hint {
  position: absolute;
  left: 50%;
  /* Sits just below the fan's bottom edge: the canvas draws the pages to fill
     the 16:9 frame, so a positive offset lands under the pages. Negative drops
     it into the empty panel space beneath, clear of the animation. */
  bottom: clamp(-3.6rem, -10%, -2.5rem);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  opacity: 0;
  transition: opacity 0.7s ease;
  pointer-events: none; /* never intercept the scrub itself */
}

.pagefan__hint-label {
  font-size: 0.68rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(240, 237, 232, 0.6);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
}

.pagefan__hint-track {
  position: relative;
  width: 46px;
  height: 1px;
  background: rgba(240, 237, 232, 0.22);
}

.pagefan__hint-track::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 4px;
  height: 4px;
  margin-top: -2px;
  border-radius: 50%;
  background: rgba(240, 237, 232, 0.85);
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.35);
  animation: pagefan-hint-glide 2.6s ease-in-out infinite;
}

@keyframes pagefan-hint-glide {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(42px); } /* track (46) − dot (4) */
}

/* Reveal only on hover/fine pointers, once armed; retire after the first scrub. */
@media (hover: hover) and (pointer: fine) {
  .pagefan.is-armed .pagefan__hint { opacity: 1; }
  .pagefan.has-scrubbed .pagefan__hint { opacity: 0; }
}

/* Safety net: if the dot ever shows under reduced motion, hold it centred. */
@media (prefers-reduced-motion: reduce) {
  .pagefan__hint-track::after { animation: none; transform: translateX(21px); }
}

/* Mobile: stack the panel below the copy (≤860px matches the JS min-width:861px
   desktop guard, so phones show the static poster, not the canvas). */
@media (max-width: 860px) {
  .translation { grid-template-columns: 1fr; }
  .pagefan { max-height: 40vh; }
}

/* =========================================================
   6. CONTACT — centered, generous padding
   ========================================================= */
.contact {
  text-align: center;
  padding: clamp(6rem, 18vh, 14rem) var(--gutter);
  /* Section-scoped brand tones so the hierarchy pieces below can share them. */
  --contact-accent: var(--hero-accent);  /* match the hero orange — eyebrow + hover */
  --contact-chalk:  #E4DFD9;             /* warm chalk — the primary Email · Phone row */
}

/* Quiet signature, not a headline — sized below every section title on the
   page (was clamp(3rem, 9vw, 9rem), bigger than the hero itself). */
.contact__name {
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* Eyebrow — mirrors the hero/section eyebrow (uppercase, tracked), in gold.
   The generous top margin sets it apart as its own block below the name,
   not a subtitle hanging off it. */
.contact__eyebrow {
  margin-top: 2.25rem;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--contact-accent);
}

/* Primary contact — Email · Phone. The row that should read as important:
   warm chalk, a step larger than the eyebrow, roomy around the middle-dot. */
.contact__primary {
  margin-top: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  font-weight: 500;
}

/* Secondary — Instagram as an afterthought, just below the primary row.
   The links' own vertical padding now carries the visual separation. */
.contact__secondary {
  margin-top: 0;
}

/* Shared by the <a> (Instagram) and <button> (Email/Phone copy-to-clipboard)
   contact links so both look identical regardless of tag. Quiet by default;
   warms to the hero orange on hover — a plain color shift, no underline or glow. */
.contact__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: none;
  border: none;
  /* Roomier hit area — taller (and a touch wider) so the links are easy to tap.
     The primary row's gap is trimmed to keep the middle-dot spacing unchanged. */
  padding: 0.6rem 0.65rem;
  font: inherit;
  cursor: pointer;
  appearance: none;
  transition: color 0.25s ease, opacity 0.25s ease;
}

.contact__link:hover,
.contact__link:focus-visible {
  color: var(--contact-accent);
}
.contact__link.is-copied {
  color: var(--contact-accent);
}

/* Primary links carry the warm chalk — the important row. */
.contact__link--primary { color: var(--contact-chalk); }

/* Instagram — deliberately lesser: smaller and muted to ~60%, reads as an
   afterthought. Hover lifts it to full-strength gold so it never feels broken. */
.contact__link--secondary {
  color: var(--contact-chalk);
  opacity: 0.6;
  font-size: 0.82rem;
}
.contact__link--secondary:hover,
.contact__link--secondary:focus-visible {
  opacity: 1;
}

.contact__dot { color: var(--text-faint); }

.contact__icon {
  width: 1.05em;
  height: 1.05em;
  flex-shrink: 0;
}

/* Visually hidden aria-live region — announces "Copied" to screen readers
   without a visible toast. */
.contact__copy-status {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* =========================================================
   2. FMIS — section-scoped purple/blue accent + detail block
   ========================================================= */
/* Remap the accent for THIS section only. Every var(--accent) used by
   descendants (index, tags, link, feature markers) turns purple/blue.
   Also lift --text-dim here: the body copy in this section runs long, so the
   default 0.55 dim was hard to read on near-black — 0.78 keeps it calm but legible. */
.fmis {
  /* Warm gold — a touch more saturated than the page accent (#c8b89a), so the
     section keeps a faint identity of its own while staying in the portfolio's
     one warm voice (no more purple). --fmis-grad-* are the two gold stops that
     replace the old blue→purple gradient everywhere in this section. */
  --accent:      #d8be88;
  --fmis-grad-1: #ecd29a;   /* light gold   */
  --fmis-grad-2: #c2904a;   /* deep amber   */
  --text-dim:    rgba(240, 237, 232, 0.78);

  /* Surface tokens for the framed shots + cards (apple-style elevation). */
  --fm-surface: #141413;
  --fm-border:  rgba(240, 237, 232, 0.10);
  --fm-radius:  22px;
}

/* =========================================================
   FMIS — product showcase (apple-style layout)
   A centred opener, two alternating framed feature rows, a four-up
   highlight grid, then the cursor-lit screen tiles. Generous vertical
   rhythm, big type, surface cards. The JS reveal hooks are preserved on
   .fm-feature / .fm-highlights / .fm-detail; tiles live under .fm-tiles
   ([data-spotlight-grid]). Warm-gold tokens come from .fmis above.
   ========================================================= */
.fm-showcase {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: clamp(5rem, 12vh, 10rem) var(--gutter) clamp(3rem, 7vh, 6rem);
  display: flex;
  flex-direction: column;
  gap: clamp(4.5rem, 11vh, 9rem);
}

/* ---- Section opener ---- */
.fm-intro {
  max-width: 58rem;
  margin: 0 auto;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 2.4vh, 1.6rem);
}

.fm-eyebrow {
  font-family: var(--font-disp);
  font-size: clamp(0.78rem, 1vw, 0.92rem);
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--accent);
}

.fm-headline {
  font-size: clamp(2.6rem, 6vw, 5.25rem);
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: -0.03em;
  max-width: 18ch;
}

/* Emphasis on the key word via solid worklight gold — a rationed highlight, not
   a clipped gradient (that technique stays exclusive to the wordmark). */
.fm-headline em {
  font-style: normal;
  color: var(--accent);
}

.fm-lead {
  max-width: 52ch;
  font-size: clamp(1.1rem, 1.4vw, 1.35rem);
  line-height: 1.55;
  color: var(--text-dim);
  text-wrap: balance;
}

/* ---- Feature rows: framed device shot + copy ---- */
.fm-feature {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
}

.fm-feature__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.15rem;
  max-width: 34rem;
}

.fm-kicker {
  font-family: var(--font-disp);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
}

.fm-feature__title {
  font-size: clamp(2rem, 3.6vw, 3.25rem);
  font-weight: 600;
  line-height: 1.06;
  letter-spacing: -0.025em;
}

.fm-feature__body {
  font-size: clamp(1.02rem, 1.25vw, 1.2rem);
  line-height: 1.6;
  color: var(--text-dim);
  max-width: 44ch;
}

.fm-tags {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.fm-tags li {
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--accent);
  padding: 0.4rem 0.9rem;
  border: 1px solid rgba(216, 190, 136, 0.30);
  border-radius: 999px;
  transition: background 0.25s ease, border-color 0.25s ease;
}

.fm-tags li:hover {
  background: rgba(216, 190, 136, 0.10);
  border-color: rgba(216, 190, 136, 0.6);
}

/* Device frame — a clean surface card with soft depth, image cover-cropped. */
.fm-frame {
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--fm-border);
  border-radius: var(--fm-radius);
  background: var(--fm-surface);
  box-shadow: 0 2rem 4rem -1.5rem rgba(0, 0, 0, 0.8);
}

.fm-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Phone: a shorter frame than before, with the shot pushed in (scale) so the
   device fills more of the crop. */
.fm-frame--phone { aspect-ratio: 5 / 5.3; }
.fm-frame--phone img { object-position: 33% center; transform-origin: center 40%; transform: scale(1.25); }
/* Laptop: the hero shot — it's the live app. Push the crop in hard and anchor
   the zoom on the upper area so the screen (not the keyboard/desk) fills the
   frame and the UI is actually legible. */
.fm-frame--laptop { aspect-ratio: 16 / 12; }
.fm-frame--laptop img {
  object-position: center;
  transform: scale(1.25);
  transform-origin: center 52%;
}

/* Give the laptop feature its own, wider figure column so the frame reads
   noticeably bigger than the phone (collapses to one column on mobile below). */
.fm-feature[data-fmis-laptop-spotlight] { grid-template-columns: 1.35fr 0.65fr; }

/* ---- Key-features highlight grid ---- */
.fm-highlights {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2rem, 5vh, 3.25rem);
}

.fm-highlights__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
  text-align: center;
}

.fm-highlights__title {
  font-size: clamp(1.9rem, 4vw, 3rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  text-align: center;
}

/* ---- Read-more — prose detail in place of a second tile row ---- */
.fm-readmore {
  max-width: 82rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 6vh, 4.5rem) var(--gutter) clamp(5rem, 11vw, 9rem);
  display: flex;
  flex-direction: column;
  /* No flex gap: the head-to-body spacing lives inside the collapsible panel so
     it collapses with it, instead of leaving a gap under a closed section. */
  transition: padding-bottom 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Closed state pulls the tail spacing in — a lone heading does not need 9rem
   of runway beneath it. Only applies once JS has taken over the toggle, and
   only at the narrow widths where the section is collapsible at all. */
@media (max-width: 860px) {
  .js .fm-readmore[data-readmore]:not(.is-open) {
    padding-bottom: clamp(3rem, 7vw, 5rem);
  }
}

.fm-readmore__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
  text-align: center;
}

/* Small gold tick marks the start of the detail section. */
.fm-readmore__head::before {
  content: "";
  width: 2.5rem;
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(120deg, var(--fmis-grad-1), var(--fmis-grad-2));
}

.fm-readmore__title {
  margin: 0;
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* The heading doubles as the control, but only where the section collapses
   (<=860px — see the block at the end of this group). On desktop the prose is
   always open, so the button keeps no chevron, no cursor and no hover state:
   it reads as a plain title. */
.fm-readmore__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  cursor: default;
  transition: color 0.3s ease;
}

/* CSS chevron — points down when closed, flips up when open. Hidden until the
   collapse is actually available. */
.fm-readmore__chev {
  display: none;
  flex: none;
  width: 0.42em;
  height: 0.42em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  border-radius: 1px;
  opacity: 0.5;
  transform: translateY(-0.12em) rotate(45deg);
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease;
}

.fm-readmore__panel {
  display: grid;
  grid-template-rows: 1fr;
}

.fm-readmore__panel-inner {
  min-height: 0;
  overflow: hidden;
}

/* Head-to-body spacing, inside the panel so it collapses with the prose. */
.fm-readmore__panel .fm-readmore__body {
  padding-top: clamp(2.5rem, 5.5vh, 4rem);
}

/* ---- Collapsible below 860px ----
   Narrow screens turn the wall of prose into an opt-in: closed by default,
   opened from the heading. The height animation is grid-template-rows going
   0fr <-> 1fr, so nothing is measured in JS. Scoped to .js because without
   scripting the markup's expanded state must stand. */
@media (max-width: 860px) {
  /* Stack the control: label on top, chevron centred beneath it. Inline beside
     the text it crowds the heading once the title wraps on a narrow screen. */
  .js .fm-readmore[data-readmore] .fm-readmore__toggle {
    cursor: pointer;
    flex-direction: column;
    align-items: center;
    gap: 0.5em;
  }
  .js .fm-readmore[data-readmore] .fm-readmore__toggle-label { text-align: center; }
  .js .fm-readmore[data-readmore] .fm-readmore__toggle:hover { color: var(--accent); }
  .js .fm-readmore[data-readmore] .fm-readmore__chev { display: block; }

  .js .fm-readmore[data-readmore] .fm-readmore__toggle:hover .fm-readmore__chev,
  .js .fm-readmore[data-readmore] .fm-readmore__toggle:focus-visible .fm-readmore__chev {
    opacity: 0.95;
  }

  .js .fm-readmore[data-readmore].is-open .fm-readmore__chev {
    transform: translateY(0.1em) rotate(-135deg);
  }

  .js .fm-readmore[data-readmore] .fm-readmore__panel {
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.55s cubic-bezier(0.22, 1, 0.36, 1);
  }

  .js .fm-readmore[data-readmore].is-open .fm-readmore__panel {
    grid-template-rows: 1fr;
  }

  .js .fm-readmore[data-readmore] .fm-readmore__panel-inner {
    opacity: 0;
    transition: opacity 0.3s ease;
  }

  .js .fm-readmore[data-readmore].is-open .fm-readmore__panel-inner {
    opacity: 1;
    transition: opacity 0.45s ease 0.15s;
  }

  @media (prefers-reduced-motion: reduce) {
    .fm-readmore,
    .js .fm-readmore[data-readmore] .fm-readmore__chev,
    .js .fm-readmore[data-readmore] .fm-readmore__panel,
    .js .fm-readmore[data-readmore] .fm-readmore__panel-inner,
    .js .fm-readmore[data-readmore].is-open .fm-readmore__panel-inner {
      transition: none;
    }
  }
}

.fm-readmore__body {
  max-width: 62ch;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.fm-readmore__body p {
  margin: 0;
  font-size: clamp(1.05rem, 1.25vw, 1.15rem);    /* ~17–18px — a touch larger for long-form prose */
  line-height: 1.65;
  color: rgba(240, 237, 232, 0.86);              /* lighter than the section's 0.78 dim for easier reading */
}

.fm-readmore__body strong {
  color: var(--accent);
  font-weight: 600;
}

/* Two-column split — left/right reading columns for the longer detail copy. */
.fm-readmore__body--split {
  max-width: none;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(2.5rem, 6vw, 5.5rem);
  align-items: start;
}

.fm-readmore__col {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.fm-readmore__col-title {
  margin: 0;
  font-family: var(--font-disp);
  font-size: clamp(1.05rem, 1.6vw, 1.4rem);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
}

.fm-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1rem, 1.6vw, 1.5rem);
}

.fm-card {
  display: flex;
  flex-direction: column;
  gap: 0.95rem;
  padding: clamp(1.6rem, 2.2vw, 2.25rem);
  background: var(--fm-surface);
  border: 1px solid var(--fm-border);
  border-radius: var(--fm-radius);
  transition: border-color 0.3s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.fm-card:hover {
  border-color: rgba(216, 190, 136, 0.45);
  transform: translateY(-4px);
}

/* Short gradient tick — a quick visual anchor so the three cards scan as a set. */
.fm-card__mark {
  width: 2.25rem;
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(120deg, var(--fmis-grad-1), var(--fmis-grad-2));
}

.fm-card__num {
  font-family: var(--font-disp);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  margin-bottom: 0.85rem;
  background: linear-gradient(120deg, var(--fmis-grad-1), var(--fmis-grad-2));
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
}

.fm-card__title {
  margin: 0;
  font-family: var(--font-disp);
  font-size: clamp(1.2rem, 1.6vw, 1.4rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
}

.fm-card__desc {
  margin: 0;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--text-dim);
}

/* ---- In-app screen tiles ---- */
.fm-detail {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: clamp(2rem, 5vh, 4rem) var(--gutter) clamp(5rem, 11vw, 9rem);
  display: flex;
  flex-direction: column;
  gap: clamp(2rem, 4.5vh, 3rem);
}

.fm-detail__head {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
}

.fm-detail__title {
  font-size: clamp(1.7rem, 3.4vw, 2.6rem);
  font-weight: 700;
  letter-spacing: -0.03em;
}

.fm-tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1rem, 2vw, 1.75rem);

  /* Feed the reused .spotlight-tile component: gold ring stops + spot radius. */
  --tile-grad-1: var(--fmis-grad-1);
  --tile-grad-2: var(--fmis-grad-2);
  --spot: 260px;
}

/* ---- FMIS responsive ---- */
@media (max-width: 980px) {
  .fm-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 860px) {
  .fm-showcase { gap: clamp(3.5rem, 9vh, 6rem); }
  .fm-feature,
  .fm-feature[data-fmis-laptop-spotlight] {
    grid-template-columns: 1fr;
    gap: clamp(1.75rem, 6vw, 2.5rem);
  }
  .fm-feature__text { max-width: none; }
  .fm-tiles { grid-template-columns: 1fr; }
  .fm-readmore__body--split { grid-template-columns: 1fr; }
}

@media (max-width: 560px) {
  .fm-grid { grid-template-columns: 1fr; }
}

/* Shared wordmark styles. The FMIS mark carries a brand gradient — blue
   (#2563eb) → purple (#6d32b8) — clipped to the glyphs. It is painted 2× wide
   so the hero instance can sweep the colours left→right once on reveal before
   settling; the resting position (0% 50%) reads blue→purple across the word. */
.fmis__wordmark {
  margin: 0;
  pointer-events: none;
  font-family: var(--font-disp);
  font-weight: 800;
  line-height: 0.9;
  letter-spacing: -0.04em;

  color: var(--text);   /* fallback if background-clip:text is unsupported */
  background-image: linear-gradient(100deg, #2563eb 0%, #6d32b8 50%, #2563eb 100%);
  background-size: 200% 100%;
  background-position: 0% 50%;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;

  /* Saturate + brighten the clipped gradient and bloom a coloured glow off the
     near-black background, so the mark reads as lit and solid — not translucent
     with the dark page bleeding through. Glow follows the glyph shapes. */
  filter:
    brightness(1.08) saturate(1.3)
    drop-shadow(0 0 18px rgba(37, 99, 235, 0.50))
    drop-shadow(0 0 46px rgba(109, 50, 184, 0.45))
    /* Dark halo to anchor the mark against the bright sun behind it. */
    drop-shadow(0 2px 26px rgba(10, 10, 10, 0.55));
}

/* Hero wordmark — the big "Fleet Management Information System" lockup over the
   city scrub. The gradient sweeps once when the title is revealed (JS adds
   .is-revealed on .cityscroll__title), then holds. */
.fmis__wordmark--hero {
  font-size: clamp(2.3rem, 5.7vw, 5rem);
  line-height: 1.04;
  letter-spacing: -0.025em;
  text-align: center;
}

/* Flow the colours left→right once, decelerating into the resting blue→purple. */
@keyframes fmis-gradient-sweep {
  from { background-position: 100% 50%; }
  to   { background-position: 0% 50%; }
}

/* Small label above the title — frames the piece before the name lands. */
.fmis__intro-kicker {
  font-family: var(--font-disp);
  font-size: clamp(0.82rem, 1.45vw, 1.12rem);
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: 0 1px 12px rgba(10, 10, 10, 0.7);
}

.fmis__intro-lead {
  max-width: 52ch;
  margin: 0 auto;
  color: var(--text-dim);
  font-size: clamp(1.12rem, 1.55vw, 1.5rem);
  font-weight: 400;
  line-height: 1.6;
  text-wrap: balance;
  text-shadow: 0 1px 16px rgba(10, 10, 10, 0.82);
}

/* Preview strip — names the three beats the viewer is about to scroll into
   (mobile → desktop → automation), so the title card hands off to the images.
   Gradient dots between items echo the eyebrow rule used in the copy blocks. */
.fmis__intro-meta {
  list-style: none;
  margin: 0.25rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  column-gap: 1.7rem;
  row-gap: 0.6rem;
  font-family: var(--font-disp);
  font-size: clamp(0.78rem, 1.05vw, 0.95rem);
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-shadow: 0 1px 12px rgba(10, 10, 10, 0.7);
}

.fmis__intro-meta li { position: relative; }

.fmis__intro-meta li + li::before {
  content: "";
  position: absolute;
  left: -0.96rem;
  top: 50%;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  transform: translateY(-50%);
  background: linear-gradient(120deg, #2563eb, #6d32b8);
}

/* ---------- FMIS spotlight tiles ----------
   Vanilla adaptation of the React glow-card. The border is a permanent
   warm-gold gradient ring (--tile-grad-* set on .fm-tiles); a cursor-tracked
   bright spot (positioned in viewport space via background-attachment:fixed,
   fed by --mx/--my from main.js) sweeps along the ring and lifts a soft tint
   across the surface. Carved to a hairline frame with mask-composite. */
.spotlight-tile {
  position: relative;
  isolation: isolate;
  min-height: 250px;
  display: flex;
  border-radius: 16px;
  background-color: var(--bg-soft);
  /* Surface tint that follows the cursor (viewport-fixed). Falls off-screen
     by default, so with no JS / on touch the tile rests as a clean card. */
  background-image: radial-gradient(
    var(--spot) var(--spot) at calc(var(--mx, -9999) * 1px) calc(var(--my, -9999) * 1px),
    rgba(216, 190, 136, 0.14), transparent 62%);
  background-attachment: fixed;
  background-repeat: no-repeat;
  box-shadow:
    0 1rem 2.5rem -1.2rem rgba(0, 0, 0, 0.85),
    0 0 0 transparent;
  transition:
    transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.4s ease;
  overflow: hidden;
}

/* Gradient border ring + cursor highlight, masked to a 2px frame. The base
   linear gradient (scroll) is always visible; the radial (fixed) brightens
   the ring where the cursor passes. */
.spotlight-tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  padding: 2px;
  background-image:
    radial-gradient(
      calc(var(--spot) * 0.8) calc(var(--spot) * 0.8)
      at calc(var(--mx, -9999) * 1px) calc(var(--my, -9999) * 1px),
      rgba(247, 238, 214, 0.95), transparent 60%),
    linear-gradient(135deg, var(--tile-grad-1), var(--tile-grad-2));
  background-attachment: fixed, scroll;
  background-repeat: no-repeat;
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
          mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}

.spotlight-tile:hover,
.spotlight-tile:focus-within {
  transform: translateY(-4px);
  box-shadow:
    0 1.4rem 2.8rem -1rem rgba(0, 0, 0, 0.85),
    0 0 30px -8px rgba(216, 190, 136, 0.5),
    0 0 54px -12px rgba(194, 144, 74, 0.42);
}

.spotlight-tile__body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 0.5rem;
  width: 100%;
  padding: clamp(1.3rem, 2vw, 1.7rem);
}

.spotlight-tile__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  margin-bottom: auto;   /* icon to the top, text to the bottom */
  border-radius: 11px;
  color: #ecd6a0;
  background: linear-gradient(135deg, rgba(216, 190, 136, 0.20), rgba(194, 144, 74, 0.20));
  border: 1px solid rgba(216, 190, 136, 0.3);
}

.spotlight-tile__icon svg { width: 1.35rem; height: 1.35rem; }

.spotlight-tile__index {
  font-family: var(--font-disp);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  background: linear-gradient(100deg, var(--tile-grad-1), var(--tile-grad-2));
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
}

.spotlight-tile__title {
  margin: 0;
  font-family: var(--font-disp);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

.spotlight-tile__hint {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text-dim);
  max-width: 30ch;
}

/* Optional real screenshot — drop an <img class="spotlight-tile__shot"> in the
   body and it center-crops to fill the tile, behind the caption. */
.spotlight-tile__shot {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

@media (prefers-reduced-motion: reduce) {
  .spotlight-tile { transition: none; }
  .spotlight-tile:hover,
  .spotlight-tile:focus-within { transform: none; }
}

/* =========================================================
   4. 3D & MOTION — 2×2 showcase grid + software marquee
   ========================================================= */

/* Software / tools marquee */
.motion__tools {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: clamp(3.5rem, 7vw, 6rem) 0;
}

.motion__tools-label {
  padding: 0 var(--gutter);
  margin-bottom: 1.9rem;
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}

.marquee__track {
  display: flex;
  width: max-content;
  gap: clamp(1rem, 2vw, 1.75rem);
  animation: marquee 34s linear infinite;
}

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

.tool {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.85rem 1.4rem;
  white-space: nowrap;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: rgba(240, 237, 232, 0.015);
  transition: border-color 0.25s ease, background 0.25s ease;
}

.tool:hover {
  border-color: rgba(200, 184, 154, 0.40);
  background: rgba(200, 184, 154, 0.05);
}

.tool__icon {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 7px;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
}

/* Real logo PNGs: let the artwork breathe — drop the placeholder chrome and
   letterbox the icon inside the 26px box so non-square logos keep their ratio. */
img.tool__icon {
  object-fit: contain;
  background: none;
  border: none;
  border-radius: 0;
}

.tool__name {
  font-size: 0.95rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-dim);
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
}

/* =========================================================
   RESPONSIVE — collapse splits to single column.
   (Lightweight: no mobile nav complexity yet.)
   ========================================================= */
@media (max-width: 860px) {
  /* 3D showcase grid collapses to one column. On phones we lead with the copy,
     so the text block jumps ahead of the tiles (order:-1); the three media
     tiles then follow in DOM order: fight → M4 → rain. */
  .motion__grid { grid-template-columns: 1fr; }
  .motion__text { padding: 0 0 0.5rem; order: -1; }
  /* Drop the desktop-only nudge that pulls the title inline with the fight
     tile — stacked, it would bleed into the section above. */
  .motion__text .split__title { margin-top: 0; }

  .media-placeholder,
  .media-frame { min-height: 260px; }
  /* .motion__tile .motion__media (0,2,0) keeps its 16:9 over the rule above. */

  /* Wordmark and links are different sizes, so centring their boxes left the
     name sitting ~1px high — align the text BASELINES instead, which is the
     edge the eye actually reads. The row gap comes in so the smaller name and
     the links still clear each other at 375px (iPhone SE, the narrowest we
     design for); nowrap is the hard guarantee that "MSc" never drops to a
     second row. */
  .nav__inner { align-items: baseline; gap: 1rem; }
  .nav__wordmark { font-size: 0.83rem; white-space: nowrap; }
  .nav__links { gap: 1.1rem; }
  .hero__scroll { display: none; }

  /* Rein in the ethereal bleed on phones: the FMIS content sits close under the
     hero here, so keep the cloud mostly within the hero and a touch softer. */
  .hero__bg { height: 114vh; opacity: 0.7; }

  /* ---- City section: legibility treatment for the static poster ----
     Phones never run the scrub (the JS gate mirrors this breakpoint), so what
     they get is the poster, full-frame, with the title centred over the
     brightest part of the skyline. Two things fix that, and deliberately not a
     heavier flat scrim — that dims the lit windows just as much as the sky and
     leaves the picture grey rather than dark.

     First, contrast on the poster itself. contrast() pivots around mid-grey, so
     the shadows drop away while the building lights come UP, and brightness
     then sets the overall level: darker overall, but with more separation
     rather than less. It costs nothing at runtime for the reason given above
     the blur rule — the poster is static, so the filter composites once and is
     cached. (.is-softening would replace this filter outright, but that class
     is only ever set by the scrub, which does not run here.) */
  .cityscroll__viewport::before {
    filter: contrast(1.16) brightness(0.84) saturate(1.06);
  }

  /* Second, weight behind the text specifically. .cityscroll__title is inset:0
     and flex-centred, so an ellipse at 50% 46% sits under the wordmark and lead
     without touching the skyline above or the foreground below — both of which
     the visitor is meant to keep seeing, and both of which the base overlay
     already treats (it reaches 0.95 black at the very bottom on its own).
     Carried on a pseudo-element so the two tuned linear gradients on .overlay
     do not have to be duplicated into this media query just to prepend a
     layer. Wider than tall because the text block is. */
  .cityscroll__overlay::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(ellipse 78% 52% at 50% 46%,
      rgba(6, 8, 12, 0.55) 0%,
      rgba(6, 8, 12, 0.42) 38%,
      rgba(6, 8, 12, 0.16) 68%,
      rgba(6, 8, 12, 0.00) 100%);
  }
}

/* Narrower than an iPhone SE (375px) the nav row is no longer guaranteed to fit
   at the sizes above, so tighten the gaps and the links a step rather than let
   the row run off the right edge. Nothing here applies at 375 and up. */
@media (max-width: 374px) {
  .nav__inner { gap: 0.6rem; }
  .nav__links { gap: 0.55rem; }
  .nav__links a { font-size: 0.8rem; }
}
