/* ==========================================================================
   Emerald Noir — tokens & base
   ========================================================================== */

:root {
  --bg: #0b1712;
  --bg-panel: #07110d;
  --text: #eef4ef;
  --text-muted: #969c9e;
  --accent: #16a34a;
  --accent-bright: #25c65e;
  --accent-soft: #8fe0a8;
  --hairline: rgba(255, 255, 255, 0.1);

  --font-family: 'Satoshi', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --nav-height: 76px;
  --slam-distance: 340px;

  /* ----------------------------------------------------------------------
     Spacing scale — the single source of vertical and horizontal rhythm.

     Whitespace here is proportional, not uniform: what makes a page read as
     grouped rather than scattered is the RATIO between steps, so every gap
     must come off this ladder. Each step is a clear jump from the one below,
     which is what lets the eye read "these belong together" vs "these are
     separate things".

     The proximity rules these encode (see §2 of DESIGN-BRIEF.md):
       eyebrow  → heading          xs   — one label, one line: nearly touching
       heading  → sub-line         sm   — same group, subordinate
       sub-line → group's content  md–lg
       group    → group            xl
       section  → section          2xl  (carried on ONE edge, see .section)
     ---------------------------------------------------------------------- */
  --space-xs: 8px;
  --space-sm: 16px;
  --space-md: 24px;
  --space-lg: 40px;
  --space-xl: 64px;
  --space-2xl: 96px;
  --space-3xl: 128px;

  /* Page gutter — the one horizontal inset every full-width block uses, so the
     nav, hero panel, sections and card strips all line up on the same edge. */
  --gutter: var(--space-md);

  /* Content measure. Wide enough that two-column layouts genuinely fill the
     viewport instead of leaving the right third empty. Long-form reading
     columns (project pages) opt out with their own narrower max-width. */
  --content-max: 1280px;
}

@media (max-width: 599px) {
  :root {
    --nav-height: 64px;
    --slam-distance: 150px;
    --gutter: 18px;
  }
}

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

html {
  scroll-behavior: smooth;
}

/* Anchor targets sit below the sticky nav rather than under it. Applies to the
   in-page jumps (#story, #contact) and to arriving from another page on
   index.html#story, where the browser's own fragment jump uses this too. */
section[id],
main[id],
footer[id] {
  scroll-margin-top: calc(var(--nav-height) + var(--space-sm));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-family);
  line-height: 1.7;
  overflow-x: hidden; /* page-level guard only — never on a glow element's immediate parent */
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  margin: 0;
  font-weight: 500;
  line-height: 1.2;
}

p {
  margin: 0;
}

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

button {
  font-family: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

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

.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  background: var(--accent);
  color: var(--bg);
  padding: 10px 16px;
  border-radius: 6px;
  z-index: 100;
  transition: top 0.2s ease;
}

.skip-link:focus {
  top: 12px;
}

:focus-visible {
  outline: 2px solid var(--accent-bright);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ==========================================================================
   The glow — accent italic words
   ========================================================================== */

/* Blur radii are em-based so the halo scales with the type it sits on.
   The brief's px values are calibrated for heading-size text: at ~40px,
   0.25em/0.55em resolve to the specified 10px/22px, and 0.35em/0.75em resolve
   to 7px/15px at 20px. At the 14–16px sizes .glow-silver is actually used at
   (the hero domain word), a fixed 15px blur is wider than the glyphs are tall —
   every letter's halo overlaps its neighbours' and they merge into one faint
   slab instead of hugging the letters. Colours and alphas are unchanged. */
.glow-emerald {
  font-style: italic;
  color: var(--accent-bright);
  text-shadow:
    0 0 0.25em rgba(34, 197, 94, 0.38),
    0 0 0.55em rgba(34, 197, 94, 0.15),
    0 1px 2px rgba(0, 0, 0, 0.3);
}

.glow-silver {
  font-style: italic;
  color: #eef2ee;
  text-shadow:
    0 0 0.35em rgba(232, 238, 233, 0.30),
    0 0 0.75em rgba(232, 238, 233, 0.13),
    0 1px 2px rgba(0, 0, 0, 0.3);
}

/* ==========================================================================
   Nav
   ========================================================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  background: rgba(7, 17, 13, 0.85);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hairline);
}

/* Same measure as .section-inner plus the gutter, so the brand sits on the
   exact left edge of every section's content below it. */
.nav-inner {
  width: 100%;
  max-width: calc(var(--content-max) + var(--gutter) * 2);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-sm);
}

.nav-brand {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  justify-self: start;
  min-width: 0;
}

.nav-name {
  font-size: 15px;
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
}

.nav-sep {
  color: var(--text-muted);
  opacity: 0.6;
}

.nav-role {
  font-size: 13px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav-pill {
  list-style: none;
  margin: 0;
  padding: 6px;
  display: flex;
  align-items: center;
  gap: 4px;
  justify-self: center;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.02);
}

.nav-pill li a {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  border-radius: 999px;
  font-size: 14px;
  color: var(--text-muted);
  transition: color 0.2s ease, background 0.2s ease;
}

.nav-pill li a:hover,
.nav-pill li a:focus-visible {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}

.nav-pill li a.active {
  color: var(--accent-bright);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  justify-self: end;
}

.nav-ext {
  font-size: 14px;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: color 0.2s ease;
}

.nav-ext:hover,
.nav-ext:focus-visible {
  color: var(--text);
}

.nav-ext .arrow {
  color: var(--accent);
}

.hamburger {
  display: none;
  justify-self: end;
  width: 44px;
  height: 44px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  border-radius: 8px;
}

.hamburger span {
  width: 20px;
  height: 2px;
  background: var(--text);
  border-radius: 2px;
  transition: transform 0.25s ease, opacity 0.25s ease;
}

.hamburger[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.hamburger[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}

.hamburger[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* Mobile full-screen menu */

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--bg-panel);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s;
}

.mobile-menu.open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.mobile-menu ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
}

.mobile-menu li a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  padding: 10px var(--space-md);
  font-size: 22px;
  color: var(--text);
}

