/* ==========================================================================
   Eric J. Lopez portfolio — homepage
   Tokens come from DESIGN.md verbatim. DESIGN.md is the authority.
   ========================================================================== */

:root {
  /* Core */
  --bg: #0D1B2E;
  --surface: #13213A;
  --surface-deep: #101E35;
  --ink: #F0F4FA;
  --muted: #9FB0C8;
  --accent: #52C7F0;

  /* Accent scale — one hue, fixed opacities */
  --accent-line: rgba(82, 199, 240, .30);
  --accent-arc: rgba(82, 199, 240, .55);
  --accent-solid: #52C7F0;

  /* Overlays */
  --lift: rgba(19, 33, 58, .55);
  --divider: rgba(255, 255, 255, .40);

  /* Type */
  --font-display: 'Space Grotesk', sans-serif;
  --font-ui: 'Inter', sans-serif;

  /* Spacing — 0.25rem base */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;
  --space-40: 10rem;

  /* Radii */
  --radius-sm: 0.5rem;
  --radius-md: 1.375rem;
  --radius-lg: 1.5rem;
  --radius-pill: 62.4375rem;

  /* Motion — one curve, three durations */
  /* One curve, and it decelerates: motion starts fast and spends most of its
     time landing, which is what makes things read as settling into place
     rather than lunging at it. The old curve was near-symmetric, so nothing
     ever felt like it arrived. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 160ms;
  --dur-mid: 320ms;
  --dur-slow: 600ms;

  /* Layout */
  --page-width: 90rem;
  --gutter: 7.5rem;
  --measure-body: 37.5rem;
  --measure-head: 42.5rem;

  /* Mobile grid — 4 stretched columns, fixed 16px margins and gutters. Only
     read inside the 900px breakpoint. Stated in px, not rem, so they hold
     regardless of what the root is doing. */
  --m-margin: 16px;
  --m-gutter: 16px;
  --m-cols: 4;
}

* { box-sizing: border-box; }

/* Proportional canvas: 1rem = 16px at the 1440 design width and scales with
   the viewport, so the layout commands the same share of any screen — a 13"
   MacBook or a wide monitor. The floor only matters below ~800px windows. */
html {
  /* Case-study pages scale with the window — 16px at the 1440 reference —
     but stop zooming at Eric's laptop scale: past a 1920px window they hold
     at 21.33px and the extra monitor width becomes margin, so the monitor
     shows the laptop rendering centred, not a blown-up copy. (First cut
     capped at 17px/1530 — that was below the laptop's own rendering, which
     shrank the pages everywhere and froze them mid-range.) */
  font-size: clamp(9px, 1.1111vw, 21.33px);
  -webkit-text-size-adjust: 100%;
}

/* The homepage is the exception: its hero, planet, and orbit choreography
   are built proportional, so it keeps the uncapped viewport scale. Wide
   canvas only — unscoped, :has() out-ranks the phone block's plain `html`
   16px pin and collapsed mobile rem to 9px. */
@media (min-width: 901px) {
  html:has(body#top) {
    font-size: max(1.1111vw, 9px);
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--muted);
  font-family: var(--font-ui);
  /* 90rem resolves to 100vw, which includes the scrollbar; clip the sliver */
  overflow-x: hidden;
}

img { display: block; }

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

:focus-visible {
  outline: 2px solid var(--accent-solid);
  outline-offset: 3px;
  border-radius: 0.25rem;
}

/* Page shell — desktop layout per DESIGN.md (1440 frame) */
.page {
  position: relative;
  min-width: var(--page-width);
  /* clip, not hidden: hidden would make .page a scroll container and
     break the journey's position: sticky earth stage */
  overflow: clip;
}

.frame {
  position: relative;
  width: var(--page-width);
  margin: 0 auto;
}

/* ==========================================================================
   Star field — 30–40 dots, --ink at 25–75%, 1.5–3.7px, never clustered
   ========================================================================== */

#stars {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Two depth layers; JS drifts them at different speeds while scrolling */
#stars .star-layer {
  position: absolute;
  inset: 0;
  will-change: transform;
}

#stars .star {
  position: absolute;
  border-radius: 50%;
  background: var(--ink);
}

/* ==========================================================================
   Nav — scrolls away with the page, nothing fixed or sticky
   ========================================================================== */

/* The shell grid: fixed 78.25rem, centered — on wide screens the layout reads
   as the 1440 canvas with breathing room, never left-packed. Nav and hero
   content align to it. */
/* The bar is fixed at every width and carries its own fill, so content
   scrolling beneath it disappears behind a solid edge instead of showing
   through half-cut. Menu on the left, mark dead centre (the spacer mirrors
   the button's width so centring is true), nothing on the right. */
.nav {
  position: fixed;
  /* the bar breathes off the viewport edge; it is glass, so it simply
     floats a step lower */
  top: 8px;
  left: 0;
  right: 0;
  z-index: 40;
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 4.5rem;
  padding: 0 max(1.5rem, calc((100% - 78.25rem) / 2));
  /* the bar floats glass-clear over the page at every width */
  background: transparent;
}

.nav-menu,
.sidenav-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.75rem;
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.sidenav-close { width: 2.75rem; }

/* Desktop carries the three links in the bar; the shapes-only menu control
   belongs to phones */
.nav-links-bar {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}

/* the contact slot is the bar's one button: compact pill, nav-sized label */
.nav-contact {
  padding: 0.4rem 1rem;
  font-size: 0.9375rem;
}

.nav-links-bar a {
  font: 600 0.9375rem/1 var(--font-ui);
  color: var(--muted);
  transition: color var(--dur-fast) var(--ease);
}

.nav-links-bar a:hover { color: var(--ink); }

/* a plain hamburger in the star color; hover warms it to the accent */
.nav-menu {
  width: 2.75rem;
  display: none;
  color: var(--ink);
}

.nav-menu:hover { color: var(--accent-solid); }

.sidenav-close:hover { color: var(--accent-solid); }

.nav-menu:active,
.sidenav-close:active { transform: scale(.92); }

.nav-spacer { width: 2.75rem; height: 1px; }

.nav-star {
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  transition: filter var(--dur-fast) var(--ease);
}

/* The mark: the logo star with its orbit ring. The ring passes behind the star
   across the top and crosses in front along the bottom, which is what makes it
   read as an orbit rather than a hoop drawn around a shape. Star takes
   currentColor so the link's hover glow carries it; the ring is the accent. */
.nav-star .logo-mark {
  display: block;
  width: 3.25rem;
  height: auto;
  overflow: visible;
}

/* The mark rests as the star alone; the orbit appears on hover, the way the
   kickers draw their line when they arrive */
.logo-mark .logo-ring {
  fill: none;
  stroke: var(--accent-solid);
  stroke-width: 1.1;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}

.nav-star:hover .logo-ring,
.nav-star:focus-visible .logo-ring { opacity: 1; }

.nav-star:hover { filter: drop-shadow(0 0 0.5rem rgba(240, 244, 250, .55)); }

.nav-star:active { filter: drop-shadow(0 0 0.875rem rgba(240, 244, 250, .85)); }

/* ---- The drawer -------------------------------------------------------
   The links live in a full-height panel that slides in from the menu edge,
   at every width. Rows are tall enough to be honest touch targets. */
.sidenav-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgba(13, 27, 46, .6);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-mid) var(--ease), visibility var(--dur-mid);
}

/* Mobile-only drawer, straight edge */
.sidenav {
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  z-index: 60;
  width: min(78vw, 20rem);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding: var(--space-4) var(--space-4) var(--space-8);
  background: var(--bg);
  border-left: 1px solid var(--accent-line);
  transform: translateX(102%);
  visibility: hidden;
  transition: transform var(--dur-mid) var(--ease), visibility var(--dur-mid);
}

body.menu-open .sidenav {
  transform: none;
  visibility: visible;
}

body.menu-open .sidenav-backdrop {
  opacity: 1;
  visibility: visible;
}

/* the X keeps to the straight edge, clear of the curve */
.sidenav-close { align-self: flex-end; }

.nav-links {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* hover is the site's own: muted to ink, nothing else. Same style as the
   desktop bar links, a step up in scale for the full-height panel */
.nav-links a {
  font: 600 1.125rem/1 var(--font-ui);
  color: var(--muted);
  padding: 1rem 0.5rem;
  transition: color var(--dur-fast) var(--ease);
}

.nav-links a:hover { color: var(--ink); }

/* ==========================================================================
   Kicker — label on a flat orbit ellipse; the text chip cuts through the line
   ========================================================================== */

/* Every kicker ellipse keeps the same shape language: the width grows only
   with the label, and the visible line flanking the chip is a constant
   80px per side so the fill never eats the stroke */
.kicker {
  position: relative;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  height: 2.0625rem;
  padding: 0 5rem;
  min-width: 20rem;
  margin: 0;
  box-sizing: border-box;
}

.kicker::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0.875rem;
  height: 1.3125rem;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--divider);
}

.kicker span {
  position: relative;
  background: var(--kicker-bg, var(--bg));
  padding: 0 var(--space-3);
  font: 700 1.125rem/1.625rem var(--font-display);
  letter-spacing: .08em;
  color: var(--accent-solid);
  white-space: nowrap;
}

/* JS swaps the ::before ellipse for an SVG twin (two half-arcs) so the line
   can draw itself left to right on reveal */
.kicker.has-line::before { display: none; }

.kicker .kao { background: transparent; }

.kicker-svg {
  position: absolute;
  left: 0;
  top: 0.875rem;
  width: 100%;
  height: 1.3125rem;
  overflow: visible;
}

.kicker-svg path {
  stroke: var(--divider);
  stroke-width: 1;
  fill: none;
}

/* ==========================================================================
   Shared type
   ========================================================================== */

.section-heading {
  margin: 0;
  max-width: var(--measure-head);
  font: 700 2.375rem/3rem var(--font-display);
  letter-spacing: -.015em;
  color: var(--ink);
  text-wrap: pretty;
}

/* ==========================================================================
   Buttons — pill, Inter Bold
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  font: 700 1rem/1 var(--font-ui);
  padding: 0.875rem 1.875rem;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease);
}

/* Press feedback: every button gives a touch under the finger. On phones this
   is the only state there is, and without it a tap lands on dead glass. */
.btn:active { transform: scale(.97); }

/* Free-floating labels size to their own words — a shared width would put
   the orbit around empty space */

/* Buttons, settled (Eric, 22 August 2026): the original pills, one size
   smaller, and every button is an outline that fills with the accent on
   hover. One style for all of them. */