.mobile-menu li a:hover,
.mobile-menu li a:focus-visible {
  color: var(--accent-bright);
}

.mobile-menu-close {
  position: absolute;
  top: 16px;
  right: 20px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: var(--text);
  border-radius: 8px;
}

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

.hero {
  min-height: calc(100vh - var(--nav-height));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  padding: var(--gutter);
}

/* The panel sizes to its content with generous padding — it no longer holds a
   fixed 75vh. A framed box is exactly where emptiness reads worst: a border
   turns leftover space into a visible void. So the frame hugs the copy, and
   whatever room is left over lives OUTSIDE it as unframed page, which reads as
   breathing room instead. The hero section is still a full viewport tall and
   centres the panel within it, so the panel still commands the first view. */
.hero-window {
  width: 100%;
  max-width: var(--content-max);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--bg-panel);
  box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.6);
  overflow: visible; /* never clip the glow */
}

.window-titlebar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--hairline);
}

/* The dots stay neutral, deliberately. The frame is a suggestion of a browser
   window, not an imitation of one — macOS traffic-light colours were tried here
   and pull the eye straight to the top-left corner, away from the headline the
   panel exists to frame. Only the accent word and interactive cues carry colour
   on this site (§2 of DESIGN-BRIEF.md, the one-colour rule). Do not re-introduce
   red and amber for the sake of realism. */
.window-titlebar .dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
}

/* Generous padding is what gives the panel its presence now that it is content
   sized. Centring is kept so the copy stays balanced if anything ever does set
   a min-height on the panel again. Text itself stays left-aligned. */
.hero-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(var(--space-xl), 8vh, var(--space-2xl))
           clamp(var(--space-lg), 5vw, var(--space-xl));
}

.hero-heading {
  font-size: clamp(2.6rem, 4.4vw, 3.9rem);
  max-width: 14ch;
}

.hero-subline {
  margin-top: var(--space-sm);
  font-size: 13px;
  color: var(--text-muted);
}

.hero-domain-line {
  margin-top: var(--space-md);
  font-size: 14px;
  color: var(--text-muted);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35em;
}

.domain-word-wrap {
  display: inline-flex;
  align-items: baseline;
  overflow: visible; /* never clip the glow */
}

.domain-word {
  display: inline-block;
}

/* The transform lives on the wrapper, not the glow-bearing text itself —
   animating a transform directly on an element with text-shadow can make
   browsers clip the shadow to a compositing-layer box (renders as a
   rectangle instead of a soft halo). */
/* `backwards`, not `both`: the 100% keyframe is already the element's resting
   state, so dropping the forwards fill looks identical but leaves no transform
   applied once the slam finishes — which lets the browser drop the compositing
   layer instead of holding the glowing text in a promoted layer between cycles. */
.domain-word-wrap.slam {
  animation: slamIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}

.domain-word-wrap.fade {
  animation: domainFade 0.6s ease backwards;
}

@keyframes slamIn {
  0%   { transform: translateX(var(--slam-distance)); opacity: 0; }
  55%  { transform: translateX(-12px); opacity: 1; }
  74%  { transform: translateX(7px); }
  88%  { transform: translateX(-3px); }
  100% { transform: translateX(0); }
}

@keyframes domainFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.scroll-cue {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--accent);
  transition: color 0.2s ease, transform 0.2s ease;
}

.scroll-cue:hover,
.scroll-cue:focus-visible {
  color: var(--accent-bright);
  transform: translateY(3px);
}

.scroll-cue svg {
  width: 22px;
  height: 22px;
}

/* ==========================================================================
   Tablet (600–899px) — desktop rules everywhere except the wheel (not built yet)
   ========================================================================== */

@media (min-width: 600px) and (max-width: 899px) {
  .hero-window {
    max-width: 90%;
  }
}

/* ==========================================================================
   Phone (<600px)
   ========================================================================== */

@media (max-width: 599px) {
  .nav-inner {
    grid-template-columns: 1fr auto;
  }

  .nav-brand {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
  }

  .nav-sep {
    display: none;
  }

  .nav-name {
    font-size: 14px;
  }

  .nav-role {
    font-size: 10.5px;
    letter-spacing: 0.02em;
  }

  .nav-pill,
  .nav-links {
    display: none;
  }

  .hamburger {
    display: flex;
  }

  .hero {
    padding: var(--gutter) var(--gutter) var(--space-lg);
    gap: var(--space-md);
  }

  .hero-window {
    border-radius: 12px;
  }

  .hero-content {
    padding: var(--space-lg) var(--space-md);
  }

  .hero-heading {
    font-size: 29px;
    max-width: none;
  }

  /* 10.1: body text minimum 16px on phones — overrides the 13/14px desktop sizes.
     Spacing comes off the scale, same as desktop. */
  .hero-subline {
    font-size: 16px;
  }

  .hero-domain-line {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    font-size: 16px;
    margin-top: var(--space-sm);
  }

  .domain-word-wrap {
    height: 30px;
    align-items: center;
  }
}

/* ==========================================================================
   Shared section scaffolding (Wheelhouse, Story, Testimonials, Contact)
   ========================================================================== */

/* Section separation is ONE 2xl step, carried entirely on the top edge. Padding
   on both edges would stack two 96px paddings between every pair of sections and
   read as a 192px void — that doubling is most of why the page felt scattered.
   The last block on each page adds its own bottom padding (see .contact,
   .work-section). */
.section {
  padding: var(--space-2xl) var(--gutter) 0;
}

/* The hero already ends in its own padding and a scroll cue, so the first
   section after it closes up — otherwise this one gap dwarfs every other on
   the page. 40px here + the hero's 24px = one xl step. */
.hero + .section {
  padding-top: var(--space-lg);
}

.section-inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

.eyebrow {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent-soft);
  font-weight: 500;
}

/* Deliberately about half the hero heading's size, so section headings read as
   the second level of the hierarchy rather than competing with the h1.
   xs above: the eyebrow is a label ON this heading, not a line above it. */
.section-heading {
  margin-top: var(--space-xs);
  font-size: clamp(1.45rem, 2.4vw, 1.95rem);
}

/* sm: still inside the heading's group, but a clear step up from the eyebrow's
   xs. That 8 → 16 ratio is what ranks the three lines instead of flattening
   them into one evenly spaced stack. */
.section-sub {
  margin-top: var(--space-sm);
  color: var(--text-muted);
  font-size: 15px;
  max-width: 52ch;
}

.see-all-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-md);
  color: var(--text-muted);
  font-size: 14px;
  transition: color 0.2s ease;
}

.see-all-link:hover,
.see-all-link:focus-visible {
  color: var(--text);
}

.see-all-link .arrow {
  color: var(--accent);
}

/* ==========================================================================
   Generic project card + horizontally scrolling strip
   (used by the Wheelhouse teaser and Testimonials)
   ========================================================================== */

.card-strip {
  display: flex;
  gap: var(--space-sm);
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding: 4px 4px 8px;
}

.card-strip::-webkit-scrollbar {
  display: none;
}

.card {
  flex: 0 0 auto;
  width: 260px;
  border: 1px solid var(--hairline);
  border-radius: 12px;
  background: var(--bg-panel);
  padding: var(--space-sm);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  transition: transform 0.2s ease, border-color 0.2s ease;
}

@media (hover: hover) {
  .card:hover {
    transform: translateY(-3px);
    border-color: var(--accent);
  }
}

@media (hover: none) {
  .card:active {
    transform: scale(0.97);
    border-color: var(--accent);
  }
}

.card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-xs);
}

/* Client names are set as TEXT WORDMARKS — this site uses no client logos at all
   (§6). One rule covers all three slots the name appears in: the wheelhouse card
   (.card-client), the Work page card (.card-wordmark) and the project page header
   (.project-logo), so the same name reads identically wherever it lands. Change
   the style here, not in one of the three. */
.card-client,
.card-wordmark,
.project-logo {
  font-size: 15px;
  font-weight: 500;
  color: rgba(238, 244, 239, 0.88);
  letter-spacing: -0.01em;
  line-height: 1.3;
}

/* Lets a long name wrap and shrink rather than crushing the sector label that
   shares its flex row. min-width: 0 is what actually permits the shrink. */
.card-client {
  flex: 1 1 auto;
  min-width: 0;
}

.card-sector {
  font-size: 11px;
  color: var(--accent-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Project cards carry the short card line only (§8) — the full headline is the
   project page's own opening, and running both here read as a repeat. */
.card-desc {
  font-size: 13px;
  color: var(--text-muted);
  line-height: 1.5;
}

.card-strip-dots {
  display: none;
  gap: 6px;
  justify-content: center;
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--hairline);
  transition: width 0.2s ease, background 0.2s ease;
}

.dot.active {
  width: 18px;
  background: var(--accent-bright);
}

/* ==========================================================================
   Wheelhouse
   ========================================================================== */

.wheel-desktop {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The one deliberate exception to "sub-line → content is md–lg": the wheel is
     a large graphic that has to sit close under its own label, or it reads as
     floating free of the section. sm, and the box below hugs the pills exactly
     so this 16px is the whole gap you actually see. */
  margin-top: var(--space-sm);
}

.wheel-mobile {
  display: none;
  margin-top: var(--space-md);
}

.wheel {
  position: relative;

  /* Sized to the pills, not to a padded square. Everything inside is positioned
     from the centre, so this box only reserves layout space — and any slack in
     it becomes dead air between the sub-line and the wheel. Height is the ring
     plus one pill (44px, half above the top spoke's centre and half below the
     bottom one); the extra width is room for the side pills to spread into. */
  width: calc(var(--wheel-radius) * 2 + 80px);
  height: calc(var(--wheel-radius) * 2 + 44px);
  margin: 0 auto var(--space-md);

  /* Single source of truth for the wheel geometry. The ring diameter, the
     radial line lengths and the JS spoke positions all read from these, so the
     spokes always sit on a true circle that the ring passes through.

     The binding constraint is horizontal, not vertical: adjacent spokes sit
     0.707 × radius apart on the x-axis, and two neighbouring pills must not
     meet in that gap. At 111px that gap is 78px, and the widest adjacent pair
     ("Op Model" + "Strategy") needs 66px at the pill sizing below — about 12px
     of clearance. Radius and .spoke sizing move together: a smaller radius
     forces a smaller pill font/padding, and a larger one buys them back. */
  --wheel-radius: 111px;
  --hub-radius: 27px;
}

.wheel-ring {
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(var(--wheel-radius) * 2); /* passes through the spoke centres */
  height: calc(var(--wheel-radius) * 2);
  border: 1px solid var(--hairline);
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

.wheel-spoke-lines {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Each line spans the gap between the hub and its pill: it starts on the outside
   of the centre circle and stops on the outside of the oval, so it never runs
   under the label. Both distances are measured per spoke in main.js, because the
   pills are wider than they are tall — the side pills' edges sit closer to the
   hub than the top and bottom ones, so the lines are legitimately not all the
   same length. rotate() then translateX() pushes the segment out along its ray. */
.spoke-line {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 1px;
  width: var(--line-length);
  background: var(--hairline);
  transform-origin: 0 50%;
  transform: rotate(var(--angle)) translateX(var(--line-start));
  transition: background 0.3s ease, box-shadow 0.3s ease;
}

.spoke-line.active {
  background: var(--accent);
  box-shadow: 0 0 6px rgba(22, 163, 74, 0.4);
}

.wheel-center {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: calc(var(--hub-radius) * 2);
  height: calc(var(--hub-radius) * 2);
  border-radius: 50%;
  border: 1px solid var(--hairline);
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

.wheel-center-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent-bright);
  box-shadow: 0 0 12px rgba(37, 198, 94, 0.6);
  animation: pulseDot 2s ease-in-out infinite;
}

@keyframes pulseDot {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.4); opacity: 0.6; }
}