.btn-primary,
.btn-secondary {
  background-color: transparent;
  color: var(--ink);
  border: 1px solid rgba(240, 244, 250, .4);
  border-radius: var(--radius-pill);
  padding: 0.625rem 1.5rem;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.btn-primary:hover,
.btn-secondary:hover {
  background-color: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

/* ==========================================================================
   Hero
   ========================================================================== */

/* The hero owns the band between the fixed nav and the horizon: its box ends
   exactly where the arc begins (earth.js reads this height), and the column
   centers inside it. That is what makes the gap under the nav and the gap
   above the arc come out the same without either being hand-tuned. */
.hero {
  position: relative;
  height: 83vh;
  padding-top: 4.625rem; /* clears the fixed nav */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

/* The moon lives in the about section now: one section, small, noticeable
   off to the right, behind the copy */
.moon {
  position: absolute;
  right: 4rem;
  top: 7rem;
  z-index: 0;
  opacity: .85;
  pointer-events: none;
}

.hero-col {
  position: relative;
  z-index: 1;
  /* the column answers to viewport height as well as width, so on a shallow
     screen the whole composition still clears the arc */
  width: min(72.5rem, 92vw, 121vh);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
}

.hero-h1 {
  margin: var(--space-6) 0 0;
  align-self: stretch;
  color: var(--ink);
  font-weight: 400;
}

/* The main H1 lines are SVG text on orbit paths; JS bends the paths and the
   bend eases out as you scroll. Type metrics match the old flat block. */
.h1-orbit {
  display: block;
  align-self: flex-start;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Inverted emphasis: the running text carries the moon's shade, the key
   words hold the white */
/* SVG text sizes are viewBox user units, NOT rem — the svg element itself
   scales with its rem-sized container, so rem here would double-scale */
.h1-orbit text {
  font: 500 64px var(--font-display);
  letter-spacing: -.02em;
  fill: var(--muted);
}

/* the greeting is the biggest text there is */
.h1-orbit .l-intro { font-size: 84px; }

.h1-orbit-m { display: none; }

.h1-orbit .hb,
.hero-orbit-info .hb {
  font-weight: 700;
  fill: var(--ink);
}

.hero-h1 b { font-weight: 700; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.hero-cta {
  display: flex;
  gap: var(--space-6);
  margin-top: var(--space-6);
}

/* (the compact base pill covers the hero CTAs too) */

/* ---- The rest of the introduction rides the planet's orbit -------------
   What used to be the kicker and the closing lines of the headline, set at
   the subtitle size and bent to the same curvature as the horizon below.
   Font sizes are viewBox user units: the wide svg renders ~0.96x, so 25
   lands at the 24px subtitle; the narrow one renders 1:1 at 19. */
.hero-orbit-info {
  position: absolute;
  left: 50%;
  bottom: 0.75rem;
  transform: translateX(-50%);
  width: min(62rem, 94vw);
  pointer-events: none;
}

.hero-orbit-info svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* no stroke of its own: the atmosphere line the canvas draws IS the orbit */
.hero-orbit-info path { stroke: none; }

/* the train inherits the muted tone; every mark rides in the same color */
.hero-orbit-info { color: var(--muted); }

.hero-orbit-info text {
  font: 500 22px var(--font-display);
  fill: var(--muted);
}

/* the role phrase travels highlighted */
.hero-orbit-info .train-role text {
  font: 700 22px var(--font-display);
  letter-spacing: -.01em;
  fill: var(--ink);
}

.hero-orbit-info .oi-narrow { display: none; }

/* ==========================================================================
   Horizon arc — chapter separator. Huge flat ellipse, stroke only,
   clipped to a shallow reveal window so just the crown shows.
   ========================================================================== */

/* The arc must reach the actual screen edges on any monitor so it reads as
   endless. .horizon keeps the 13.125rem slot in the flow; .horizon-bleed breaks
   out to whichever is wider, the viewport or the 1440 frame, and the ellipse
   overshoots that on both sides. */
/* Same curvature as the ¿Hablamos? surface lift (2840:1500), so both
   horizons read as the same planet. The window height tracks the viewport so
   the arc dips all the way to the screen edges before it's clipped. */
.horizon {
  position: relative;
  width: 100%;
  height: calc(16vw + 2.5rem);
  pointer-events: none;
}

.horizon-bleed {
  position: absolute;
  top: 0;
  left: calc((100% - max(100vw, 100%)) / 2);
  width: max(100vw, 100%);
  height: 100%;
  overflow: hidden;
}

.horizon i {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 0;
  width: max(177.5rem, 115vw);
  aspect-ratio: 2840 / 1500;
  border-radius: 50%;
}

/* Outer shadow only: the glow breathes upward into the sky above the line,
   never below it */
.horizon-glow {
  box-shadow: 0 -2px 1.875rem rgba(82, 199, 240, .13);
  -webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 92%);
  mask-image: linear-gradient(to bottom, #000 45%, transparent 92%);
}

.horizon-svg {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 0;
  width: max(177.5rem, 115vw);
  height: auto;
}

.horizon-draw {
  stroke: var(--accent-arc);
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
}

/* ==========================================================================
   Selected work
   ========================================================================== */

.work {
  position: relative;
  margin-top: 8.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Checkpoint spacing lives on .work-focus (see case-study block): from the
   hero you see only the arc at the bottom of the screen; Selected Work
   waits a beat of empty sky below it */

/* Block is centered on the page; heading and body share a left edge */
.work-intro {
  margin-top: var(--space-12);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.work-intro .section-heading { max-width: none; }

.section-body {
  margin: 1.625rem 0 0;
  max-width: var(--measure-body);
  font: 500 1.5rem/2rem var(--font-display);
  color: var(--muted);
  text-wrap: pretty;
}

.carousel {
  position: relative;
  width: 100%;
  margin-top: var(--space-16);
}

/* Clip horizontally for the slide mechanic, but pad the clip box so hover
   growth, shadows, and the ring glow never get cut */
.carousel-viewport {
  overflow: hidden;
  padding: 3.75rem 0 5.625rem;
  margin: -3.75rem 0 -5.625rem;
}

.carousel-track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  transform: translateX(0);
}

.slide {
  position: relative;
  flex: 0 0 100%;
  height: 45rem;
}

/* Orbit ring — the signature element. One wide ellipse spanning under both
   the artifact and the text. Positioned without transform so hover can
   scale it freely. */
.orbit-ring {
  position: absolute;
  bottom: 1.5rem;
  left: 50%;
  margin-left: -37.5rem;
  width: 75rem;
  height: 7.75rem;
  border-radius: 50%;
  background: rgba(19, 33, 58, .85);
  box-shadow: inset 0 0 0 1.5px var(--accent-arc), 0 0.375rem 2rem rgba(82, 199, 240, .16);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

.slide-row {
  position: relative;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: 2.5rem;
  transition: transform var(--dur-fast) var(--ease);
}

/* Hover: the arc widens and brightens, the card grows with it */
.slide:hover .slide-row { transform: scale(1.04); }

.slide:hover .orbit-ring {
  transform: scale(1.06);
  box-shadow: inset 0 0 0 1.5px var(--accent-arc), 0 0.375rem 2.25rem rgba(82, 199, 240, .28);
}

/* The art box hugs its artifact so image and text meet as one group in the
   middle of the space */
.slide-art {
  margin: 0 0 6rem;
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  align-items: flex-end;
}

.slide-art img {
  filter: drop-shadow(0 0.625rem 1.25rem rgba(0, 0, 0, .10));
}

.slide-text {
  width: 30rem;
  flex-shrink: 0;
  align-self: center;
}

.slide-title {
  margin: 0;
  font: 700 4rem/4.5rem var(--font-display);
  letter-spacing: -.015em;
  color: var(--ink);
  text-wrap: pretty;
}

.slide-oneliner {
  margin: var(--space-4) 0 0;
  max-width: 29.375rem;
  font: 500 1.75rem/2.375rem var(--font-display);
  color: var(--muted);
  text-wrap: pretty;
}

/* Slide indicators — rotated-square diamonds, accent when active */
.carousel-dots {
  display: flex;
  justify-content: center;
  gap: 0.625rem;
  margin-top: var(--space-8);
}

.carousel-dots button {
  width: 0.5625rem;
  height: 0.5625rem;
  padding: 0;
  border: none;
  cursor: pointer;
  transform: rotate(45deg);
  background: rgba(240, 244, 250, .16);
  transition: background var(--dur-fast) var(--ease);
}

/* The strips are only carousels below 900px, so the dots main.js hands them
   stay out of the way on the wide canvas where every card is already visible */
.strip-dots { display: none; }

.carousel-dots button:hover { background: rgba(240, 244, 250, .4); }

.carousel-dots button.active { background: var(--accent-solid); }

/* Carousel controls */
.circle-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 3.25rem;
  height: 3.25rem;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: transparent;
  border: 1px solid rgba(240, 244, 250, .4);
  color: var(--ink);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease);
}

.circle-btn:hover { border-color: rgba(240, 244, 250, .7); }

/* same press give as the pills; the translate keeps it on its centre line */
.circle-btn:active { transform: translateY(-50%) scale(.94); transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease); }

/* Out at the frame edges, clear of the slide content; they don't need to
   align with the orbit */
.carousel-prev { left: 1rem; }
.carousel-next { right: 1rem; }

/* ==========================================================================
   About me
   ========================================================================== */

.about {
  position: relative;
  margin-top: 55vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.sparkle { position: absolute; }
.sparkle-a { left: 10rem; top: -1.875rem; }
.sparkle-b { right: 10.625rem; top: 15rem; }

.about-row {
  width: 79.625rem;
  margin-top: var(--space-12);
  display: flex;
  gap: 5.25rem;
  align-items: flex-start;
}

.portrait-wrap { flex-shrink: 0; }

/* Rotation lives on the img, not the reveal wrapper, so the scroll-reveal
   transform can't overwrite it */
.portrait {
  width: 27.3125rem;
  height: 27.5625rem;
  object-fit: cover;
  border-radius: var(--radius-lg);
  transform: rotate(-6.1deg);
  box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, .10);
}

.about-col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-8);
  padding-top: var(--space-1);
}

.cred-cols {
  display: flex;
  gap: 5.625rem;
}

.cred-col {
  width: 18.75rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.cred-head {
  margin: 0;
  font: 500 1.5rem/2rem var(--font-display);
  color: var(--ink);
}

.cred-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.cred-list li {
  position: relative;
  padding-left: 1.375rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Diamond marker */
.cred-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.375rem;
  width: 0.4375rem;
  height: 0.4375rem;
  transform: rotate(45deg);
  background: var(--accent-solid);
}

.cred-title {
  font: 600 1rem/1.375rem var(--font-ui);
  color: var(--ink);
}

.cred-detail {
  font: 400 0.875rem/1.25rem var(--font-ui);
  color: var(--muted);
}

/* ==========================================================================
   ¿Hablamos? — surface lift marks the scene change, then the Vieques horizon
   ========================================================================== */

.contact-wrap {
  position: relative;
  margin-top: 55vh;
  overflow: hidden;
  /* the lift blends to ~#101E35 over the field; kicker chips match it */
  --kicker-bg: var(--surface-deep);
}

.surface-lift {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 0;
  width: 177.5rem;
  height: 93.75rem;
  border-radius: 50%;
  background: var(--lift);
  pointer-events: none;
}

.contact {
  position: relative;
  z-index: 1;
  padding-top: 8rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.contact .section-heading {
  margin-top: var(--space-12);
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}

.contact-body {
  margin: 1.625rem 0 0;
  font: 500 1.5rem/2rem var(--font-display);
  color: var(--muted);
}

.contact-cta {
  display: flex;
  gap: var(--space-6);
  margin-top: var(--space-12);
}

/* Vieques horizon — geometry transcribed from the Figma frame */
.vieques {
  position: relative;
  z-index: 1;
  height: 18.5rem;
  margin-top: 8.75rem;
  overflow: hidden;
}

.vq-mountain {
  position: absolute;
  left: 0;
  width: 100%;
  min-width: var(--page-width);
}

.vq-col {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: var(--page-width);
  height: 18.5rem;
}

.vq-tree { position: absolute; top: 12.0625rem; }

/* The little star over the mountains is the way back up: it glows on
   hover like the nav star and scrolls the page home when clicked */
.vq-star {
  position: absolute;
  left: 44.1875rem;
  top: 2.9375rem;
  cursor: pointer;
  transition: filter var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.vq-star:hover {
  filter: drop-shadow(0 0 0.5rem rgba(240, 244, 250, .85));
  transform: scale(1.15);
}

.copyright {
  position: absolute;
  left: 0;
  right: 0;
  top: 16.5625rem;
  margin: 0;
  text-align: center;
  font: 400 0.875rem/1 var(--font-ui);
  color: var(--ink);
  z-index: 1;
}

/* ==========================================================================
   Case study pages
   ========================================================================== */

.cs-main {
  position: relative;
  width: 72.5rem;
  margin: 0 auto;
}

/* Full-screen hero: copy left, mockup right, horizon arc landing at the
   bottom of the viewport. Content aligns to the same shell grid as the nav
   (78.25rem at 1440, scaling with the viewport); 1rem is only the floor at
   the smallest breakpoint. */
.cs-hero {
  position: relative;
  width: 100%;
}

/* Copy is always 736 wide; each page sets its own text-to-mockup gap inline
   (NGA 1.5rem, Naks 0.8125rem, ORA 4.625rem, Woven 5.25rem at 1440). Laptop mockups may
   bleed slightly past the shell's right edge; phones play with spacing. */
.cs-hero-inner {
  position: relative;
  width: 78.25rem;
  left: 50%;
  transform: translateX(-50%);
  min-height: calc(90vh - 6.875rem);
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 4.625rem;
}

.cs-hero-copy {
  width: 46rem;
  flex-shrink: 0;
}

/* Scroll-down cue sitting under the arc's crown */
.cs-scroll {
  position: absolute;
  left: 50%;
  bottom: calc(16vw - 3.125rem);
  transform: translateX(-50%);
  margin: 0;
  color: var(--muted);
  z-index: 1;
}

.cs-kicker {
  margin: 0;
  font: 600 1rem/1.5rem var(--font-display);
  letter-spacing: .08em;
  color: var(--accent-solid);
}

.cs-hero h1 {
  margin: var(--space-2) 0 0;
  max-width: 46rem;
  font: 700 3.5rem/4.375rem var(--font-display);
  letter-spacing: -.02em;
  color: var(--ink);
  text-wrap: pretty;
}

.cs-sub {
  margin: var(--space-2) 0 0;
  max-width: var(--measure-body);
  font: 500 1.375rem/2rem var(--font-display);
  color: var(--muted);
  text-wrap: pretty;
}

.cs-sub b { font-weight: 700; }

/* Columns hug their content, 24px apart, aligned under the title */
.cs-meta {
  display: flex;
  gap: 1.5rem;
  margin: var(--space-6) 0 0;
}

.cs-meta dt {
  font: 700 0.75rem/1.125rem var(--font-ui);
  letter-spacing: .08em;
  color: var(--accent-solid);
}

.cs-meta dd {
  margin: var(--space-2) 0 0;
  font: 400 0.875rem/1.25rem var(--font-ui);
  color: var(--muted);
}

.cs-hero-art {
  flex-shrink: 0;
  pointer-events: none;
}

.cs-hero-art img { filter: drop-shadow(0 0.625rem 1.25rem rgba(0, 0, 0, .10)); }

/* Sections share the homepage rhythm: centered kicker, then content */
.cs-section {
  margin-top: 18.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.cs-section > .kicker { margin-bottom: var(--space-12); }

.cs-row {
  display: flex;
  gap: var(--space-16);
  align-items: center;
  justify-content: center;
  width: 100%;
}

.cs-row--flip { flex-direction: row-reverse; }

/* Heading and body share one column beside the artwork. On the wide canvas
   this behaves exactly as the text block did before it was split in two; on
   phones the column dissolves so the artwork can sit between its halves. */
.cs-col {
  display: flex;
  flex-direction: column;
  max-width: 35rem;
}

.cs-text { max-width: 35rem; }

.cs-text .section-heading { max-width: none; }

.cs-text .section-body { margin-top: 1.625rem; }

/* Narrative stacks sit centered on the page but read ragged-right: the
   stack hugs its widest line, every child shares that left edge, and the
   whole block centers in the section */
.cs-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: fit-content;
  max-width: 100%;
  text-align: left;
}

.cs-stack .cs-caption { text-align: left; }

.cs-stack .section-body { margin-top: 1.625rem; }

/* Placeholder containers for images / interactive mockups / video */
.ph {
  background: rgba(19, 33, 58, .55);
  border-radius: var(--radius-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(159, 176, 200, .55);
  font: 500 0.875rem/1.25rem var(--font-ui);
  letter-spacing: .04em;
  text-align: center;
  flex-shrink: 0;
}

.ph--wide { width: 60rem; max-width: 100%; height: 32.5rem; margin-top: var(--space-16); }

.ph--half { width: 32.5rem; height: 25rem; }

.ph--screen { width: 15.625rem; height: 28.75rem; border-radius: var(--radius-md); }

.ph--tilt { transform: rotate(-4deg); }

.cs-caption {
  margin: var(--space-6) 0 0;
  font: 500 0.9375rem/1.625rem var(--font-ui);
  color: var(--muted);
  text-align: center;
}

.screen-strip {
  display: flex;
  gap: var(--space-6);
  justify-content: center;
  margin-top: var(--space-16);
}

.screen-strip figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  align-items: center;
}

.screen-strip figcaption {
  font: 500 0.9375rem/1.625rem var(--font-ui);
  color: var(--muted);
}

/* Woven stat blocks */
.stat-row {
  display: flex;
  gap: var(--space-16);
  justify-content: center;
  margin-top: var(--space-12);
}

.stat { max-width: 20rem; }

.stat b {
  display: block;
  font: 700 3.5rem/1.1 var(--font-display);
  letter-spacing: -.02em;
  color: var(--ink);
}

.stat p {
  margin: var(--space-3) 0 0;
  font: 500 0.9375rem/1.375rem var(--font-ui);
  color: var(--muted);
}

.stat cite {
  display: block;
  margin-top: var(--space-2);
  font: 400 0.75rem/1.125rem var(--font-ui);
  font-style: normal;
  color: rgba(159, 176, 200, .6);
}

/* Naks pull quotes */
.pull-quotes {
  display: flex;
  gap: var(--space-24);
  justify-content: center;
  margin-top: var(--space-16);
}

.pull-quote {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: center;
  text-align: center;
  max-width: 22.5rem;
}

.pull-quote b {
  font: 700 1.5rem/1 var(--font-display);
  color: var(--accent-solid);
}

.pull-quote p {
  margin: 0;
  font: 500 1.5rem/2rem var(--font-display);
  color: var(--ink);
}

/* ORA results — the copy requires a list, not a paragraph */
.result-list {
  margin: var(--space-8) 0 0;
  padding: 0;
  list-style: none;
  max-width: 38.75rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  text-align: left;
}

.result-list li {
  font: 500 1.5rem/2rem var(--font-display);
  color: var(--muted);
}

.result-list b { color: var(--ink); font-weight: 700; }

/* Naks page toggles: inactive secondary, active primary, one at a time */
.page-toggle {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  justify-content: center;
  margin-top: var(--space-16);
}

.page-toggle .btn {
  cursor: pointer;
  font-size: 0.875rem;
  padding: 0.5rem 1.25rem;
  /* tabs share the outline pill */
  background-image: none;
  background-color: transparent;
  border: 1px solid rgba(240, 244, 250, .4);
  border-radius: var(--radius-pill);
  color: var(--ink);
}

.page-toggle .btn.active {
  background: var(--accent);
  color: var(--bg);
  border-color: transparent;
}

/* The redesign prototype: a laptop floating on its own orbit ring, the
   video running inside its screen with no chrome */
.proto-wrap {
  position: relative;
  margin-top: var(--space-16);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* breathing room between the laptop and the orb's lower curve */
  margin-bottom: 4rem;
}

.laptop {
  --screen-w: 46rem;
  position: relative;
  filter: drop-shadow(0 1.25rem 2.5rem rgba(0, 0, 0, .10));
}

.laptop-screen {
  width: var(--screen-w);
  aspect-ratio: 16 / 10;
  background: #101013;
  border-radius: 1.125rem 1.125rem 0 0;
  border: 1px solid rgba(240, 244, 250, .08);
  padding: 0.875rem 0.875rem 0.75rem;
  box-sizing: border-box;
}

.laptop-screen video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
  border-radius: 0.375rem;
  display: block;
  background: var(--surface-deep);
}

/* The prototype fills the laptop screen it sits in */
.laptop--proto { --screen-w: min(64rem, 90vw); }

.proto-frame {
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: 0.375rem;
  display: block;
  background: #101013;
}

/* A short loop of the live site, playing only while it is on screen */
.site-clip {
  height: auto;
  display: block;
  border-radius: var(--radius-sm);
  filter: drop-shadow(0 0.5rem 1rem rgba(0, 0, 0, .10));
}

/* the lip overhangs both sides evenly, so the laptop still centres on the
   screen rather than half an overhang off */
.laptop-base {
  width: calc(var(--screen-w) + 7rem);
  height: 1.125rem;
  margin: 0 -3.5rem;
  background: linear-gradient(#31363f, #191c22);
  border-radius: 0.1875rem 0.1875rem 1.125rem 1.125rem;
  position: relative;
}

.laptop-base::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  width: 8.5rem;
  height: 0.4375rem;
  background: #101013;
  border-radius: 0 0 0.5rem 0.5rem;
}

/* Existing-pages slideshow: one page at a time beside the narrative,
   its top edge level with the section heading */
.cs-row--top { align-items: flex-start; }

.pg-slider {
  width: 38rem;
  max-width: 100%;
  flex-shrink: 0;
}

.pg-viewport { overflow: hidden; }

.pg-track { display: flex; transition: transform 320ms var(--ease); align-items: flex-start; }

.pg-slide {
  flex: 0 0 100%;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: zoom-in;
}

.pg-slide img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-sm);
  filter: drop-shadow(0 0.5rem 1rem rgba(0, 0, 0, .10));
  transition: transform var(--dur-fast) var(--ease), filter var(--dur-fast) var(--ease);
}

.pg-slide:hover img {
  transform: scale(1.02);
  filter: drop-shadow(0 0.75rem 1.5rem rgba(0, 0, 0, .14));
}

.pg-slide .cs-caption { margin-top: var(--space-4); }

.pg-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  margin-top: var(--space-6);
}

.pg-controls .circle-btn {
  position: static;
  transform: none;
  width: 2.5rem;
  height: 2.5rem;
}

.pg-dots { margin-top: 0; }

/* Pills and panels ride one entrance, so the controls and the thing they
   control land together instead of the panels arriving on their own */
.cs-how .how-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: min(92rem, 98vw);
}