.spoke {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) translate(var(--x), var(--y));

  /* Width follows the label rather than a fixed 92px — that is what keeps the
     wheel compact (see --wheel-radius). The 11px vertical padding keeps the
     pill 44px tall, so it stays a comfortable tap target on a touch tablet
     where this wheel still renders — and .wheel's height calc assumes that 44. */
  padding: 11px 8px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text-muted);
  font-size: 12px;
  text-align: center;
  white-space: nowrap;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease,
    box-shadow 0.2s ease;
}

.spoke:hover,
.spoke:focus-visible {
  color: var(--text);
}

/* The active pill fills solid rather than tinting, so the lit spoke reads at a
   glance and the fill stays opaque against the line arriving at its edge. */
.spoke.active {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 14px rgba(22, 163, 74, 0.35);
}

/* Belongs to the wheel above it (md), and is a separate group from the cards
   below it (lg) — the asymmetry is what tells you which way to read. */
.wheel-detail {
  text-align: center;
  margin-bottom: var(--space-lg);
}

.wheel-detail-name {
  font-size: 20px;
}

.wheel-detail-line {
  margin-top: var(--space-xs);
  color: var(--text-muted);
  font-size: 14px;
  max-width: 52ch;
  margin-inline: auto;
}

#wheel-strip-desktop {
  justify-content: center;
  max-width: 900px;
  margin: 0 auto;
}

/* Mobile vertical list (10.5) */

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

.wheel-row {
  width: 100%;
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px var(--space-sm);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.02);
  color: var(--text-muted);
  font-size: 16px;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.wheel-row[aria-expanded="true"] {
  color: var(--text);
  border-color: rgba(37, 198, 94, 0.4);
}

.wheel-row .chevron {
  color: var(--text-muted);
  transition: transform 0.2s ease, color 0.2s ease;
}

.wheel-row[aria-expanded="true"] .chevron {
  color: var(--accent-bright);
  transform: rotate(90deg);
}

.wheel-row-detail {
  padding: var(--space-sm) 4px 4px;
}

.wheel-row-detail[hidden] {
  display: none;
}

.wheel-row-detail .wheel-detail-name {
  font-size: 17px;
  text-align: left;
}

.wheel-row-detail .wheel-detail-line {
  margin-top: 6px;
  font-size: 15px;
  color: var(--text-muted);
  text-align: left;
  margin-inline: 0;
}

.wheel-row-detail .card-strip {
  margin-top: var(--space-md);
}

/* ==========================================================================
   Story
   ========================================================================== */

/* minmax(0, 1fr) rather than 1fr: a bare 1fr floors at the content's min size,
   which lets the copy column push past half and squeeze the media column — the
   asymmetry that read as "content hugging the left, right side empty". Both
   columns now split the full --content-max evenly and fill what they are given.
   xl gap: this is a group-to-group boundary, the widest gap inside any section. */
.story-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-xl);
  align-items: start;
}

/* md: the heading previously butted straight onto this text with no gap at all,
   which is why the Story block read as one undifferentiated slab. */
.story-text {
  margin-top: var(--space-md);
}

/* 16px, not 15: the column is wider now, and 15px over ~600px runs past a
   comfortable measure. At 16px the same column is about 68 characters. */
.story-text p {
  margin-top: var(--space-sm);
  color: var(--text-muted);
  font-size: 16px;
}

.story-text p:first-child {
  margin-top: 0;
}

.pill-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-md) 0 0;
  padding: 0;
}

.pill-tags li {
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 8px 14px;
  font-size: 12px;
  color: var(--accent-soft);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-photos {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-sm);
}

/* Same 12px radius and 1px weight as .card, so the photos read as part of the
   same family. overflow: hidden is what clips the image corners to the radius —
   safe here because nothing inside carries a glow (see §2's note on clipping
   halos). The gradient shows through only while the image is still loading.

   The border is deliberately NOT --hairline (0.1). A photo fills its frame edge
   to edge, so the border has image on one side instead of page background, and
   at 0.1 the edge disappears into the picture. 0.18 is scoped to these frames
   only — do not raise --hairline itself, which would brighten every card and
   divider on the site. Weight and radius are unchanged. */