/* The "how we got it done" column: heading, body, pills and the panel
   one-liners all hang from the same left edge */
.cs-how .cs-stack {
  width: min(46rem, 92vw);
  max-width: none;
}

/* heading and body both take the column so the block reads as one measure */
.cs-how .cs-stack .section-heading,
.cs-how .cs-stack .section-body { max-width: 100%; }

.cs-how .page-toggle {
  width: min(46rem, 92vw);
  justify-content: flex-start;
}

/* Before/after panels: problem in red, recommendation in cyan, then the
   annotated pages under a draggable split */
.ba-panels {
  width: min(92rem, 98vw);
  margin-top: 2.5rem;
}

.ba-panel { display: none; }

.ba-panel.ba-active {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.ba-panel .section-body {
  width: 100%;
  margin: 0;
  text-align: left;
}

/* What testers said: the confusion collected inside one quiet orbit
   ellipse, louder quotes carrying the story */
/* Tall enough to hold the full lap without the satellites clipping */
.quote-field {
  position: relative;
  width: min(78rem, 94vw);
  aspect-ratio: 1.62 / 1;
  margin-top: var(--space-16);
}

/* Satellites share one size and the muted tone; only the central quote
   is bigger and in ink. All of them speak Space Grotesk. */
.qf {
  position: absolute;
  left: 50%;
  top: 50%;
  margin: 0;
  font: 600 1rem/1.5 var(--font-display);
  color: var(--muted);
  width: 17rem;
  transform: translate(-50%, -50%);
}

.q-caps {
  text-transform: uppercase;
  letter-spacing: .05em;
}

.q-xl {
  font: 700 1.625rem/1.4 var(--font-display);
  color: var(--ink);
  width: 30rem;
  text-align: center;
}

/* Satellites hold their distance from the main quote, spread around it by
   their start angle; with motion off they simply rest there */
.qf-orbit {
  transform: translate(-50%, -50%)
             rotate(var(--oa, 0deg)) translate(var(--rx)) rotate(calc(-1 * var(--oa, 0deg)));
}

/* One-liners above each slider: a fixed-width block so every panel's
   lines share the same left edge as the narrative above */
.ba-lines {
  width: min(46rem, 100%);
}

.ba-lines .section-body { margin: 0; text-align: left; width: auto; }

.ba-lines .ba-fix { margin-top: 0.5rem; }

/* Only the labels carry color */
.ba-problem b { color: #E63946; font-weight: 700; }

.ba-fix b { color: var(--accent-solid); font-weight: 700; }

/* Sits centered under the issue and recommendation lines */
.ba-hint {
  margin: var(--space-8) 0 0;
  font: 400 0.8125rem/1.5 var(--font-ui);
  color: var(--muted);
  text-align: center;
  width: 100%;
}

/* The tags sit over the page screenshot's own edges (the canvas is wider
   than the screen because the annotation margins are part of it) */
.ba-heads {
  width: 100%;
  display: flex;
  justify-content: space-between;
  margin: 0.875rem 0 0.625rem;
  padding-left: 30.6%;
  padding-right: 30.6%;
  box-sizing: border-box;
}

.ba-tag {
  font: 700 0.75rem/1 var(--font-display);
  letter-spacing: .1em;
  transition: opacity 200ms var(--ease);
}

.ba-tag--before { color: #E63946; }

.ba-tag--after { color: var(--accent-solid); }

.ba-slider {
  position: relative;
  width: 100%;
  display: grid;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}

.ba-img {
  grid-area: 1 / 1;
  width: 100%;
  height: auto;
  align-self: start;
  display: block;
  pointer-events: none;
  -webkit-user-drag: none;
}

.ba-img--after {
  clip-path: inset(0 0 0 var(--pos, 50%));
  will-change: clip-path;
}

/* A quiet accent seam marks the split; grab it (or anywhere) and drag */
.ba-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos, 50%);
  width: 2px;
  margin-left: -1px;
  background: rgba(82, 199, 240, .35);
  cursor: ew-resize;
}

.ba-divider::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -0.75rem;
  right: -0.75rem;
}

/* The grab pill riding the seam — pinned near the top so it's in view
   even on the tallest pages */
.ba-handle {
  position: absolute;
  top: min(50%, 18rem);
  left: 50%;
  transform: translate(-50%, -50%);
  white-space: nowrap;
  font: 600 0.6875rem/1 var(--font-display);
  letter-spacing: .06em;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid rgba(82, 199, 240, .35);
  border-radius: 999px;
  padding: 0.4375rem 0.75rem;
  pointer-events: none;
}

/* Sign-off + in-orbit */
.gracias {
  margin-top: 18.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.gracias .cs-kicker { font-size: 1.5rem; }

.gracias .section-heading { margin-top: var(--space-4); }

.gracias .section-body { margin-top: 1.625rem; }

.in-orbit {
  margin-top: 18.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-16);
}

/* Three equal columns keep the middle mockup dead-center in the viewport
   even though the artifacts differ in width */
.orbit-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  justify-items: center;
  align-items: end;
  width: min(80rem, 96vw);
}

.orbit-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  align-items: center;
  font: 700 1.125rem/1.625rem var(--font-display);
  color: var(--ink);
  transition: transform var(--dur-fast) var(--ease);
}

.orbit-card img { filter: drop-shadow(0 0.5rem 1rem rgba(0, 0, 0, .08)); }

/* Hover reveals an orbit ring under the whole card — mockup and name both
   ride it, echoing the homepage carousel — while the card grows */
.orbit-card::before {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -0.875rem;
  width: 130%;
  min-width: 20rem;
  height: 3.5rem;
  transform: translateX(-50%);
  border-radius: 50%;
  background: rgba(19, 33, 58, .85);
  box-shadow: inset 0 0 0 1.5px var(--accent-arc), 0 0.375rem 2rem rgba(82, 199, 240, .16);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
  z-index: -1;
}

.orbit-card:hover::before { opacity: 1; }

/* The three mockups float along the footer's lift ellipse, waiting to be
   clicked: the middle rides the crown, the outer two sit lower on the curve */
.orbit-card:nth-child(1) { transform: rotate(-6deg) translateY(5rem); }
.orbit-card:nth-child(2) { transform: rotate(-2deg); }
.orbit-card:nth-child(3) { transform: rotate(5deg) translateY(3.5rem); }

.orbit-card:nth-child(1):hover { transform: rotate(-6deg) translateY(5rem) scale(1.05); }
.orbit-card:nth-child(2):hover { transform: rotate(-2deg) scale(1.05); }
.orbit-card:nth-child(3):hover { transform: rotate(5deg) translateY(3.5rem) scale(1.05); }

.orbit-card:hover img { filter: drop-shadow(0 0.75rem 1.5rem rgba(0, 0, 0, .14)); }

/* Case pages: pull the contact lift up so the in-orbit cards hover right
   above its crown */
.page:has(.in-orbit) .contact-wrap { margin-top: 10rem; }

/* Homepage carousel cards are links into their case pages */
.slide-link {
  display: block;
  height: 100%;
  color: inherit;
}

.work-focus {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  margin-top: calc(55vh - 16vw);
}

/* ==========================================================================
   The journey — one persistent earth behind hero, work, and about.
   The canvas rides a sticky full-viewport stage; sections flow over it.
   ========================================================================== */
.journey { position: relative; }

/* The nav is fixed with its own fill everywhere now — the homepage no longer
   needs a special positioning scheme or the gradient scrim it used to carry */

.earth-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  margin-bottom: -100vh;
  z-index: 0;
  pointer-events: none;
}

#earth-canvas { display: block; }

.journey > section,
.journey > .j-exit { position: relative; z-index: 1; }

/* Work: intro flows past, then the case deck pins while the earth rotates
   city to city. Each case slides in from the left and settles. */
.work-head {
  min-height: 52vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-top: 8vh;
}

/* This copy floats over the planet — it reads in ink, not muted */
.work-head .section-body { color: var(--ink); }

/* …and it only arrives once the planet is actually behind it */
.wh-gate {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  opacity: 0;
  transition: opacity 420ms var(--ease);
}

.work-head.lit .wh-gate { opacity: 1; }

.wj-scroll {
  margin-top: 2.5rem;
  color: var(--muted);
}

/* full width: .work is a centered flex column, so without this the deck
   collapses to zero and the cases position from the page's middle */
.wj-deck { height: 340vh; position: relative; width: 100%; }

.wj-sticky { position: sticky; top: 0; height: 100vh; width: 100%; }

/* Front and present: the case sits between the left margin and the planet
   rather than hugging the edge, so the gap on either side of it reads even.
   Explicit width so the mockups scale proportionally instead of the
   block shrink-wrapping them down. */
.wj-case {
  position: absolute;
  left: 18vw;
  top: 52%;
  width: min(40vw, 44rem);
  transform: translate(-2.5rem, -50%);
  opacity: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  color: inherit;
  transition: opacity 450ms var(--ease), transform 450ms var(--ease);
  pointer-events: none;
}

.wj-case.active {
  opacity: 1;
  transform: translate(0, -50%);
  pointer-events: auto;
}

/* Hover: no ring behind it, the case simply drifts a touch further in */
.wj-case.active:hover { transform: translate(1.25rem, -50%); }

.wj-case .slide-oneliner { max-width: 100%; }

/* Narrow phone mockups center in the block instead of hugging its left
   edge; wide laptops already fill it */
.wj-art {
  margin: 0 0 2rem;
  align-self: center;
}

.wj-art img {
  max-width: 100%;
  height: auto;
  display: block;
  position: relative;
  filter: drop-shadow(0 0.75rem 1.5rem rgba(0, 0, 0, .12));
}


/* About pins for a beat while the earth zooms down to Vieques.
   It plays over the zoomed ocean, so the kicker inverts: sea-blue fill,
   deep-navy text and orbit line. */
.j-about { min-height: 200vh; }

/* Over the planet the chip matches the field exactly, so the orbit line
   simply stops at the label instead of crossing a tinted box */
.journey .kicker.on-world span { color: var(--ink); }

/* The about kicker's orbit holds itself undrawn until the close view
   arrives, then draws with the same sweep every other kicker uses */
.j-about:not(.in-focus) .kicker::before { opacity: 0; }

/* The place name that appears when a node is hovered */
.earth-tip {
  position: absolute;
  transform: translate(-50%, -190%);
  padding: 0.375rem 0.75rem;
  font: 600 0.8125rem/1 var(--font-display);
  letter-spacing: .04em;
  color: var(--ink);
  background: rgba(13, 27, 46, .82);
  border: 1px solid var(--accent-arc);
  border-radius: var(--radius-pill, 999px);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms var(--ease);
  z-index: 5;
}

.earth-tip.on { opacity: 1; }

/* The canvas lays a navy haze under this copy once the planet fills the
   frame, so the block keeps the site's own ink and clears AA either way */
.j-about .section-heading,
.j-about .cred-head,
.j-about .cred-title { color: var(--ink); }

.j-about .cred-detail { color: #C7D4E6; }

.j-about-sticky {
  position: sticky;
  top: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.j-exit { height: 70vh; }

/* Reduced motion / no JS: everything reads as a normal page */
html:not(.fx) .wj-deck { height: auto; }
html:not(.fx) .wj-sticky { position: static; height: auto; }
html:not(.fx) .wj-case {
  position: static;
  opacity: 1;
  transform: none;
  pointer-events: auto;
  margin: 4rem auto;
  max-width: 46rem;
}
html:not(.fx) .j-about { min-height: 0; }
html:not(.fx) .j-exit { height: 0; }

/* Narrow viewports: earth stays centered and small; content stacks under it */
@media (max-width: 900px) {
  .wj-case {
    left: 50%;
    top: auto;
    bottom: 6vh;
    max-width: 88vw;
    transform: translate(-50%, 1.5rem);
  }
  .wj-case.active { transform: translate(-50%, 0); }
  .wj-case.active:hover { transform: translate(-50%, -0.75rem); }
}

/* ==========================================================================
   Case-study furniture: captioned media, solution orb, idea fan, sign-off
   ========================================================================== */

/* Screenshot media with its source caption underneath, tilted slightly as
   if drifting in space (the portrait's language) */
.cs-media {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  align-items: center;
  flex-shrink: 0;
}

.cs-media .ph,
.cs-media svg,
.cs-media img { transform: rotate(-4deg); }

/* Variants: some artifacts sit straight, some lean the other way */
.cs-media--flat .ph,
.cs-media--flat svg,
.cs-media--flat img { transform: none; }

.cs-media--tilt-r img { transform: rotate(4deg); }

/* The buyer-journey graphic: chart rebuilt on portfolio tokens */
.cs-media .journey {
  width: 32.5rem;
  height: auto;
  display: block;
}

.cs-media .cs-caption { margin: 0; }

/* Left-aligned narrative block (centered as a group, text ragged-right) */
.cs-left {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  max-width: 46rem;
}

.cs-left .section-body { margin-top: 1.625rem; }

/* THE SOLUTION lives inside a giant ellipse — a soft pool of color fully
   visible top to bottom (never cut by a clip edge); only its sides run
   past the viewport */
.cs-solution {
  position: relative;
  /* block-flow child of .cs-main: shift to full-bleed viewport center */
  left: calc(50% - 50vw);
  width: 100vw;
  padding: 6rem 0 7rem;
  /* the orb tint over the field blends to ~surface-deep; kicker chips match */
  --kicker-bg: var(--surface-deep);
}

.solution-orb {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 200vw;
  height: 100%;
  border-radius: 50%;
  background: rgba(19, 33, 58, .5);
  box-shadow: inset 0 0 0 2px var(--accent-line);
  pointer-events: none;
}

.cs-solution > *:not(.solution-orb) { position: relative; z-index: 1; }

/* Bulleted, left-aligned solution copy */
.sol-list {
  margin: 1.5rem 0 0;
  padding-left: 1.5rem;
  max-width: 44rem;
  text-align: left;
  font: 500 1.375rem/2rem var(--font-display);
  color: var(--muted);
}

.sol-list li + li { margin-top: .25rem; }

.sol-list b { color: var(--ink); }

/* Screens ride the orb's curve: outer ones higher, following the bend */
.cs-solution .screen-strip figure:nth-child(1),
.cs-solution .screen-strip figure:nth-child(4) { transform: translateY(-3.5rem); }

/* The list block hugs its text so the stack reads centered and balanced */
.sol-list { width: fit-content; }

/* A solution intro that speaks to the whole room: heading and its lines sit
   on the page centre, with whatever follows keeping its own alignment */
.sol-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  max-width: min(52rem, 92vw);
}

.sol-intro .section-body { margin-top: 1.625rem; }

/* the aside that follows the last line, quieter and narrower than the copy */
.sol-intro .cs-caption { margin-top: var(--space-4); max-width: 34rem; }

/* Mockup-video phones on the orb's inner curve — the standard phone frame
   treatment (radius-md, surface fill, accent hairline, quiet shadow) */
.phone-strip {
  display: flex;
  gap: 1.5rem;
  justify-content: center;
  align-items: flex-end;
  margin-top: 4rem;
}

.phone-strip figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  align-items: center;
  cursor: zoom-in;
}

.phone-strip figcaption {
  font: 500 0.9375rem/1.625rem var(--font-ui);
  color: var(--muted);
}

/* iPhone device mockup around each recording, matching the mockups the
   videos were presented in */
.phone-frame {
  position: relative;
  display: grid;
  grid-template: 1fr / 1fr;
  width: 13.75rem;
  aspect-ratio: 393 / 852;
  border-radius: 2.25rem;
  background: #101013;
  padding: 0.4rem;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .12), 0 0.75rem 1.5rem rgba(0, 0, 0, .10);
}

/* Still and recording share one cell. The still carries the screen at all
   times, so the phone is never an empty white rectangle while a recording
   is idle, buffering, or unplayable. */
.phone-frame .phone-still,
.phone-frame video {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: 1.9rem;
  background: #101013;
}

/* The recording lies over the still and only shows once frames are
   actually rendering, the way the ORA screens do */
.phone-frame video {
  opacity: 0;
  transition: opacity 200ms var(--ease);
  pointer-events: none;
}

.phone-frame video.live { opacity: 1; }

/* dynamic island */
.phone-frame::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 0.85rem;
  transform: translateX(-50%);
  width: 28%;
  height: 0.75rem;
  border-radius: 999px;
  background: #000;
}

/* distributed along the orb's bottom curve, like the Woven solution:
   outer phones ride higher as the curve rises toward the edges */
.phone-strip figure:nth-child(1),
.phone-strip figure:nth-child(5) { transform: translateY(-2.25rem); }

.phone-strip figure:nth-child(2),
.phone-strip figure:nth-child(4) { transform: translateY(-1rem); }

/* ORA solution stills: framed screenshots ride the same curve; captions
   carry the research number or quote that backs each feature */
/* Captions differ in length, so pin the stills to the strip's top edge:
   the phones keep the curve, the text hangs below at its own height */
.phone-strip--stills figure { align-self: flex-start; }

.phone-strip--stills img {
  width: 13.75rem;
  height: auto;
  display: block;
  filter: drop-shadow(0 0.5rem 1rem rgba(0, 0, 0, .10));
}

.phone-strip--stills figcaption {
  width: 13.75rem;
  text-align: left;
  margin-top: 0.5rem;
}

.strip-stat {
  display: block;
  font: 400 0.8125rem/1.45 var(--font-ui);
  color: var(--muted);
  margin-top: 0.25rem;
}

/* Live sections: a screen recording sits inside the still's bezel and
   fades in once it is actually playing, NGA-style */
.ora-shell {
  position: relative;
  display: block;
}

.ora-shell .ora-screen {
  position: absolute;
  left: 4.6%;
  top: 2.1%;
  width: 90.8%;
  height: 95.8%;
  object-fit: cover;
  border-radius: 0.9375rem;
  opacity: 0;
  transition: opacity 200ms var(--ease);
  pointer-events: none;
}

.ora-shell .ora-screen.live { opacity: 1; }

/* Cut-features slideshow: one lo-fi board at a time, annotations kept on
   the sides the originals used, arrows aimed at their phones */
.wf-slider {
  position: relative;
  margin-top: var(--space-16);
  width: 100vw;
}

.wf-viewport { overflow: hidden; width: 100%; }

.wf-track { display: flex; transition: transform 320ms var(--ease); }

.wf-slide {
  flex: 0 0 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.wf-title {
  margin: 0 0 2.5rem;
  font: 700 1rem/1.5 var(--font-display);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-solid);
  text-align: center;
}

.wf-units {
  display: flex;
  gap: 3rem;
  justify-content: center;
  align-items: flex-start;
}

.wf-units--map { gap: 2rem; }

.wf-unit {
  position: relative;
  display: flex;
  gap: 0.75rem;
  align-items: stretch;
}

.wf-phone {
  width: 12.5rem;
  height: auto;
  align-self: flex-start;
  flex-shrink: 0;
  border-radius: 0.25rem;
}

.wf-units--map .wf-phone { width: 10.5rem; }

.wf-side { position: relative; width: 11rem; }

.wf-units--map .wf-side { width: 9rem; }

.wf-note {
  position: absolute;
  left: 0;
  right: 0;
  margin: 0;
  font: 400 0.8125rem/1.45 var(--font-ui);
  color: var(--muted);
  text-align: left;
}

.wf-side--l .wf-note { text-align: right; }

.wf-note .wf-arrow {
  color: var(--accent-solid);
  vertical-align: -0.125rem;
  margin: 0 0.125rem;
}

.wf-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.75rem;
  margin-top: var(--space-8);
}

.wf-controls .circle-btn {
  position: static;
  transform: none;
}

.wf-dots { margin-top: 0; }

/* Usability results as a quiet table on the portfolio tokens: a faint
   blue surface holds it together beside the narrative */
.test-table {
  border-collapse: separate;
  border-spacing: 0;
  background: rgba(19, 33, 58, .55);
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px rgba(82, 199, 240, .12);
  padding: 1.25rem 1.75rem;
}

.tt-wrap { flex-shrink: 0; }

.tt-wrap .cs-caption { margin-top: var(--space-4); }

.test-table th {
  font: 600 0.875rem/1.5 var(--font-display);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
  text-align: left;
  padding: 0.75rem 2.5rem 0.75rem 0;
  border-bottom: 1px solid var(--divider);
}

.test-table td {
  font: 400 0.9375rem/1.5 var(--font-ui);
  color: var(--muted);
  text-align: left;
  padding: 0.75rem 2.5rem 0.75rem 0;
  border-bottom: 1px solid rgba(159, 176, 200, .12);
}

.test-table td:first-child { color: var(--ink); }

.test-table tr:last-child td { border-bottom: none; }

.tt-dot {
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--sig, var(--accent-solid));
  margin-right: 0.625rem;
  vertical-align: 0.0625rem;
}

/* "Try it yourself" — a button in the flow; the interactive canvas opens
   as a modal so it doesn't eat the section. The card's interior follows
   the Art or Fart system (NGA tokens), not the portfolio system. */
.tryit {
  margin-top: 4rem;
  display: flex;
  justify-content: center;
}

.try-modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(13, 27, 46, .88);
}

.try-modal.open { display: flex; }

.tryit-phone {
  position: relative;
  width: 26rem;
  max-height: 94vh;
  background: #FFFFFF;
  border-radius: var(--radius-md);
  padding: 1.25rem;
  box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, .35);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  color: #000;
  font-family: var(--font-ui);
}

.tp-close {
  position: absolute;
  right: 1rem;
  top: 0.85rem;
  background: none;
  border: 0;
  font: 700 1.25rem var(--font-ui);
  color: #000;
  cursor: pointer;
}

.tp-label {
  margin: 0;
  font: 700 1rem/1.2 var(--font-ui);
  color: #3D3D3D;
}

.tp-theme {
  margin: 0;
  text-align: center;
  font: 700 1.125rem/1.5rem 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
  color: #000;
}