.photo-frame {
  aspect-ratio: 3 / 4;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: linear-gradient(160deg, #1b2620, #0e1713);
  overflow: hidden;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

/* cover, so a portrait fills the 3:4 frame without distorting — the crop is
   what gives, not the proportions of the face.

   The grade: 85% saturation knocks the photos back so they sit in the palette
   instead of fighting it, and the small positive hue-rotate cools the greens and
   greys toward emerald. It is deliberately mild — enough to unify, not enough to
   read as a treatment. No duotone, no overlay. */
.photo-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* One grade, three places: the Story photos above and the project screenshots
   (§7) — gallery stage and phone carousel. Change it here, not in one of the
   three, so photography and screenshots keep sitting in the palette together. */
.photo-frame img,
.gallery-shot,
.mobile-shot {
  filter: saturate(0.85) contrast(1.04) brightness(0.95) hue-rotate(6deg);
  transition: filter 0.4s ease;
}

/* Matches .card's hover: the border picks up the accent, plus a soft emerald
   halo, and the grade lifts to full colour. Hover-capable pointers only — on
   touch there is no hover state to return from, so the graded version stands. */
@media (hover: hover) {
  .photo-frame:hover {
    border-color: var(--accent);
    box-shadow: 0 0 18px -2px rgba(22, 163, 74, 0.35);
  }

  .photo-frame:hover img {
    filter: none;
  }
}

/* lg above: the timeline is a separate group from the photos. The 20px left
   padding is NOT off the scale — it is optical alignment to the hairline rule,
   and .timeline-dot's -25.5px offset is measured from it (20 padding + 1 border
   + 4.5 half-dot). Change one and you must change the other. */
.timeline {
  list-style: none;
  margin: var(--space-lg) 0 0;
  padding: 0 0 0 20px;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  border-left: 1px solid var(--hairline);
}

.timeline li {
  position: relative;
}

.timeline-dot {
  position: absolute;
  left: -25.5px;
  top: 4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent-bright);
  box-shadow: 0 0 8px rgba(37, 198, 94, 0.5);
}

.timeline-date {
  display: block;
  font-size: 12px;
  color: var(--accent-soft);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.timeline-text {
  display: block;
  margin-top: 4px;
  font-size: 14px;
  color: var(--text);
}

/* ==========================================================================
   Testimonials
   ========================================================================== */

/* Matches .section-inner's measure plus the gutter, so the cards start on the
   same left edge as the heading above them and run the full width — four 320px
   cards now very nearly fill the row instead of stopping two-thirds across.
   lg above: heading and strip are separate groups (8px read as a mistake). */
.testimonials-strip-wrap {
  padding: 0 var(--gutter);
  margin-top: var(--space-lg);
  max-width: calc(var(--content-max) + var(--gutter) * 2);
  margin-inline: auto;
}

.testimonial-card {
  width: 320px;
}

.testimonial-quote {
  font-size: 14px;
  color: var(--text-muted);
  line-height: 1.6;
}

.testimonial-author {
  font-size: 13px;
  color: var(--text);
  margin-top: auto;
}

.testimonial-author span {
  color: var(--text-muted);
}

/* ==========================================================================
   Contact / footer
   ========================================================================== */

/* .section carries its 2xl separation on the top edge only, so the last block
   on the page closes itself out. xl rather than 2xl: nothing follows it, so it
   only needs to not end abruptly. */
.contact {
  padding-bottom: var(--space-xl);
}

/* The contact body plays the sub-line role here: sm below the heading. */
.contact-body {
  margin-top: var(--space-sm);
  max-width: 60ch;
  color: var(--text-muted);
  font-size: 15px;
}

/* md, not lg — the buttons are this copy's call to action, so they stay inside
   the group. The lg below .contact-details is what separates the two. */
.contact-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  padding: 12px 22px;
  border-radius: 8px;
  font-size: 14px;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.btn-solid {
  background: var(--text);
  color: var(--bg);
}

.btn-solid:hover,
.btn-solid:focus-visible {
  background: #ffffff;
}

.btn-outline {
  border: 1px solid var(--hairline);
  color: var(--text-muted);
}

.btn-outline:hover,
.btn-outline:focus-visible {
  color: var(--text);
  border-color: var(--accent);
}

.btn .arrow {
  color: var(--accent);
}

.contact-details {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-lg);
  font-size: 13px;
  color: var(--text-muted);
}

.detail-label {
  color: var(--accent-soft);
  font-size: 11px;
  letter-spacing: 0.08em;
  margin-right: 6px;
}

.detail-sep {
  color: var(--hairline);
}

.contact-details a {
  color: var(--text-muted);
  transition: color 0.2s ease;
}

.contact-details a:hover,
.contact-details a:focus-visible {
  color: var(--text);
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--space-md);
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--hairline);
}

/* Copyright and the built-by credit stack as one left-hand group, so the
   whakataukī stays on the right — .footer-bottom is space-between, and a third
   direct child would land in the middle of the row. */
.footer-left {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.footer-copyright {
  font-size: 13px;
  color: var(--text-muted);
}

/* Muted grey, not the accent: §2 reserves green for interactive cues, and this
   line is a credit, not something to click. */
.footer-built {
  font-size: 11px;
  color: var(--text-muted);
}

.footer-whakatauki {
  text-align: right;
}

.whakatauki-line {
  display: block;
  font-style: italic;
  color: var(--accent-soft);
  font-size: 14px;
}

.footer-translation {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-muted);
  opacity: 0.75;
}

/* ==========================================================================
   Phone (<600px) — new sections
   ========================================================================== */

@media (max-width: 599px) {
  /* One step down the scale from desktop's 2xl, still top-edge only. --gutter
     is already 18px on phones, so the inset comes along for free. */
  .section {
    padding: var(--space-xl) var(--gutter) 0;
  }

  .contact {
    padding-bottom: var(--space-lg);
  }

  /* 10.1: body text minimum 16px on phones */
  .story-text p,
  .contact-body,
  .testimonial-quote,
  .wheel-detail-line,
  .card-desc,
  .section-sub {
    font-size: 16px;
  }

  /* 10.5: no circle below 600px — vertical list, tap-only */
  .wheel-desktop {
    display: none;
  }

  .wheel-mobile {
    display: block;
  }

  /* Story stacks — the xl gap becomes lg once the columns are one on top of
     the other, since stacking already separates them. */
  .story-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-lg);
  }

  /* 10.7: photos stack full-width. Side by side they land at ~165px on a 375px
     screen, too small to carry the section — which works by the contrast between
     the two images reading clearly. sm rather than xs between them: once they are
     full-width they need a real gap to read as two photos, not a strip. */
  .story-photos {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-sm);
  }

  /* 10.6: testimonials bleed to the edge, swipe with snap + dots, no arrows */
  .testimonials-strip-wrap {
    padding: 0;
    max-width: none;
  }

  .card-strip.testimonials-strip {
    padding-left: var(--gutter);
    padding-right: var(--gutter);
    scroll-snap-type: x mandatory;
  }

  .testimonial-card {
    width: 280px;
    scroll-snap-align: center;
  }

  .testimonials-strip-wrap .card-strip-dots {
    display: flex;
    margin-top: var(--space-sm);
  }

  /* Wheelhouse mobile detail card strip — same swipe + dots treatment */
  .wheel-row-detail .card-strip {
    scroll-snap-type: x mandatory;
  }

  .wheel-row-detail .card {
    width: 240px;
    scroll-snap-align: center;
  }

  .wheel-row-detail .card-strip-dots {
    display: flex;
    margin-top: var(--space-sm);
  }

  /* Contact stacks */
  .contact-buttons {
    flex-direction: column;
    align-items: stretch;
  }

  .contact-buttons .btn {
    width: 100%;
  }

  .footer-bottom {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
  }

  .footer-whakatauki {
    text-align: left;
  }
}

/* ==========================================================================
   WORK PAGE — atmosphere (§4 cursor spotlight / §10.4 ambient glow)
   Both are scoped to .page-work so Home stays bold and unlit. Crossing between
   them should feel like entering a different room.
   ========================================================================== */

.spotlight {
  position: fixed;
  top: 0;
  left: 0;
  width: 360px;
  height: 360px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(34, 197, 94, 0.14), transparent 68%);
  pointer-events: none;
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity 0.3s;
  z-index: 1;
  display: none;
}

/* Cursor-dependent, so it only exists where there is a cursor. */
@media (hover: hover) {
  .page-work .spotlight {
    display: block;
  }
}

/* §4: hide it entirely under reduced motion (not just freeze it). */
@media (prefers-reduced-motion: reduce) {
  .page-work .spotlight {
    display: none;
  }
}

/* §10.4: touch devices get the same mood without a cursor. The global
   reduced-motion rule freezes this at its resting keyframe, which leaves a
   static glow — §11 asks for the drift to stop, not the atmosphere. */
.ambient-glow {
  display: none;
  position: fixed;
  top: -25%;
  left: -15%;
  width: 130vw;
  height: 130vh;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(34, 197, 94, 0.13), transparent 65%);
  pointer-events: none;
  z-index: 1;
  animation: drift 14s ease-in-out infinite;
}

@media (hover: none) {
  .page-work .ambient-glow {
    display: block;
  }
}

@keyframes drift {
  0%, 100% { transform: translate(-10%, -6%) scale(1); }
  50%      { transform: translate(12%, 8%) scale(1.15); }
}

/* Content sits above the glow. */
.page-work main,
.page-work .work-footer {
  position: relative;
  z-index: 2;
}

/* ==========================================================================
   WORK PAGE — project strip (§6)
   ========================================================================== */

/* Only block on the page, so it closes itself out — see the note on .contact. */
.work-section {
  padding-bottom: var(--space-xl);
}

/* This one is a page h1, not a section h2 — it opts out of the reduced
   .section-heading size so it still leads the Work page. */
.work-heading {
  font-size: clamp(1.8rem, 3.2vw, 2.6rem);
  max-width: 24ch;
}

/* Same measure + gutter as .testimonials-strip-wrap, so both card strips line
   up with the section heading above them. lg: heading → its content. */
.work-strip-wrap {
  position: relative;
  max-width: calc(var(--content-max) + var(--gutter) * 2);
  margin: var(--space-lg) auto 0;
  padding: 0 var(--gutter);
}

.work-strip {
  padding: 6px 4px 12px;
}

.project-card {
  width: 320px;
}

/* Type is set in the shared wordmark rule above — this is layout only. */
.card-wordmark {
  flex: 1 1 auto;
  min-width: 0;
}

.project-card .card-sector {
  flex: 0 0 auto;
  text-align: right;
  white-space: nowrap;
  padding-top: 2px;
}

/* One circular arrow button, shared by the Work strip (§6) and the project
   galleries (§7). The look is defined here once — change it here, not in one of
   the two. .strip-arrow then adds the absolute positioning it needs to float
   over the cards; the gallery arrows sit in a control row and take visuals only. */
.strip-arrow,
.gallery-arrow {
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--hairline);
  border-radius: 50%;
  background: var(--bg-panel);
  color: var(--accent);
  font-size: 24px;
  line-height: 1;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}

.strip-arrow {
  position: absolute;
  top: 50%;
  display: none;
  transform: translateY(-50%);
  z-index: 3;
}

.strip-arrow span,
.gallery-arrow span {
  display: block;
  margin-top: -3px;
}

.strip-arrow:hover,
.strip-arrow:focus-visible,
.gallery-arrow:hover,
.gallery-arrow:focus-visible {
  color: var(--accent-bright);
  border-color: var(--accent);
  background: var(--bg);
}

.strip-arrow-prev { left: 0; }
.strip-arrow-next { right: 0; }

/* Arrows are for manual browsing with a cursor (§6). On touch they are replaced
   by swipe + dots (§10.6), so they only appear where hover exists — and the
   gutter widens to hold them clear of the cards. */
@media (hover: hover) {
  .strip-arrow {
    display: flex;
  }

  .work-strip-wrap {
    padding: 0 var(--space-xl);
  }
}

/* §10.6: no auto-drift on touch — a swipeable snapping carousel with progress
   dots instead. Gated on hover, not width, so touch tablets get it too. */
@media (hover: none) {
  .work-strip {
    scroll-snap-type: x mandatory;
  }

  .project-card {
    scroll-snap-align: center;
  }

  .work-section .card-strip-dots {
    display: flex;
    margin-top: var(--space-sm);
  }
}

.work-footer {
  padding: 0 var(--gutter) var(--space-lg);
}

.work-footer .footer-bottom {
  margin-top: 0;
}

/* Work page — phone (<600px) */

@media (max-width: 599px) {
  .work-heading {
    max-width: none;
  }

  /* §10.6: let the strip bleed to the screen edges. */
  .work-strip-wrap {
    max-width: none;
    margin-top: var(--space-md);
    padding: 0;
  }

  .work-strip {
    padding-left: var(--gutter);
    padding-right: var(--gutter);
  }

  .project-card {
    width: 280px;
  }

  .work-footer {
    padding: 0 var(--gutter) var(--space-md);
  }
}

/* ==========================================================================
   PROJECT PAGES (§7) — one template, eight pages
   Project pages carry .page-work on <body>, which lights the cursor spotlight
   and the touch ambient glow defined above. Same room as work.html.
   ========================================================================== */

/* Scroll reveal (§7). Scoped to .js-reveal, which project.js's inline head
   script sets — so if scripting fails the content is simply visible rather than
   permanently transparent. */
.reveal {
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.js-reveal .reveal {
  opacity: 0;
  transform: translateY(24px);
}

.js-reveal .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal {
    opacity: 1;
    transform: none;
  }
}

.project-page {
  padding: var(--space-lg) var(--gutter) var(--space-2xl);
}

/* Deliberately narrower than --content-max. This is a long-form reading column,
   and the scale governs rhythm, not measure — text set across 1280px is
   unreadable no matter how well spaced it is. */
.project-inner {
  max-width: 860px;
  margin: 0 auto;
}

/* 1. Top bar */

.project-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 44px;
  font-size: 14px;
  color: var(--text-muted);
  transition: color 0.2s ease;
}