.tp-style { color: #FFB200; font-family: var(--font-ui); }

.tp-object { color: #00D6A4; font-family: var(--font-ui); }

.tp-canvas {
  position: relative;
  height: min(34rem, 52vh);
  border: 2px solid #000;
  border-radius: 0.625rem;
  overflow: hidden;
}

.tp-canvas canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: crosshair;
}

.tp-tools {
  display: flex;
  justify-content: space-around;
  align-items: center;
  border: 2px solid #000;
  border-radius: 6.25rem;
  padding: 0.5rem;
  background: #FFFFFF;
}

.tp-tool {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  border: 2.5px solid #000;
  background: #FFFFFF;
  display: grid;
  place-items: center;
  cursor: pointer;
  padding: 0;
}

/* the box is square, the icons are not; contain keeps them undistorted */
.tp-tool img { width: 1.25rem; height: 1.25rem; object-fit: contain; }

.tp-tool.on { background: #000; }

.tp-tool.on img { filter: invert(1); }

.tp-color i {
  display: block;
  width: 1.875rem;
  height: 1.875rem;
  border-radius: 50%;
}

.tp-reset {
  border: 0;
  border-radius: 6.25rem;
  background: #4C40FF;
  color: #FFFFFF;
  font: 700 1.125rem var(--font-ui);
  height: 3rem;
  cursor: pointer;
}

/* auto brush = brush icon with a tiny star */
.tp-auto { position: relative; }

.tp-auto .star-badge {
  position: absolute;
  right: 1px;
  top: 1px;
  width: 0.8rem;
  height: 0.8rem;
}

.tp-auto.on .star-badge { filter: invert(1); }

/* pickers that pop above the toolbar (colors / stamp patterns) */
.tp-pop {
  position: absolute;
  bottom: 7.75rem;
  left: 50%;
  transform: translateX(-50%);
  background: #FFFFFF;
  border: 2px solid #000;
  border-radius: 1rem;
  padding: 0.6rem 0.75rem;
  display: none;
  gap: 0.5rem;
  align-items: center;
  z-index: 5;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, .15);
}

.tp-pop.open { display: flex; }

.tp-sw {
  width: 1.875rem;
  height: 1.875rem;
  border-radius: 50%;
  border: 2px solid #000;
  cursor: pointer;
  padding: 0;
}

.tp-sw.on { outline: 3px solid #4C40FF; outline-offset: 2px; }

.tp-stamp-opt {
  font: 700 0.875rem var(--font-ui);
  border: 2px solid #000;
  border-radius: 999px;
  padding: 0.4rem 0.8rem;
  background: #FFFFFF;
  color: #000;
  cursor: pointer;
}

.tp-stamp-opt.on { background: #000; color: #FFFFFF; }

/* Cut ideas orbit radially along their own arc, tilted with the bend;
   they may bleed past the margins but never the viewport */
.cs-fan {
  position: relative;
  width: 100vw;
  margin-top: 4rem;
  /* deep enough bottom padding that the arc's visible band widens past the
     viewport edges before the overflow clip — a shallow band dies mid-air */
  padding: 4rem 0 10rem;
  overflow: hidden;
  display: flex;
  justify-content: center;
}

/* The full-bleed horizon arc (same geometry as the hero's) runs edge to
   edge fully below the cut ideas and their captions */
.fan-arc {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 38.5rem;
  width: max(177.5rem, 115vw);
  aspect-ratio: 2840 / 1500;
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px var(--accent-arc);
  pointer-events: none;
}

.fan-cards {
  position: relative;
  display: flex;
  gap: 4rem;
  justify-content: center;
  align-items: flex-end;
}

.fan-cards .idea {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.fan-cards .idea img {
  height: 26rem;
  width: auto;
  display: block;
  filter: drop-shadow(0 0.5rem 1rem rgba(0, 0, 0, .10));
}

.fan-cards .idea figcaption {
  max-width: 16rem;
  text-align: center;
}

.fan-cards .idea figcaption b {
  display: block;
  font: 600 0.9375rem/1.4 var(--font-ui);
  color: var(--ink);
}

.fan-cards .idea figcaption span {
  font: 400 0.8125rem/1.45 var(--font-ui);
  color: var(--muted);
}

.fan-cards .idea:nth-child(1) { transform: rotate(-9deg) translateY(1.5rem); }
.fan-cards .idea:nth-child(2) { transform: rotate(-3.5deg); }
.fan-cards .idea:nth-child(3) { transform: rotate(3.5deg) translateY(0.25rem); }
.fan-cards .idea:nth-child(4) { transform: rotate(9deg) translateY(1.75rem); }

/* ¡Gracias! sits inside a quiet stroke-only ellipse */
.gracias { position: relative; padding: 5rem 0; }

.gracias-orb {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(75rem, 96vw);
  height: 120%;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--accent-line);
  pointer-events: none;
}

.gracias > *:not(.gracias-orb) { position: relative; z-index: 1; }

/* Screen lightbox: click a screen to examine it enlarged, close, tap the
   next one */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(13, 27, 46, .88);
  cursor: zoom-out;
}

.lightbox.open { display: flex; }

.lightbox .lb-item {
  max-width: 72vw;
  max-height: 82vh;
  cursor: default;
  overflow: auto;
}

/* Enlarged images can be clicked once more to magnify and pan */
.lightbox img.lb-zoomable { cursor: zoom-in; }

.lightbox img.lb-zoomed {
  max-width: none;
  max-height: none;
  width: 110rem;
  cursor: zoom-out;
}

.lightbox .lb-item .ph {
  width: 24rem;
  height: 44rem;
  transform: none;
}

.lightbox img {
  max-width: 72vw;
  max-height: 82vh;
  border-radius: var(--radius-md);
}

/* The playable prototype opens phone-sized in the lightbox */
.lightbox iframe {
  width: 26rem;
  height: min(54rem, 88vh);
  border: 0;
  border-radius: var(--radius-md);
  background: #0D1B2E;
}

.lightbox video {
  max-width: 72vw;
  max-height: 82vh;
  border-radius: var(--radius-md);
  display: block;
}

/* Enlarged inline graphics (e.g. the buyer-journey chart) sit on a quiet
   panel so the strokes stay legible over the dimmed page */
.lightbox svg {
  width: min(72vw, 64rem);
  height: auto;
  display: block;
  background: var(--surface-deep);
  border-radius: var(--radius-md);
  padding: 2.5rem;
  box-sizing: border-box;
  transform: none;
}

.screen-strip figure,
figure.cs-media,
.cs-hero-art { cursor: zoom-in; }

/* ==========================================================================
   SVG chrome — width/height attributes don't scale with rem, so size the
   attribute-sized pieces here
   ========================================================================== */

.nav-star svg:not(.logo-mark) { width: 2.5rem; height: 2.5rem; }

.moon svg { width: 8rem; height: 8rem; }

.cs-scroll svg { width: 1.5rem; height: 1.5rem; }

.circle-btn svg { width: 1.375rem; height: 1.375rem; }

.contact .section-heading svg { width: .875rem; height: .875rem; }

.vq-tree { width: 10.343rem; height: 5.489rem; }

.vq-star { width: 1.625rem; height: 1.625rem; }

/* ==========================================================================
   Motion — everything gated on prefers-reduced-motion.
   Hidden states only apply once JS confirms it's running (html.fx),
   so content is never lost without JS.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }

  /* Directional reveals. Each fires once and stays — content does not
     re-animate when you scroll back past it. Stagger with --d, hand-set in
     the markup, or handed out by main.js when siblings arrive together. */
  .reveal {
    transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
    transition-delay: 0s;
    will-change: opacity, transform;
  }

  .reveal.in { transition-delay: var(--d, 0s); }

  /* Small travel, per the spec: opacity plus a 16px drift. The old version
     grew everything from 72% scale, which read as content lunging at the
     screen; a short rise that settles reads as it arriving. */
  html.fx .reveal:not(.in) { opacity: 0; }
  html.fx .reveal-up:not(.in) { transform: translateY(1rem); }
  html.fx .reveal-down:not(.in) { transform: translateY(-1rem); }
  html.fx .reveal-left:not(.in) { transform: translateX(-1rem); }
  html.fx .reveal-right:not(.in) { transform: translateX(1rem); }
  html.fx .reveal-zoom:not(.in) { transform: scale(.96); }

  /* Star-first hero entrance: the container itself never hides — the white
     key words twinkle in first, the muted text follows */
  html.fx .reveal-stars:not(.in) { opacity: 1; }

  @keyframes bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(0.375rem); }
  }

  .cs-scroll svg { animation: bob 2.2s ease-in-out infinite; }

  /* The kaomoji blows up and settles when its section is discovered */
  .kicker .kao { display: inline-block; }

  @keyframes kao-pop {
    0% { transform: scale(1); }
    35% { transform: scale(1.7); }
    100% { transform: scale(1); }
  }

  html.fx .kicker.in .kao { animation: kao-pop 1.4s var(--ease) .5s both; }

  /* fill-opacity, not opacity: SVG's opacity property does not apply to
     tspan elements at all, so when the hero merged into tspans-on-textPaths
     the staged entrance silently stopped. fill-opacity is honored by text
     content elements and transitions the same way. */
  html.fx .h1-orbit .hb,
  html.fx .hero-arcs .hb,
  html.fx .hero-orbit-info .hb {
    fill-opacity: 0;
    transition: fill-opacity .8s var(--ease) var(--wd, 0s);
  }

  html.fx .h1-orbit .dim,
  html.fx .hero-arcs .dim,
  html.fx .hero-orbit-info .dim {
    fill-opacity: 0;
    transition: fill-opacity 1s var(--ease) .9s;
  }

  html.fx .h1-orbit.in .hb,
  html.fx .h1-orbit.in .dim,
  html.fx .hero-arcs.in .hb,
  html.fx .hero-arcs.in .dim,
  html.fx .hero-orbit-info.in .hb,
  html.fx .hero-orbit-info.in .dim { fill-opacity: 1; }

  /* No-gravity drift: artwork bobs gently in its own space */
  @keyframes floaty {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-0.625rem); }
  }

  @keyframes floaty-tilt {
    0%, 100% { transform: rotate(-6.1deg) translateY(0); }
    50% { transform: rotate(-6.1deg) translateY(-0.5625rem); }
  }

  @keyframes floaty-tilt4 {
    0%, 100% { transform: rotate(-4deg) translateY(0); }
    50% { transform: rotate(-4deg) translateY(-0.5625rem); }
  }

  @keyframes floaty-tilt4r {
    0%, 100% { transform: rotate(4deg) translateY(0); }
    50% { transform: rotate(4deg) translateY(-0.5625rem); }
  }

  /* the about moon keeps its gentle drift in its new home */
  .moon svg { animation: floaty 8s ease-in-out infinite; }

  @keyframes wj-bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(0.5rem); }
  }

  .wj-scroll { animation: wj-bob 2s ease-in-out infinite; }

  /* One very slow lap around the question; each satellite enters the cycle
     at its own offset via a negative delay, so they stay spread out. The
     inner counter-rotation keeps every line upright as it travels. */
  @keyframes qf-lap {
    from { transform: translate(-50%, -50%) rotate(0deg) translate(var(--rx)) rotate(0deg); }
    to   { transform: translate(-50%, -50%) rotate(360deg) translate(var(--rx)) rotate(-360deg); }
  }

  .qf-orbit {
    animation: qf-lap 150s linear infinite;
    animation-delay: var(--od, 0s);
    will-change: transform;
  }

  .slide-art img { animation: floaty 7s ease-in-out infinite; }

  .portrait { animation: floaty-tilt 8s ease-in-out infinite; }

  /* case-study imagery drifts like the portrait (solution screens excepted) */
  .cs-media img,
  .cs-media svg { animation: floaty-tilt4 7.5s ease-in-out infinite; }

  .cs-media--flat img,
  .cs-media--flat svg { animation: floaty 7.5s ease-in-out infinite; }

  .cs-media--tilt-r img { animation: floaty-tilt4r 7.5s ease-in-out infinite; }

  .fan-cards .idea img,
  .orbit-card img { animation: floaty 7s ease-in-out infinite; }


  /* Warp: clicking a case study zooms into that card (transform-origin is
     set to the click target by JS), its orbit ring expands outward into
     what becomes the case page's horizon arc, and the case hero pops in
     via its own reveals */
  .page {
    transition: opacity .55s var(--ease), transform .55s var(--ease);
  }

  body.warp .page {
    opacity: 0;
    transform: scale(1.6);
  }

  body.warp .warp-focus .orbit-ring {
    transform: scale(2.6);
    opacity: 0;
    transition: transform .55s var(--ease), opacity .5s var(--ease);
  }

  /* Hyperspace streaks: scroll velocity stretches the stars into short
     light-trails that relax as you decelerate into the next quiet moment */
  #stars .star { transform: scaleY(var(--stretch, 1)); }

  .carousel-track { transition: transform var(--dur-mid) var(--ease); }

  /* Line-draw reveals: hidden state is the undrawn stroke; entering view
     draws it left to right, leaving view resets it instantly for a replay */
  html.fx .horizon-draw { stroke-dasharray: 1; stroke-dashoffset: 1; }

  /* Slight overshoot past 0 so the drawn line always reaches the far right
     edge with nothing left undrawn */
  html.fx .horizon.in .horizon-draw {
    stroke-dashoffset: -0.04;
    transition: stroke-dashoffset 2.2s var(--ease);
  }

  /* Sweep finished: no dash left to undo, so the arc simply stays */
  html.fx .horizon.drawn .horizon-draw {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }

  html.fx .horizon.drawn { opacity: 1; }

  html.fx .kicker-svg path { stroke-dasharray: 1; stroke-dashoffset: 1; }

  html.fx .kicker.in .kicker-svg path {
    stroke-dashoffset: 0;
    transition: stroke-dashoffset 1.6s var(--ease) var(--d, 0s);
  }

  /* …except the about kicker, which waits for the close view */
  html.fx .j-about:not(.in-focus) .kicker.in .kicker-svg path {
    stroke-dashoffset: 1;
  }

  #stars .star { animation: twinkle var(--tw-dur, 6s) ease-in-out var(--tw-delay, 0s) infinite; }

  @keyframes twinkle {
    0%, 100% { opacity: var(--o1, .35); }
    50% { opacity: var(--o2, .7); }
  }
}

/* ==========================================================================
   Woven × Pratt — driving question, findings board, pillars
   ========================================================================== */

/* The question the whole study hangs on: bigger than a section heading,
   centered, its own beat on the page */
.cs-question {
  width: min(56rem, 92vw);
  margin: var(--space-16) 0 0;
  font: 700 2.75rem/3.4rem var(--font-display);
  letter-spacing: -.015em;
  color: var(--ink);
  text-align: center;
  text-wrap: pretty;
}
/* Quotes handed back by the client team carry an attribution line */
.pull-quote--said p { font-weight: 400; font-size: 1.25rem; line-height: 1.9rem; }

.pull-quote cite {
  font: 500 0.875rem/1.5 var(--font-ui);
  font-style: normal;
  color: var(--accent-solid);
}

@media (max-width: 900px) {
  .cs-question { font-size: 2rem; line-height: 2.6rem; }
}

/* Solution captures are desktop screenshots, two across */
.screen-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 3rem 3.5rem;
  width: min(66rem, 92vw);
  margin-top: var(--space-16);
}

.screen-grid figure,
.screen-single {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.screen-single {
  width: min(32rem, 92vw);
  margin: var(--space-16) 0 0;
}

.screen-grid figcaption,
.screen-single figcaption {
  font: 500 0.9375rem/1.625rem var(--font-ui);
  color: var(--muted);
}

/* Browser-shaped placeholder for the desktop captures */
.ph--shot {
  width: 100%;
  aspect-ratio: 16 / 11;
  border-radius: var(--radius-md);
}

/* This list reads as a list */
.result-list--bullets { padding-left: 1.5rem; list-style: disc; }

.result-list--bullets li { display: list-item; }

.result-list--bullets li::marker { color: var(--accent-solid); }

/* Participant words sit under the finding they back up */
.finding q {
  display: block;
  margin-top: var(--space-3);
  font: 500 0.875rem/1.6 var(--font-ui);
  color: var(--accent-solid);
  quotes: '\201C' '\201D';
}

/* The hand-off shares the solution's lift so the results read as one arc */
.cs-handoff { --kicker-bg: var(--surface-deep); }

@media (max-width: 900px) {
  .screen-grid { grid-template-columns: minmax(0, 1fr); gap: 2.5rem; }
}


/* Findings arrive one at a time, hanging from the same left edge as the
   copy above so the section reads as one column */
.finding-slider {
  width: 100%;
  max-width: var(--page-width);
  margin-top: var(--space-16);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.fs-viewport { overflow: hidden; width: 100%; }

.fs-track { display: flex; transition: transform 320ms var(--ease); }

.finding-slider .finding {
  flex: 0 0 50%;
  min-height: 13rem;
  /* keeps each column off its neighbour */
  padding-right: 4rem;
  box-sizing: border-box;
}


.finding-slider .finding b { font-size: 1.375rem; line-height: 1.4; }

.finding-slider .finding p { font-size: 1.0625rem; line-height: 1.8; }

.fs-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  margin-top: var(--space-8);
  width: 100%;
}

.fs-controls .circle-btn {
  position: static;
  transform: none;
  width: 2.5rem;
  height: 2.5rem;
}

.fs-dots { margin-top: 0; }

/* The hand-off rides a rounder, unlined lift so the results feel held
   rather than framed */
.cs-handoff .solution-orb {
  width: 150vw;
  height: 118%;
  border-radius: 50%;
  background: rgba(19, 33, 58, .62);
  box-shadow: none;
}

@media (max-width: 900px) {
}

/* Participants: the row and the findings share one column so the copy and
   the first finding hang from the same left edge */
.cs-participants .cs-row,
.cs-participants .finding-slider { width: min(78rem, 94vw); }

.cs-participants .cs-row { justify-content: space-between; }

.cs-participants .finding-slider { max-width: none; }

/* Three pillars read as a row of definitions under the section copy */
.pillar-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 3.5rem;
  width: min(72rem, 92vw);
  margin: var(--space-16) 0 0;
}

.pillar-row dt {
  font: 700 1.5rem/1.4 var(--font-display);
  color: var(--ink);
  margin-bottom: var(--space-3);
}

.pillar-row dd {
  margin: 0;
  font: 400 1rem/1.75 var(--font-ui);
  color: var(--muted);
}

@media (max-width: 900px) {
  .pillar-row { grid-template-columns: minmax(0, 1fr); gap: 2rem; }
}

/* Captures pulled from the study: screen recordings of the diary tool and
   the sheet they exported into */
.shot {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--divider);
  background: var(--surface-deep);
}

/* The figures carry the accent, same blue as the kicker lines */
.stat b { color: var(--accent-solid); }

/* Solution intro: copy on the left sharing the captures' left edge, the
   voice orb beside it standing in for the tool itself */
.tool-intro {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 3rem 3.5rem;
  align-items: center;
  width: min(66rem, 92vw);
  margin-top: var(--space-12);
}

.tool-intro .cs-text { max-width: none; }

.tool-intro .result-list { margin-top: var(--space-6); }

.tool-orb {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.tool-orb img,
.tool-orb video {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-md);
}

.tool-orb .cs-caption { text-align: left; margin: 0; }

.cs-participants .finding-slider { margin-top: var(--space-8); }

/* The captures open enlarged, like the rest of the case imagery */
.screen-grid figure,
.screen-single { cursor: zoom-in; }

@media (max-width: 900px) {
  .tool-intro { grid-template-columns: minmax(0, 1fr); }
}

/* A figure sizes to its image, so a long caption wraps underneath instead
   of stretching the figure and squeezing the copy beside it */
.cs-media--fit { width: min-content; }

.cs-media--fit .cs-caption { max-width: 100%; }

/* Space between the voice orb and the captures that follow it */
.tool-intro { margin-bottom: var(--space-24); }

/* The four captures, two across, shown at their own resolution */
.gif-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2.5rem 3.5rem;
  width: min(66rem, 92vw);
  margin-top: var(--space-16);
}

.gif-cell {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  cursor: zoom-in;
}

.gif-cell figcaption {
  font: 500 0.9375rem/1.625rem var(--font-ui);
  color: var(--muted);
}

/* the beat where the findings heading used to sit */
.findings-gap { height: var(--space-24); }