.back-link:hover,
.back-link:focus-visible {
  color: var(--text);
}

.back-link .arrow {
  color: var(--accent);
}

.project-counter {
  font-size: 12px;
  letter-spacing: 0.12em;
  color: var(--text-muted);
}

/* 2. Header */

.project-header {
  margin-top: var(--space-lg);
}

/* Type is set in the shared wordmark rule near .card-client. */

.project-meta {
  margin-top: var(--space-xs);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent-soft);
}

.project-headline {
  margin-top: var(--space-sm);
  font-size: clamp(1.9rem, 3.4vw, 2.6rem);
  max-width: 20ch;
}

.project-header .pill-tags {
  margin-top: var(--space-md);
}

/* 3a. Visual block — gallery variant */

.project-visual {
  margin-top: var(--space-xl);
}

.gallery-frame {
  position: relative;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--bg-panel);
  box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.6);
}

/* The same bottom fade the phone carousel has: a screenshot cut off at the
   frame edge reads as a bad crop, and the fade says "this continues". It lives
   on the frame rather than in the track so it stays put while slides move past
   underneath, and is inset by the 10px mat so it fades the image, not the mount.
   Percentage height, so it keeps its proportion as the frame scales with the
   window. pointer-events: none is what keeps the lightbox reachable — a click
   in the faded area passes straight through to the image behind it. */
.gallery-frame::after {
  content: '';
  position: absolute;
  right: 10px;
  bottom: 10px;
  left: 10px;
  height: 18%;
  border-radius: 0 0 6px 6px;
  background: linear-gradient(to bottom, rgba(7, 17, 13, 0), rgba(7, 17, 13, 0.92));
  pointer-events: none;
  z-index: 1;
}

/* No padding here, deliberately. A slide's flex-basis of 100% resolves against
   this element's CONTENT box, but the scrollport is its padding box — so any
   padding here makes every slide narrower than the scrollport and leaves a
   sliver of the next one showing. The mat lives on .gallery-slide instead. */
.gallery-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
  background: var(--bg-panel);
  border-radius: 0 0 13px 13px;
}

.gallery-track::-webkit-scrollbar {
  display: none;
}

/* The 10px mat. The screenshot floats on panel rather than butting into the
   frame border, the way a print sits inside a mount. It sits on the slide, not
   the track — see the note above. box-sizing: border-box (global reset) keeps
   the slide exactly one scrollport wide with the padding inside it. */
.gallery-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  padding: 10px;
}

/* 2:1 — the ratio the screenshots are captured at. The gradient is a backdrop
   while the image loads, not decoration. The hairline ring reads as the edge of
   the screenshot itself against the mat; the grade is set with the Story photos
   above. No zoom cursor here — see the (hover: none) block below. */
.gallery-shot {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 1;
  object-fit: cover;
  border-radius: 6px;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
  background: linear-gradient(160deg, #16211c, #0a1310);
}

/* Tap-to-zoom is wired for touch only (§10.8 — project.js gates the listener on
   !hover), so everything that advertises it belongs here. On a desktop pointer
   the cursor stays default and the grade lifting to full colour is the whole
   hover story; a zoom-in cursor there would promise a click nothing listens for.

   The hint is decorative: pointer-events: none keeps the tap landing on the
   image beneath, and z-index 2 lifts it clear of the frame's bottom fade
   (z-index 1) so it does not dissolve into it. Two pseudo-elements because the
   glyph is a mask — it has to carry var(--accent-soft) rather than a colour
   baked into the SVG, and a mask would take the backdrop with it. */
@media (hover: none) {
  .gallery-shot {
    cursor: zoom-in;
  }

  .gallery-slide {
    position: relative;
  }

  /* 28px inset: 10px clears the mat, then 18px in from the image's corner. */
  .gallery-slide::before,
  .gallery-slide::after {
    content: '';
    position: absolute;
    right: 28px;
    bottom: 28px;
    width: 28px;
    height: 28px;
    pointer-events: none;
    z-index: 2;
  }

  .gallery-slide::before {
    border-radius: 8px;
    background: rgba(7, 17, 13, 0.72);
  }

  .gallery-slide::after {
    background-color: var(--accent-soft);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 4h6v6M20 4l-8 8M10 20H4v-6M4 20l8-8'/%3E%3C/svg%3E") center / 16px 16px no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 4h6v6M20 4l-8 8M10 20H4v-6M4 20l8-8'/%3E%3C/svg%3E") center / 16px 16px no-repeat;
  }
}

/* The controls sit under the frame, not on top of the image — arrows left, dots
   right. Over the image they covered the screenshot they were there to browse. */
.gallery-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

.gallery-arrows {
  display: none;
  gap: var(--space-xs);
}

.gallery-arrow {
  display: flex;
}

/* §10.6 / §10.8: no arrows on touch — swipe with snap and dots instead.
   Hiding the wrapper rather than the buttons keeps the row's gap off the dots. */
@media (hover: hover) {
  .gallery-arrows {
    display: flex;
  }
}

.gallery-dots {
  display: flex;
  gap: 6px;
  justify-content: center;
}

/* The phone carousel — one phone showing one screenshot at a time, with its own
   arrows and dots. Same behaviour as the gallery above it, at phone width, so
   the two read as one control language rather than two. */

.mobile-strip-wrap {
  margin-top: var(--space-lg);
}

.phone-carousel {
  width: 300px;
  max-width: 100%;
  margin-top: var(--space-sm);
}

/* Concentric radii: the bezel's 30px less its own 10px of padding leaves 20px
   for the screen, so the two curves run parallel instead of drifting apart at
   the corners. Change one and you must change the other. The border runs at
   0.16 rather than --hairline for the same reason .photo-frame does — a screen
   fills the bezel edge to edge, and at 0.1 the edge disappears into the image. */
.phone-frame {
  padding: 10px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 30px;
  background: var(--bg-panel);
}

/* Decorative — a phone reads as a phone with an earpiece slot at the top. */
.phone-notch {
  display: block;
  width: 44px;
  height: 4px;
  margin: 0 auto 8px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.16);
}