@media (max-width: 900px) {
  .gif-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Each stage of the pipeline gets its own moment on the way down */
.cs-pipeline .cs-row { margin-top: var(--space-40); }

.cs-pipeline .cs-stack + .cs-row { margin-top: var(--space-24); }

/* The findings sit centered under the recruiting copy, given room to
   spread so each pair reads across the page rather than in a narrow column */
.findings-gap { height: var(--space-40); }

.cs-participants .finding-slider {
  align-items: center;
  margin-top: 0;
}


.cs-participants .fs-viewport { width: 100%; }

/* Every stage's figure occupies the same column, so the narrower research
   assistant slide still lines up with the ones above and below it */
.cs-pipeline .cs-media {
  width: 30rem;
  max-width: 100%;
  align-items: flex-start;
}

.cs-pipeline .cs-media img { max-width: 100%; }

.cs-pipeline .cs-media .cs-caption { text-align: left; }

/* The stage names read at the same weight as every other row heading */
.role-name { margin: 0 0 var(--space-3); }

/* The credit line under the sign-off breaks onto a second line rather than
   running wider than the paragraph above it */
.thanks-note {
  margin-top: var(--space-8);
  max-width: 41rem;
}

/* ==========================================================================
   Cursor orb
   A small bead rides the pointer in place of the arrow. It inverts whatever
   is behind it and pulls the result to neutral, so over the navy field it
   reads white, over white text it flips near-black, and over the cyan
   accents it lands mid-grey. It darkens and swells over anything you can
   click, drag or open.
   ========================================================================== */
.cursor-orb { display: none; }

@media (hover: hover) and (pointer: fine) {
  html.orb-cursor,
  html.orb-cursor * { cursor: none; }

  /* text fields keep their caret so typing still reads normally */
  html.orb-cursor input,
  html.orb-cursor textarea,
  html.orb-cursor select,
  html.orb-cursor [contenteditable] { cursor: auto; }

  /* the NGA drawing canvas keeps its own crosshair */
  html.orb-cursor .tp-canvas,
  html.orb-cursor .tp-canvas canvas { cursor: crosshair; }

  .cursor-orb {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    width: 1.125rem;
    height: 1.125rem;
    margin: -0.5625rem 0 0 -0.5625rem;
    border-radius: 50%;
    background: transparent;
    -webkit-backdrop-filter: invert(1) grayscale(1) brightness(1.12);
    backdrop-filter: invert(1) grayscale(1) brightness(1.12);
    pointer-events: none;
    z-index: 999;
    opacity: 0;
    transform: translate3d(-200px, -200px, 0);
    transition:
      opacity 200ms var(--ease),
      width 180ms var(--ease),
      height 180ms var(--ease),
      margin 180ms var(--ease),
      background-color 180ms var(--ease),
      -webkit-backdrop-filter 180ms var(--ease),
      backdrop-filter 180ms var(--ease);
    will-change: transform;
  }

  .cursor-orb.awake { opacity: 1; }

  /* over something actionable: larger, and dark enough to read as a press */
  .cursor-orb.hot {
    width: 2.125rem;
    height: 2.125rem;
    margin: -1.0625rem 0 0 -1.0625rem;
    -webkit-backdrop-filter: invert(1) grayscale(1) brightness(0.5);
    backdrop-filter: invert(1) grayscale(1) brightness(0.5);
  }

  /* held down */
  .cursor-orb.down {
    width: 1.5rem;
    height: 1.5rem;
    margin: -0.75rem 0 0 -0.75rem;
    -webkit-backdrop-filter: invert(1) grayscale(1) brightness(0.3);
    backdrop-filter: invert(1) grayscale(1) brightness(0.3);
  }

  /* Older engines fall back to a difference-blended white bead: same
     inversion, slightly warmer over the navy. */
  @supports not ((backdrop-filter: invert(1)) or (-webkit-backdrop-filter: invert(1))) {
    .cursor-orb { background: #FFFFFF; mix-blend-mode: difference; }
    .cursor-orb.hot { background: #93A6C0; }
    .cursor-orb.down { background: #6C82A2; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .cursor-orb { transition: opacity 200ms linear; }
}

/* Where the copy is meant to read as one column with its heading, the body
   takes the heading's measure rather than the narrower reading width, so the
   paragraph and the heading share both edges */
.cs-stack--wide .section-body { max-width: var(--measure-head); }

/* One finding at a time: the slide fills the track, and the text sits in a
   centred column under the copy above it — still left-aligned inside */
.finding-slider .finding {
  flex: 0 0 100%;
  padding-right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.finding-slider .finding > * {
  width: 100%;
  max-width: 44rem;
}

.finding-slider .finding b { display: block; }

/* ==========================================================================
   Mobile
   The desktop layout is a proportional canvas: every measurement is in rem
   and the rem tracks viewport width, so 90rem always equals the screen —
   right down to 900px, where the rem hits its 10px floor. Below that the
   canvas stops fitting and the page needs a different shape, not a smaller
   one. So here the fixed canvas gives way to a single fluid column: rows
   stack, strips of mockups become swipeable, and the journey keeps its
   earth with each case riding over the planet instead of beside it.
   ========================================================================== */
@media (max-width: 900px) {

  /* ---- foundation ------------------------------------------------------
     Desktop runs on a proportional canvas: 1rem rides the viewport so the
     whole layout keeps the same share of any screen. Phones do the opposite.
     The rem is pinned at 16px, which makes every rem below one real pixel of
     the mobile spec, and the 4-column grid holds the shape instead of the
     type. See DESIGN.md → Responsive.

     4-column stretched grid: 16px margins either side, 16px gutters, columns
     flexing into whatever is left. Everything aligns to it, so no section
     invents its own padding. */
  html { font-size: 16px; }

  .page { min-width: 0; }

  .frame {
    width: 100%;
    max-width: 100%;
    padding: 0 var(--m-margin);
  }

  /* nothing built for the wide canvas may push the page sideways */
  .cs-media img, .cs-media video,
  .cs-hero-art img, .cs-row img, .cs-row video,
  .cs-stack img, .sol-intro img, figure > img, figure > video {
    max-width: 100%;
    height: auto;
  }

  /* ---- nav: the fixed bar at phone proportions ------------------------- */
  /* the bar reads at full presence on phones: taller, bigger mark,
     bigger hamburger */
  .nav {
    height: 4.5rem;
    padding: 0 var(--m-margin);
  }

  /* phones swap the inline links for the hamburger */
  .nav-links-bar { display: none; }

  .nav-menu { display: inline-flex; height: 3.25rem; width: 3.25rem; }

  .nav-menu svg { width: 3rem; height: 3rem; }

  .nav-star .logo-mark { width: 3.5rem; }

  .nav-star svg:not(.logo-mark) { width: 2rem; height: 2rem; }

  /* ---- hero ------------------------------------------------------------ */
  .hero {
    height: auto;
    min-height: 78vh;
    padding-top: 6rem;
    padding-bottom: 4rem;
  }

  .hero-col { width: 100%; }

  /* (the moon lives in the about section now) */

  .hero-cta { flex-wrap: wrap; justify-content: center; }

  /* ---- selected work --------------------------------------------------- */
  .work { margin-top: 5rem; }

  .work-intro { align-items: center; text-align: center; }


  /* Each case rides on the planet rather than beside it: centred over the
     earth with a soft scrim so the copy stays readable against the ocean. */
  .wj-deck { height: 300vh; }

  .wj-case {
    left: 50%;
    right: auto;
    top: 50%;
    bottom: auto;
    width: min(84vw, 30rem);
    max-width: none;
    align-items: center;
    text-align: center;
    transform: translate(-50%, calc(-50% + 1.5rem));
  }

  .wj-case.active { transform: translate(-50%, -50%); }
  .wj-case.active:hover { transform: translate(-50%, calc(-50% - 0.5rem)); }

  .wj-case .slide-oneliner { text-align: center; }

  .wj-art { margin-bottom: 1.5rem; align-self: center; }

  /* the inline width on these figures beats a plain `width: auto`, so the
     max-height clamp was squashing the phone; both axes stay auto and only
     the max-* constraints bite, which preserves the ratio */
  .wj-art img { max-height: 42vh; width: auto !important; height: auto; max-width: 100%; }

  /* ---- about ----------------------------------------------------------- */
  /* the about copy is taller than a phone screen, so it scrolls past the
     planet rather than pinning to it */
  .j-about { min-height: 120vh; }

  .j-about-sticky {
    position: static;
    min-height: 0;
    width: 100%;
    justify-content: flex-start;
    padding: 8rem 0 4rem;
  }

  /* portrait over the copy, not beside it */
  .about-row {
    width: 100%;
    flex-direction: column;
    align-items: center;
    gap: 2.5rem;
  }

  .about-row .portrait-wrap { align-self: center; }

  .portrait { width: min(60vw, 16rem); height: auto; }

  .about-col { width: 100%; align-items: center; text-align: center; gap: 2.25rem; }

  .about-col .section-heading { max-width: 100%; }

  .cred-cols { flex-direction: column; gap: 2.25rem; width: 100%; align-items: center; }

  .cred-col { width: 100%; max-width: 20rem; align-items: center; text-align: center; }

  /* ---- contact + footer ------------------------------------------------ */
  /* The nav is fixed on the homepage, so it hangs over whatever is beneath it.
     With no top padding the ¿HABLAMOS? kicker started at the very first pixel
     of the section and ran straight into the bar. */
  .contact { padding: var(--space-24) var(--m-margin) 0; }

  /* 140px of margin plus the 94px the star occupies inside the footer left a
     234px hole between the buttons and the first ridge. Half that closes the
     scene up without crowding the star. */
  .vieques { margin-top: var(--space-12); }

  .contact-cta { flex-wrap: wrap; justify-content: center; }

  /* the footer band keeps its mountains and its flamboyanes */
  .vq-col { width: 100%; left: 0; transform: none; }

  /* The ridges carry min-width: 90rem so they never squash on a narrow desktop
     window. On a phone that pinned them to 1440px anchored at the left edge,
     so the screen only ever showed the leftmost quarter of the range. Scaling
     them to the screen puts the whole composition back, which also brings the
     range's tallest peak into view for the first time: it sits at the exact
     centre of the artwork, directly under the star. Lifting the star opens
     that gap back up. Dropping the ridges instead would have worked too, but
     it slid the treeline down over the copyright. */
  .vq-mountain { min-width: 0; }

}

/* ==========================================================================
   Mobile — case studies
   Same idea as the homepage: the two-column rhythm collapses to one column,
   and anywhere the desktop lays mockups out in a row they become a swipeable
   strip instead of shrinking to nothing.
   ========================================================================== */
@media (max-width: 900px) {

  /* ---- hero ------------------------------------------------------------ */
  .cs-hero-inner {
    width: 100%;
    left: 0;
    transform: none;
    min-height: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 2.5rem !important;
    padding: 7rem 0 3rem;
  }

  .cs-hero-copy { width: 100%; }

  .cs-hero-copy h1 { font-size: 2.5rem; line-height: 1.18; }

  .cs-hero-art { align-self: center; max-width: 100%; }

  .cs-hero-art img { width: auto !important; max-width: 100%; height: auto; }

  /* the four facts read as a 2×2 card rather than one long row */
  .cs-meta {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.5rem 1.75rem;
    width: 100%;
  }

  .cs-scroll { display: none; }

  /* ---- section rhythm --------------------------------------------------- */
  .cs-section { margin-top: 7rem; }

  .cs-section > .kicker { margin-bottom: 2.5rem; }

  .cs-row {
    flex-direction: column !important;
    gap: 2.5rem;
    align-items: flex-start;
  }

  .cs-row .cs-text, .cs-row .cs-media, .cs-row figure, .cs-row .cs-col { width: 100%; max-width: 100%; }

  /* Kicker, line, heading, image, body — the order a case study reads in.
     The heading and the body sit in one column beside the artwork on desktop,
     so here that column dissolves and its two halves become siblings of the
     image, which is the only way the image can land between them. Both halves
     are still real boxes, so both keep their own reveal. */
  .cs-col { display: contents; }

  /* One card at a time. The half-visible next card was the swipe cue, but a
     cropped mockup reads as a layout mistake rather than an invitation, so
     the dot row underneath does that job instead. */
  .phone-strip figure,
  .screen-strip figure,
  .fan-cards .idea,
  .orbit-card {
    flex: 0 0 100% !important;
    width: 100% !important;
    max-width: 100% !important;
    scroll-snap-align: center;
  }

  /* the dots the strips get handed by main.js */
  .strip-dots {
    display: flex;
    justify-content: center;
    gap: 0;
    margin-top: var(--space-6);
  }

  /* the trailing spacer only existed to let the last card reach the start
     line; full-width cards land there on their own */
  .orbit-cards::after { content: none; }

  /* The orb is a wide flat ellipse on the desktop canvas: 200vw across against
     a section only a fraction of that tall, so all you see of it is a shallow
     horizon top and bottom. At phone width 200vw is 786px while the section
     runs past 1600px, which stood the ellipse on its end and bent the line the
     wrong way. Widening it to roughly the hero arc's 1.9:1 puts the same
     shallow curve back. The sides sit far off-screen either way. */
  .solution-orb { width: 800vw; }

  /* Capped at 96vw the sign-off ring came out 377 wide by 379 tall, which is a
     circle, not the flat oval it is on the wide canvas. An oval that both
     stays inside the screen and still clears three lines of near-full-width
     copy is not possible at this size, so it runs a little past both edges
     instead. Same trick as the solution orb: the curve you see is the top and
     bottom of a wide ellipse, and no text is pushed out. */
  .gracias-orb {
    width: 170vw;
    height: 105%;
  }

  /* A list's own 24px marker indent pushed this copy to x=40 while every other
     line on the page starts at the 16px grid margin. Markers move inside so
     the first line, the wrapped lines and the body copy above all share one
     left edge. */
  .sol-list,
  .result-list {
    padding-left: 0;
    list-style-position: inside;
  }

  /* Anything that is not the heading or the body sits between them, whether
     that is a figure, a slideshow or a results table. */
  .cs-row > * { order: 2; }
  .cs-row > .cs-col > .section-heading { order: 1; }
  .cs-row > .cs-col > .cs-text { order: 3; }

  /* Centred on the screen, still read left. fit-content makes a short heading
     hug its own words so it can be centred, while a long one still runs the
     full column. Same behaviour the narrative stacks already have, so a line
     like "Where I was hands on." sits in the middle instead of stranded on
     the left of a column it does not fill. */
  .cs-row > .cs-col > .section-heading,
  .cs-row > .cs-col > .cs-text {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;
  }

  .cs-media--fit { width: 100%; }

  .cs-stack, .sol-intro, .cs-text { width: 100%; max-width: 100%; }

  .section-heading { max-width: 100%; font-size: 2rem; line-height: 1.28; }

  .section-body { max-width: 100%; }

  .cs-question { font-size: 1.75rem; line-height: 1.35; }

  /* Inline widths on figures were tuned for the wide canvas. Width leads for
     anything landscape, but a portrait capture taken to the full column turns
     into a tower: the NGA fun-and-games screenshot came out 860px tall on an
     852px screen, taller than the phone showing it. Capping the height and
     letting the width follow keeps it an object on the page instead of a
     section of its own, and keeps every aspect ratio honest. */
  .cs-media img, .cs-media video, .cs-stack img, .shot {
    width: auto !important;
    height: auto !important;
    max-width: 100%;
    max-height: 62vh;
  }

  /* Phones sit the artwork straight. On the wide canvas the 4° lean has room
     to spare, but here the image already fills the column, so a rotated box
     needs more width than there is and the picture gets cropped at the edge.
     The taller the image the worse it reads: the NGA fun-and-games capture
     lost 27px off its left side. Only the rotation goes, the gentle float
     stays, so the artwork still drifts the way it does everywhere else. */
  .cs-media .ph,
  .cs-media svg,
  .cs-media img,
  .cs-media--tilt-r img { transform: none; }

  .cs-media img,
  .cs-media svg,
  .cs-media--tilt-r img { animation-name: floaty; }

  /* Two figures side by side is a wide-canvas idea. Here the pair was sharing
     311px, which left the first stat 122px and set its sentence about seven
     characters to a line. Stacked, each one gets the whole column. */
  .stat-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-12);
  }

  .stat { max-width: 100%; }

  /* Same story for the two quotes the Woven team left us: side by side they
     were 112px wide and the second one ran off the text column entirely. */
  .pull-quotes {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-12);
  }

  .pull-quote { max-width: 100%; }

  /* The solution sections run full-bleed so the orbit ring behind them can
     reach past both screen edges. Their other blocks take the gutter back on
     their own, but the heading stack and the quote pair never did, so that
     copy alone began at the very first pixel while every other line on the
     page held its margin. */
  .cs-solution > .cs-stack,
  .cs-solution > .sol-intro,
  .cs-solution > .pull-quotes { padding-inline: var(--m-margin); }

  /* ---- rows of mockups become swipeable strips -------------------------- */
  .phone-strip, .screen-strip {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    padding: 0 var(--m-margin) 1rem;
    justify-content: flex-start;
    align-items: flex-start;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .phone-strip::-webkit-scrollbar, .screen-strip::-webkit-scrollbar { display: none; }

  .phone-strip figure, .screen-strip figure {
    flex: 0 0 auto;
    scroll-snap-align: center;
    width: 68vw;
    max-width: 17rem;
    transform: none !important;
  }

  .phone-frame { width: 100%; }

  .screen-strip figure img, .screen-strip figure video { width: 100%; height: auto; }

  /* ---- the quote constellation lies flat ------------------------------- */
  .quote-field {
    width: 100%;
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.75rem;
    text-align: center;
  }

  .qf, .qf-orbit {
    position: static !important;
    transform: none !important;
    animation: none !important;
    width: 100%;
    max-width: 26rem;
  }

  /* ---- the idea fan stacks --------------------------------------------- */
  .cs-fan { padding: 2rem 0 3rem; }

  .cs-fan .idea {
    position: static !important;
    transform: none !important;
    width: 100% !important;
    margin: 0 0 1.5rem;
  }

  /* ---- before / after --------------------------------------------------- */
  .ba-panels { width: 100%; }

  .ba-lines { width: 100%; }

  .page-toggle { justify-content: center !important; width: 100% !important; }


  /* ---- sliders and slideshows ------------------------------------------ */
  .pg-slider, .wf-slider, .proto-wrap, .laptop, .laptop--proto {
    width: 100% !important;
    max-width: 100%;
  }

  .laptop { --screen-w: 100%; }

  .laptop-base { width: 100%; margin: 0; }

  .cs-row--top { flex-direction: column !important; }

  /* ---- in orbit --------------------------------------------------------- */
  .orbit-cards {
    grid-template-columns: minmax(0, 1fr);
    gap: 3.5rem;
    width: 100%;
  }

  .orbit-card,
  .orbit-card:nth-child(1),
  .orbit-card:nth-child(2),
  .orbit-card:nth-child(3) { transform: none !important; }

  .orbit-card img { max-width: min(62vw, 15rem); height: auto; }

  /* ---- sign-off / gracias ---------------------------------------------- */
  .gracias { margin-top: 8rem; }

  .in-orbit { margin-top: 8rem; }
}

@media (max-width: 900px) {
  /* the case-study column is the screen now */
  .cs-main { width: 100%; }

  /* the wireframe boards were three phones side by side */
  .wf-units, .wf-units--map {
    flex-direction: column;
    align-items: center;
    gap: 2rem;
  }

  /* The annotated boards are a wireframe with notes down either side, each one
     pinned opposite the part it describes. That needs width to work. Here the
     side columns collapsed to about 49px, which stacked the words into towers
     two characters wide and let the notes run over each other. Phones get the
     same content in reading order instead: the board first, then its notes
     underneath as a list. The arrows come off because once a note sits below
     the board it is no longer pointing at anything. */
  .wf-unit {
    width: 100%;
    max-width: 100%;
    flex-direction: column;
    align-items: center;
    gap: 1.25rem;
  }

  .wf-phone { order: -1; }

  .wf-side,
  .wf-units--map .wf-side {
    position: static;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }

  .wf-side .wf-note,
  .wf-side--l .wf-note {
    position: static;
    text-align: left;
  }

  .wf-note .wf-arrow { display: none; }

  .wf-phone,
  .wf-units--map .wf-phone { width: min(58vw, 13rem); }

  .wf-unit img, .wf-slide img { max-width: 100%; height: auto; }

  .wf-title { font-size: 1.5rem; margin-bottom: 1.75rem; }
}

@media (max-width: 900px) {
  /* the kicker chip fits the screen and lets long labels breathe */
  .kicker {
    min-width: 0;
    max-width: 100%;
    padding: 0 2.5rem;
    height: auto;
    min-height: 2.0625rem;
  }

  /* The kicker is held on one line so it cuts the flanking rule cleanly, which
     is right on the wide canvas where the longest one still fits. Here
     "DESIGN DECISIONS THAT DIDN'T WORK OUT ¯\(ツ)/¯" measures 428px against a
     393px screen and ran 18px off both edges. Letting it wrap is the only way
     it fits, and the height above is already free to take a second line. */
  .kicker span {
    text-align: center;
    white-space: normal;
  }

  /* the results table keeps its columns and scrolls sideways on its own */
  .tt-wrap {
    width: 100%;
    max-width: 100%;
    flex-shrink: 1;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .test-table { display: table; white-space: normal; min-width: 26rem; }

  .test-table th { padding-right: 1.25rem; }

  .tt-wrap .cs-caption { white-space: normal; }
}

@media (max-width: 900px) {
  /* The hand-tuned line breaks were for the wide canvas; let it wrap. Keep a
     space before any such <br> in the markup — hiding the break also removes
     the word gap it was standing in for. */
  .cs-hero-copy h1 br { display: none; }
}

/* The prototype's own window: only offered where the embedded laptop is too
   small to drive, which is every phone */
.proto-open { display: none; }

@media (max-width: 900px) {
  /* a taller screen so the embedded prototype still shows something useful */
  .laptop--proto .laptop-screen { aspect-ratio: 3 / 4; }

  .proto-open { display: inline-flex; margin-top: 2rem; }
}

@media (max-width: 900px) {
  /* The project titles carried a dark halo to hold them off the planet. It
     read as an outline drawn round the letters rather than as depth, so it is
     gone. Without it --muted was too close in value to the ocean underneath,
     so the one-liner takes --ink here and leans on weight for hierarchy
     instead of colour: the title stays bold, this stays medium. */
  .wj-case .slide-oneliner { color: #DCE7F5; }
}

@media (max-width: 900px) {
  /* The before/after pages are 2722px-wide annotated screenshots — at phone
     width the annotations are unreadable and the drag says nothing, so the
     whole section sits this one out. */
  .cs-how { display: none; }
}

/* The hamburger only exists on phones; the wide layout keeps its links out */
.nav-toggle { display: none; }

/* (the inline-links mobile nav is gone: the drawer serves every width) */

/* ==========================================================================
   Mobile — homepage, second pass
   Nav star to the centre, the case deck riding the planet, the about section
   standing alone in space, and the footer left clear to be seen.
   ========================================================================== */
@media (max-width: 900px) {

  /* ---- hero ------------------------------------------------------------ */
  .hero { padding-top: 7rem; }

  /* the headline is one SVG, so all four lines share the scale the longest
     one sets — give it the full column so it is not shrunk twice */
  .hero-col { width: 100%; max-width: 100%; }

  /* ---- about: alone in space ------------------------------------------- */
  /* portrait centred at the top, then the copy under it left-aligned */
  .about-row { gap: 2rem; }

  .portrait { width: min(52vw, 13rem); }

  .about-col {
    align-items: flex-start;
    text-align: left;
    gap: 1.75rem;
  }

  .about-col .section-heading { max-width: 100%; }

  /* experience and education stay side by side, the way the desktop reads */
  .cred-cols {
    flex-direction: row;
    gap: 1.25rem;
    align-items: flex-start;
  }

  .cred-col {
    flex: 1 1 0;
    width: auto;
    max-width: none;
    align-items: flex-start;
    text-align: left;
  }

  /* the resume button returns to the centre under both columns */
  .about-col > .btn { align-self: center; margin-top: 0.5rem; }

  /* ---- footer: the flamboyanes come back ------------------------------- */
  .vq-tree {
    display: block;
    width: 8.5rem;
    height: auto;
    top: 11.5rem;
  }

  .vq-tree:nth-child(1) { left: -1rem !important; }
  .vq-tree:nth-child(2) { left: auto !important; right: -1rem; }

  /* the back-to-top star centres over the ridge */
  /* top was 3.25rem, which sat the star right on the peak once the full ridge
     came into view */
  .vq-star { left: 50%; margin-left: -0.8125rem; top: 1.25rem; }
}

/* (the scattered-arcs experiment is retired: the hero is the centered stack
   with the bend baked in as the resting state — see .h1-orbit) */

/* The canvas is sized to the viewport by earth.js, so it has to be centred on
   the viewport rather than on the padded frame it lives inside — otherwise it
   starts at the frame's gutter and leaves a strip of empty sky down one edge. */
#earth-canvas {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
}

@media (max-width: 900px) {
  /* ---- headline: the phone stack ---------------------------------------- */
  .h1-orbit:not(.h1-orbit-m) { display: none; }

  .h1-orbit.h1-orbit-m { display: block; width: 100%; height: auto; }

  /* The stack fills its width: each line sized to run nearly rail to rail
     on the 434-unit arc, measured per line (¡Hola! 399, Boricua 405,
     user-data 406 of 434). The arcs moved down so the big caps clear the
     frame; curvature is unchanged. */
  .h1-orbit-m text { font-size: 28px; }

  .h1-orbit-m .l-intro { font-size: 66px; }

  .h1-orbit-m .t3 { font-size: 25px; }

  /* the about moon at phone scale: tucked to the right, above the portrait */
  .moon {
    right: 1.25rem;
    top: 3.75rem;
    opacity: .8;
  }

  .moon svg { width: 4.75rem; height: 4.75rem; }

  /* ---- the sky reaches the edges --------------------------------------- */
  .earth-stage {
    width: 100vw;
    margin-left: calc(50% - 50vw);
  }
}

/* iOS reports a shorter innerHeight than 100vh whenever the toolbar is up, so
   the canvas came up short of the fold. dvh follows the visible viewport. */
@supports (height: 100dvh) {
  @media (max-width: 900px) {
    .earth-stage { height: 100dvh; margin-bottom: -100dvh; }

    /* The frames that sit on top of the planet have to be the same height as
       the planet, or the card centres itself in a taller box than the one you
       can see and the bottom of the screen stops being covered. Only the
       full-screen frames move to dvh: the deck's scroll length stays in vh so
       the page does not change length every time the toolbar slides. */
    .wj-sticky { height: 100dvh; }

    .j-about-sticky { min-height: 100dvh; }
  }
}

@media (max-width: 900px) {
  /* the greeting keeps its centre; each sentence after it starts a line */
  .h1-hola { display: block; text-align: center; }

  .h1-line { display: block; }

  /* the moon hangs level with the greeting rather than above it:
     kicker height + the column gap + the h1's own top margin */
  /* the moon hangs in the sky below the buttons now, not level with the
     greeting — see the moon block above */

  /* ---- about ------------------------------------------------------------
     The portrait sits centred over the copy. It used to be pinned right,
     which is a wide-canvas idea: with the copy underneath rather than beside
     it, hard right just reads as misaligned. */
  .about-row { align-items: center; }

  /* the drifting tilt is an animation, which outranks a plain transform —
     both have to go for the portrait to sit square */
  .portrait {
    width: min(58vw, 15rem);
    height: auto;
    aspect-ratio: 27.3125 / 27.5625;
    animation: none;
    transform: none;
    align-self: center;
  }

  .about-row .portrait-wrap { align-self: center; }

  /* These carried generous multiples from when the rem sat near 11.6px. At a
     16px root they came out at 28, 22 and 19, which put "Experience" level
     with the section heading above it and flattened the whole hierarchy.
     cred-head takes the body size, and the two below it fall back to the
     desktop values, which were already right for a phone. */
  .cred-head { font: 500 1.125rem/1.75rem var(--font-display); }

  .cred-col { gap: var(--space-4); }
}


/* ==========================================================================
   Mobile type scale

   The rem is pinned at 16px below 900px, so every value here is one real pixel
   of the spec. Sizes are size / line-height. Letter-spacing is NOT touched:
   tracking is identical at both sizes, so it stays wherever the desktop rule
   set it. Nav, button labels and meta values do not shrink at all; they are
   already at the smallest comfortable size and going below hurts tap targets.

     Role              Desktop     Mobile
     Hero H1           56 / 70     36 / 44
     Hero subtitle     24 / 32     19 / 28
     Section heading   38 / 48     28 / 36
     Section body      24 / 32     18 / 28
     Kicker            18 / 26     15 / 22
     Project title     32 / 40     24 / 32
     Caption           15 / 26     14 / 22
     Meta label        12 / 18     11 / 16
     Meta value        14 / 20     14 / 20   unchanged
     Nav               15          15        unchanged
     Button label      16          16        unchanged
   ========================================================================== */
@media (max-width: 900px) {

  /* ---- hero ------------------------------------------------------------- */
  .cs-hero-copy h1,
  .hero-h1 { font-size: 2.25rem; line-height: 2.75rem; }

  .cs-sub { font-size: 1.1875rem; line-height: 1.75rem; }

  /* ---- section ---------------------------------------------------------- */
  .section-heading,
  .about-col .section-heading { font-size: 1.75rem; line-height: 2.25rem; }

  .section-body,
  .stat p,
  .strip-stat,
  .wf-note,
  .finding q,
  .result-list li,
  .sol-list li,
  .qf,
  .pillar-row dd,
  .finding-slider .finding p { font-size: 1.125rem; line-height: 1.75rem; }

  /* ---- kicker ----------------------------------------------------------- */
  .kicker span,
  .cs-kicker { font-size: 0.9375rem; line-height: 1.375rem; }

  /* ---- project title ---------------------------------------------------- */
  .orbit-card span,
  .slide-title,
  .pillar-row dt,
  .finding-slider .finding b,
  .fan-cards .idea figcaption b,
  .wf-title { font-size: 1.5rem; line-height: 2rem; }

  /* ---- caption ---------------------------------------------------------- */
  .cs-caption,
  .screen-strip figcaption,
  .phone-strip figcaption,
  .screen-single figcaption,
  .screen-grid figcaption,
  .gif-cell figcaption,
  .test-table td,
  .fan-cards .idea figcaption span,
  .stat cite,
  .pull-quote cite,
  .ba-hint,
  .ba-handle,
  .tp-stamp-opt,
  .ph { font-size: 0.875rem; line-height: 1.375rem; }

  /* ---- meta label ------------------------------------------------------- */
  .cs-meta dt,
  .copyright,
  .test-table th,
  .earth-tip,
  .ba-tag { font-size: 0.6875rem; line-height: 1rem; }

  /* ---- meta value, nav, buttons: unchanged from desktop ----------------- */
  .cs-meta dd { font-size: 0.875rem; line-height: 1.25rem; }

  .btn,
  .page-toggle .btn { font-size: 1rem; }
}

/* ==========================================================================
   Mobile — the fan, the orbit, touch targets, caption measure
   ========================================================================== */
@media (max-width: 900px) {

  /* ---- the cut ideas ---------------------------------------------------
     Four cards in a flex row with a fixed 26rem image height: on a phone the
     row shrank each card to 58px while the height held, squashing the art by
     up to 64%. They swipe now, at a size you can actually read. The arc is
     desktop geometry — it cut through the captions here. */
  .cs-fan {
    display: block;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    padding: 1rem 0 3rem;
    overflow: visible;
  }

  .fan-arc { display: none; }

  .fan-cards {
    justify-content: flex-start;
    align-items: flex-start;
    gap: 2rem;
    padding: 0 var(--m-margin) 1rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .fan-cards::-webkit-scrollbar { display: none; }

  .fan-cards .idea {
    flex: 0 0 auto;
    scroll-snap-align: center;
    width: 62vw;
    max-width: 16rem;
  }

  .fan-cards .idea img {
    width: 100%;
    height: auto;
  }

  .fan-cards .idea figcaption { text-align: left; }

  /* ---- in orbit ---------------------------------------------------------
     One project at a time, swiped. The next one peeks so the gesture is
     discoverable rather than hidden. */
  .orbit-cards {
    display: flex;
    grid-template-columns: none;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    gap: 0;
    padding-bottom: 1rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .orbit-cards::-webkit-scrollbar { display: none; }

  .orbit-card {
    flex: 0 0 86%;
    scroll-snap-align: center;
    padding: 0 var(--m-margin);
    /* was var(--space-5), which is not a step in the scale, so the browser
       dropped the line and the label sat tight under the mockup. 24 is the
       next real step above the 16 desktop uses. */
    gap: var(--space-6);
  }

  /* Sized to a common width, these came out 181, 251 and 359 tall, so the
     Woven phone stood twice as tall as the Naks laptop and the three read as
     different sizes rather than three projects. Height leads instead: every
     mockup occupies the same band and the width follows its own proportions,
     which is what makes them look like a set. */
  .orbit-card img {
    height: auto !important;
    width: auto !important;
    max-height: 24vh;
    max-width: 88%;
  }

  .orbit-card span { font-size: 1.35rem; }

  /* ---- touch targets ----------------------------------------------------
     Everything interactive was under the 44px floor; the carousel dots were
     9x9. The dots keep their size and gain a hit area around them. */
  .carousel-dots { gap: 0; }

  .carousel-dots button { position: relative; }

  .carousel-dots button::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 3.8rem;
    height: 3.8rem;
    transform: translate(-50%, -50%);
  }

  .circle-btn,
  .fs-controls .circle-btn,
  .fs-prev, .fs-next, .wf-prev, .wf-next, .pg-prev, .pg-next {
    width: 3.8rem;
    height: 3.8rem;
  }

  .btn { padding: 1.35rem 1.75rem; }

  .nav-toggle { width: 3.8rem; height: 3.8rem; justify-content: center; }

  .nav-star {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 3.8rem;
    min-height: 3.8rem;
  }

  /* ---- caption measure --------------------------------------------------
     Captions were running 64-68 characters; a phone wants 35-60. */
  .cs-caption,
  .screen-strip figcaption,
  .phone-strip figcaption,
  .screen-single figcaption,
  .screen-grid figcaption,
  .gif-cell figcaption,
  .finding q {
    max-width: 34rem;
  }
}

/* Mobile — the touch targets and strips the first pass didn't reach.
   These selectors have to match the two-class rules that set the desktop
   sizes (.pg-controls .circle-btn and friends) to outrank them. */
@media (max-width: 900px) {

  .pg-controls .circle-btn,
  .fs-controls .circle-btn,
  .wf-controls .circle-btn {
    width: 3.8rem;
    height: 3.8rem;
  }

  /* full-bleed maths off a fit-content parent landed a few pixels short, and
     an off-centre scroll strip reads as a mistake. Inside the frame, with the
     next card peeking, is both exact and the clearer swipe cue. */
  .orbit-cards {
    width: auto;
    margin-left: 0;
  }

  .orbit-card {
    flex: 0 0 82%;
    padding: 0 0.75rem;
  }


}

/* Mobile — the strips bleed to the edges properly.
   .in-orbit and the fan's parent are centring flex columns, so a scroll strip
   left at width:auto shrink-wraps to its content and overflows both sides.
   Stretching to the line first, then pulling out by the frame padding, lands
   it exactly edge to edge. */
@media (max-width: 900px) {

  .orbit-cards,
  .cs-fan {
    align-self: stretch;
    width: auto;
    margin-inline: calc(-1 * var(--m-margin));
  }

  .orbit-cards {
    padding-inline: var(--m-margin);
    scroll-padding-inline: var(--m-margin);
  }

  .orbit-card { flex: 0 0 82%; padding: 0; }
}

@media (max-width: 900px) {
  /* enough air between cards to read as separate projects, not so much that
     the next one stops peeking */
  .orbit-cards { gap: 1rem; }

  /* Every card lands on the same line as the text above it. `center` is left
     over from when this strip ran full-bleed; once it moved inside the frame
     there was nothing to the left of the first card to scroll through, so it
     could never reach centre and the three cards settled on three different
     lines (17, 49, 54). Starting them instead puts all three on the gutter.
     The trailing spacer is the sliver a card does not cover, which is exactly
     what the last card needs in order to reach that same line. */
  .orbit-card { scroll-snap-align: start; }

  .orbit-cards::after {
    content: '';
    flex: 0 0 calc(100% - 82%);
  }
}

/* ==========================================================================
   Homepage second pass — drawer nav era, curved intro, quieter hero
   ========================================================================== */

/* The case-study pages flow under the fixed bar, so they clear its height
   plus breathing room — at exactly the bar's height their heroes touched the
   nav on wide monitors. The homepage hero handles its own clearance. */
body:not(#top) { padding-top: 6.5rem; }

@media (max-width: 900px) {
  body:not(#top) { padding-top: 5rem; }

  /* The hero holds only the greeting on phones: the CTAs leave, the curved
     introduction below carries the rest, and the open sky does the talking */
  .hero .hero-cta { display: none; }

  /* Phones take the narrow curvature and the 19px train */
  .hero-orbit-info { width: 100vw; bottom: 0; }

  .hero-orbit-info .oi-wide { display: none; }

  .hero-orbit-info .oi-narrow { display: block; }


  /* The deck caption sits under its 24px title at the subtitle size — 14 was
     a step too far the other way */
  .wj-case .slide-oneliner { font-size: 1.1875rem; line-height: 1.75rem; }

  /* Selected-work intro: the block holds the middle of the screen while the
     copy reads left. A hard max-width does the centring; fit-content grew to
     the full column and read as left-stuck. */
  .work-intro .section-body {
    width: auto;
    max-width: 21rem;
    margin-inline: auto;
    text-align: left;
  }

  /* The about lockup holds its two-line shape on phones only at 18 — the
     digit plus an unwrapped phrase is wider than the screen at anything
     bigger. The sanctioned exception in DESIGN.md moves to 18/26. */
  .about-col .section-heading { font-size: 1.125rem; line-height: 1.625rem; }

  /* the phone's looser leading needs a touch more digit to span both lines */
  .about-heading .about-2 { font-size: 4.1em; }
}

/* ==========================================================================
   Homepage third pass — orbit buttons era
   ========================================================================== */

/* The about heading is a lockup: the 2 stands two text-lines tall like a
   stat figure, the sentence rides beside it one phrase per line, and nothing
   sits beneath the numeral */
.about-heading {
  display: flex;
  align-items: center;
  gap: 0.4em;
  max-width: none;
}

/* exactly one phrase per line, never rewrapped — the br is the only break */
.about-heading .about-lines { white-space: nowrap; }

/* per Eric's example: the digit fills the full height of both text lines —
   a numeral's cap is ~0.72em, so matching two lines of 1.26em leading needs
   ~3.6em of font size, not 2.5 */
.about-heading .about-2 {
  font-size: 3.6em;
  line-height: .7;
  color: var(--ink);
}

.about-heading .about-lines {
  display: block;
  text-align: left;
}

@media (max-width: 900px) {
  /* the lockup centres as one unit on phones */
  .about-heading { justify-content: center; }
}

/* (the role line rides the atmosphere curve now — see .hero-orbit-info) */

/* ==========================================================================
   Case-study mobile polish (NGA review, 22 August 2026) + slender footer CTAs
   ========================================================================== */

/* Footer CTAs everywhere: slender pills, quieter label */
.contact-cta .btn {
  padding: 0.45rem 1.15rem;
  font-size: 0.875rem;
}

@media (max-width: 900px) {
  /* Kickers hold a single line, small — a wrapped kicker chip cut its own
     orbit line twice, which can never happen */
  .kicker span {
    font-size: 0.75rem;
    line-height: 1.125rem;
    letter-spacing: .08em;
    white-space: nowrap;
  }

  .cs-kicker {
    font-size: 0.75rem;
    line-height: 1.125rem;
    white-space: nowrap;
  }

  /* Captions hug their artwork, so image-to-body reads the same as
     heading-to-image — every stacked case-study layout, every page */
  .cs-media .cs-caption { margin-top: 0.5rem; }

  /* Body copy fills the column instead of hugging its widest line */
  .cs-row > .cs-col > .cs-text { width: 100%; }

  /* The game mockups sit at the size the lightbox used to show — the frame
     shrinks inside the full-width card so nothing peeks */
  .phone-strip .phone-frame {
    width: min(64vw, 16rem);
    margin-inline: auto;
  }

  /* The cut ideas match the game mockups, titles and captions centred */
  .fan-cards .idea img {
    width: auto;
    max-width: min(64vw, 16rem);
    margin-inline: auto;
  }

  .fan-cards .idea figcaption { text-align: center; }

  /* The diamonds breathe: visual spacing between dots, hit areas trimmed to
     match so neighbours do not swallow each other's taps */
  .carousel-dots { gap: 1.25rem; }

  .carousel-dots button::after { width: 2.5rem; height: 2.5rem; }
}

/* ==========================================================================
   ORA mobile review (22 August 2026) — plus the rules that carry everywhere
   ========================================================================== */

/* A sentence that must not break mid-thought */
.keep-line { white-space: nowrap; }

@media (max-width: 900px) {
  /* Body copy starts at the row gap, so image-to-text space equals
     heading-to-image space — every stacked case-study row, every page */
  .cs-text > .section-body:first-child { margin-top: 0; }

  /* The figure's own 20px gap collapsed: the caption's 8px margin is now the
     whole distance, so captions genuinely hug their artwork */
  .cs-media { gap: 0; }

  /* 'pretty' wrapping was pulling words down and leaving holes on the right
     edge of paragraphs; phones fill the measure instead */
  .section-body { text-wrap: wrap; }

  /* The buyer-journey chart drew at its desktop 520px and ran off both sides
     of the screen; it draws at screen width now */
  .cs-media .journey { width: 100%; }

  /* ORA stills match the NGA mockup size, centred; the numbered caption line
     centres under its phone while the validation note stays readable-left */
  .phone-strip--stills .ora-shell,
  .phone-strip--stills img {
    width: min(64vw, 16rem);
    margin-inline: auto;
  }

  .phone-strip--stills figcaption {
    width: min(64vw, 16rem);
    margin-inline: auto;
    text-align: center;
  }

  .phone-strip--stills .strip-stat { text-align: left; }

  /* Cut-features boards: one image per slide, centred, its notes below */
  .wf-unit:nth-child(n + 2) { display: none; }

  .wf-phone,
  .wf-units--map .wf-phone { margin-inline: auto; }

  /* Slide controls: the dots do the driving on phones — chevrons off, and the
     row sits close to the artwork it controls */
  .wf-controls .circle-btn,
  .fs-controls .circle-btn,
  .pg-controls .circle-btn { display: none; }

  .wf-controls { margin-top: 1rem; }

  /* The results table fits the screen instead of scrolling off it: the
     forced 26rem minimum and the 40px desktop column air both go */
  .test-table { min-width: 0; width: 100%; padding: 1rem; }

  .test-table th,
  .test-table td { padding-right: 1rem; }
}

@media (max-width: 900px) {
  /* The In Orbit strip is a horizontal scroller, and a scroller clips
     anything that pokes above its box — a shadow, a scale, a card mid-
     transition. Pad the clip box upward and pull the layout back, exactly
     the homepage carousel's trick, so a mockup's top can never be sliced. */
  .orbit-cards {
    padding-top: 5rem;
    margin-top: -5rem;
  }
}

/* ==========================================================================
   Naks Yetu + Woven mobile review (22 August 2026)
   ========================================================================== */

@media (max-width: 900px) {

  /* ---- Naks Yetu -------------------------------------------------------- */

  /* The solution intro reads left-aligned; the section still centres it */
  .sol-intro--left {
    align-items: flex-start;
    text-align: left;
  }

  /* A laptop screen is nothing to drive on a phone — the prototype lives
     behind its button alone */
  .laptop--proto { display: none; }

  .proto-open { margin-top: 0; }

  /* ---- Woven ------------------------------------------------------------ */

  /* The problem's two figures centre on the screen */
  .stat-row { align-items: center; }

  .stat { text-align: center; }

  /* The solution heading centres, and the voice orb answers it directly —
     the copy follows, ending on the line the captures pick up from */
  .cs-solution > .cs-stack .section-heading {
    text-align: center;
    margin-inline: auto;
  }

  .tool-orb { order: -1; }

  /* the captures sit right after "Nobody filled out a form" instead of a
     160px pause */
  .tool-intro { margin-bottom: 0; }

  .gif-grid { margin-top: 2.5rem; }

  /* The pillars centre as a stack */
  .pillar-row div { text-align: center; }

  /* The hand-off lift was drawing 645 wide by 1033 tall — an egg. Wide
     enough to lie down again, matching the other orbit curves */
  .cs-handoff .solution-orb { width: 420vw; }

  /* ---- ORA -------------------------------------------------------------- */

  /* Each cut-features slide describes the one board it shows: the title
     carries it, the note stacks stay on the wide canvas */
  .wf-side,
  .wf-units--map .wf-side { display: none; }
}

/* ==========================================================================
   Menu, nav, and In Orbit refinements (22 August 2026)
   ========================================================================== */

@media (max-width: 900px) {
  /* Each cut-features board keeps its one defining note, centred under the
     phone like a caption; the full annotation stacks stay on the wide canvas */
  .wf-unit:first-child .wf-side--key { display: flex; }

  .wf-side--key .wf-note { display: none; }

  .wf-side--key .wf-note--key {
    display: block;
    text-align: center;
    max-width: 20rem;
    margin-inline: auto;
  }

  /* The tap-ring under an In Orbit mockup sits at the mockup's own scale
     instead of swallowing the whole card */
  .orbit-card::before {
    width: 65%;
    min-width: 0;
    height: 2.75rem;
  }
}

/* ==========================================================================
   Selected-work cards hold laptop scale on wide monitors (22 August 2026)
   ==========================================================================
   The homepage canvas scales without a ceiling, which is right for the
   planet and the hero — but it carried the work deck with it, and on a 27"
   monitor the Woven phone stood taller than the viewport and ran under the
   nav. Past the 1530px reference the cards freeze at their laptop pixel
   sizes (each value below is the card's rem size times the 16px root) while
   the rest of the page keeps its proportional scale. */
@media (min-width: 1530px) {
  .wj-case {
    width: min(40vw, 915px);       /* laptop scale + 30%, sized against Eric's monitor */
    align-items: center;
    text-align: center;
  }

  .wj-case .slide-title { font-size: 83px; line-height: 94px; }

  .wj-case .slide-oneliner {
    font-size: 36px;
    line-height: 49px;
    max-width: 611px;
  }

  /* inline widths are per-mockup, so the caps are too */
  .wj-case:nth-child(1) .wj-art img { width: 790px !important; }
  .wj-case:nth-child(2) .wj-art img { width: 582px !important; }
  .wj-case:nth-child(3) .wj-art img { width: 790px !important; }
  .wj-case:nth-child(4) .wj-art img { width: 458px !important; }
}

/* ==========================================================================
   One nav, one scale (22 August 2026)
   ==========================================================================
   The homepage root keeps growing past 1920px while the case-study pages
   cap at 21.33px, which left the same bar rendering at two sizes. The bar
   now pins to its 21.33px-root measurements on every page once the cap
   engages, so the nav is identical site-wide at any width. (Values are the
   rem sizes times 21.33.) */
@media (min-width: 1920px) {
  .nav {
    height: 96px;
    padding: 0 max(32px, calc((100% - 1669px) / 2));
  }

  .nav-star .logo-mark { width: 69px; }

  .nav-links-bar { gap: 43px; }

  .nav-links-bar a { font-size: 20px; }

  .nav-contact { padding: 8px 21px; font-size: 20px; }
}

/* ==========================================================================
   The sky, deepened (preview) — reference: Eric's night-sky photo
   ==========================================================================
   Three moves, all in the site's own hues: a viewport-fixed wash that
   brightens low-left and falls into deeper night upper-right; a dense
   dust field behind the parallax stars (built by main.js); and a few
   bright standouts with halos, one in three leaning accent. The wash
   lives on a fixed pseudo so it never stretches with document height. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 15% 92%, rgba(82, 199, 240, .07), transparent 55%),
    linear-gradient(115deg, #13213A 0%, #0D1B2E 48%, #0A1626 100%);
}

/* dust carries its grains as box-shadows on a transparent anchor; the
   twinkle animation would pulse the whole field at once, so it sits out */
#stars .star--dust { animation: none !important; }

#stars .star--bright {
  box-shadow: 0 0 6px 1px rgba(240, 244, 250, .4);
}

#stars .star--blue {
  background: var(--accent-solid);
  box-shadow: 0 0 8px 2px rgba(82, 199, 240, .45);
}