/* The screen clips the track's corners; the track cannot do it itself because
   it has to stay overflow-x: auto to scroll. */
.phone-screen {
  position: relative;
  border-radius: 20px;
  overflow: hidden;
}

/* These screenshots are taller than the screen, so every one of them runs off
   the bottom edge. The fade says "this continues" — without it the cut reads as
   a careless crop. Above the image, and never in the way of a swipe. */
.phone-screen::after {
  content: '';
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 74px;
  background: linear-gradient(to bottom, rgba(7, 17, 13, 0), rgba(7, 17, 13, 0.92));
  pointer-events: none;
  z-index: 1;
}

.mobile-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.mobile-track::-webkit-scrollbar {
  display: none;
}

.mobile-slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
}

/* No radius or ring: the bezel already separates the screen from the page, and
   a third outline inside it just muddies the edge. No zoom-in cursor either —
   the lightbox is bound to the gallery only, so these are not clickable and
   must not claim to be. */
.mobile-shot {
  display: block;
  width: 100%;
  height: auto;
}

/* The grade lifts on hovering the frame, matching the Story photos and the
   gallery. Hover-capable pointers only: on touch there is nothing to return
   from, so the graded version stands. */
@media (hover: hover) {
  .gallery-frame:hover .gallery-shot,
  .phone-frame:hover .mobile-shot {
    filter: none;
  }
}

/* §11: the grade still applies, it just arrives instantly. */
@media (prefers-reduced-motion: reduce) {
  .gallery-shot,
  .mobile-shot {
    transition: none;
  }
}

/* 3c. Visual block — phone beside outcome cards (Dial)
   Dial has one screen worth showing and no platform gallery, so the phone sits
   next to the outcome cards rather than carrying the block alone. */

.visual-split {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  gap: var(--space-xl);
  align-items: start;
}

/* One column below 900px — DOM order puts the phone first, then the cards. */
@media (max-width: 899px) {
  .visual-split {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* A static phone: one complete screen, no track, no arrows, no dots, no JS.
   Takes .phone-frame's bezel, notch, radii and grade unchanged — this modifier
   only sets the width .phone-carousel would otherwise give it, and removes the
   fade. */
.phone-frame-static {
  width: 300px;
  max-width: 100%;
}

/* No bottom fade here. The fade exists because a scrolled screenshot is cut off
   mid-content and needs to read as continuing. This is a whole screen with
   nothing below it, so a fade would only dim the Get Started button. */
.phone-frame-static .phone-screen::after {
  content: none;
}

/* Beside the phone the cards stack, so the two columns read as one pairing
   rather than a grid of four things. */
.outcome-column {
  grid-template-columns: minmax(0, 1fr);
}

/* Two cards are shorter than the phone beside them, and .visual-split aligns to
   start — so the column is centred against the phone instead of hanging at the
   top of a taller row. Below 900px the split is a single column and this is a
   no-op, since the row is only as tall as the cards. */
.visual-split .outcome-column {
  align-self: center;
}

/* 3b. Visual block — outcome cards variant */

.outcome-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-sm);
}

.outcome-card {
  border: 1px solid var(--hairline);
  border-radius: 12px;
  background: var(--bg-panel);
  padding: var(--space-md);
}

.outcome-value {
  font-size: clamp(1.7rem, 3vw, 2.2rem);
  font-weight: 500;
  line-height: 1.1;
  color: var(--text);
}

.outcome-label {
  margin-top: var(--space-xs);
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
}

/* 4–6. Challenge / Role / Outcome */

/* xl between blocks, sm inside one — the ratio is what makes Challenge / Role /
   Outcome read as three things rather than one long column of text. */
.project-block {
  margin-top: var(--space-xl);
}

.project-block .block-body {
  margin-top: var(--space-sm);
  font-size: 15px;
  color: var(--text-muted);
  max-width: 62ch;
}

/* 7. Next project */

/* A section boundary within the page, so it gets the 2xl step — split across
   the rule: xl above the line, lg below it. */
.next-projects {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--hairline);
}

.next-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.next-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  border: 1px solid var(--hairline);
  border-radius: 12px;
  background: var(--bg-panel);
  padding: var(--space-md);
  transition: transform 0.2s ease, border-color 0.2s ease;
}

@media (hover: hover) {
  .next-card:hover {
    transform: translateY(-3px);
    border-color: var(--accent);
  }
}

@media (hover: none) {
  .next-card:active {
    transform: scale(0.98);
    border-color: var(--accent);
  }
}

.next-card .card-sector {
  font-size: 11px;
}

.next-card-title {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--text);
}

.next-card-go {
  font-size: 13px;
  color: var(--text-muted);
}

.next-card-go .arrow {
  color: var(--accent);
}

/* Tap-to-zoom (§10.8). Touch only — a cursor already gets a large, legible
   frame. The overlay leaves native pinch-zoom alone rather than reimplementing
   it in JS. */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  overflow: auto;
  background: rgba(4, 10, 7, 0.96);
  touch-action: pinch-zoom;
}

.lightbox.open {
  display: flex;
}

.lightbox-content {
  width: 100%;
  max-width: var(--content-max);
}

.lightbox-close {
  position: absolute;
  top: 12px;
  right: 16px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: var(--text);
  border-radius: 8px;
}

/* Project pages — phone (<600px) */

@media (max-width: 599px) {
  .project-page {
    padding: var(--space-md) var(--gutter) var(--space-xl);
  }

  .project-headline {
    font-size: 27px;
    max-width: none;
  }

  /* §10.1: body text minimum 16px on phones */
  .project-block .block-body,
  .outcome-label,
  .next-card-title {
    font-size: 16px;
  }

  /* §10.8: outcome cards stack full-width */
  .outcome-row {
    grid-template-columns: 1fr;
  }

  /* §10.8: next-project cards stack vertically */
  .next-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .project-block {
    margin-top: var(--space-lg);
  }

  .project-header {
    margin-top: var(--space-md);
  }

  /* §10.6: arrows are gone on touch, so the dots take the row on their own and
     centre, the way every other strip on the site does. */
  .gallery-controls {
    justify-content: center;
  }
}
