@import './tokens.css';

@font-face {
  font-family: "Suisse Int'l Light";
  src: url('./fonts/SuisseIntl-Light.otf') format('opentype');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Suisse Int'l Book";
  src: url('./fonts/SuisseIntl-Book.otf') format('opentype');
  font-weight: 450;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Suisse Int'l Semi Bold";
  src: url('./fonts/SuisseIntl-SemiBold.otf') format('opentype');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Suisse Int'l Mono";
  src: url('./fonts/SuisseIntlMono-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

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

html, body {
  background: var(--color-surface-page);
  color: var(--color-text-primary);
  font-family: "Suisse Int'l Book", Inter, sans-serif;
  font-weight: 450;
  word-spacing: var(--word-spacing-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background-color 500ms var(--ease-smooth),
              color 500ms var(--ease-smooth);
}


body { opacity: 1; }

/* In-page anchor targets clear the fixed header plus a breath of space, so
 * a section heading never parks under the header after a scroll. The lerp
 * scroll in main.js reads this via getComputedStyle(el).scrollMarginTop;
 * native scroll (reduced-motion, find-in-page) honours it too.
 * --header-height is measured + published by initHeaderHeightVar() in
 * main.js; the fallback keeps the offset sane before JS runs. */
[id] {
  scroll-margin-top: calc(var(--header-height, 72px) + var(--space-8));
}

/* Per-media fade-in. initMediaSkeleton() in main.js adds .is-media-loading to
 * the parent of every <img>/<video> until it has loaded its first frame, so the
 * media fades in over the quiet static placeholder instead of popping in. No
 * shimmer — the moving highlight read as distracting. */
.is-media-loading > img,
.is-media-loading > video {
  opacity: 0;
}

img, video {
  transition: opacity 320ms var(--ease-smooth);
}

h1, h2, h3, h4, h5, h6 { font-weight: 600; word-spacing: normal; }

a {
  color: inherit;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  transition: color 260ms var(--ease-smooth);
}

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

/* ── Header — hides on scroll down, reveals on scroll up or mouse near top */
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-6) var(--space-12);
  background: var(--color-surface-page);
  z-index: var(--z-nav);
  transform: translateY(0);
  transition: transform 420ms var(--ease-smooth);
}

.header.is-hidden {
  transform: translateY(-100%);
}

.menu-toggle {
  width: 24px;
  height: 24px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  padding: 0;
}

.brand {
  font-size: 18px;
  line-height: 24px;
  letter-spacing: -0.03em;
  font-weight: 600;
  color: var(--color-text-primary);
}

.menu-icon-bar {
  display: block;
  width: 100%;
  height: 1.5px;
  background: var(--color-text-primary);
  transition: transform 260ms var(--ease-smooth), opacity 260ms var(--ease-smooth);
}

.menu-toggle[aria-expanded="true"] .menu-icon-bar:nth-child(1) {
  transform: translateY(3.25px) rotate(45deg);
}
.menu-toggle[aria-expanded="true"] .menu-icon-bar:nth-child(2) {
  transform: translateY(-3.25px) rotate(-45deg);
}

/* ── Fullscreen Menu ───────────────────────
 * Editorial takeover: the whole viewport becomes the nav.
 * Three columns — current-location, projects, secondary — set
 * in large display type. Opens with a fade + content stagger. */
.site-menu {
  position: fixed;
  inset: 0;
  background: var(--color-surface-page);
  z-index: var(--z-modal);
  display: flex;
  flex-direction: column;
  padding: var(--space-6) var(--space-12);
  opacity: 0;
  visibility: hidden;
  transition: opacity 420ms var(--ease-smooth),
              visibility 0s linear 420ms;
  /* The shell stays put — header pinned to viewport top, footer
   * pinned to bottom. Only `.site-menu-inner` scrolls. Without
   * this the whole menu moved as a sheet on mobile. */
  overflow: hidden;
}

.site-menu[aria-hidden="false"] {
  opacity: 1;
  visibility: visible;
  transition: opacity 520ms var(--ease-smooth),
              visibility 0s linear 0s;
}

/* align-items: flex-start keeps the Greg M. wordmark at the exact
 * same Y position as the sticky header's .brand — the close button
 * is taller, but it extends downward rather than pushing the brand. */
.site-menu-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.site-menu-brand {
  font-size: 18px;
  line-height: 24px;
  letter-spacing: -0.03em;
  font-weight: 600;
  color: var(--color-text-primary);
}

/* Bar-based close. Two thin bars form an × at rest; on hover the
 * wrapper makes a slow quarter-turn while the bars stay fixed at
 * ±45°, so the X stays an X — just rotates a touch. Uses the same
 * bar primitive as the hamburger toggle so the chrome shares one
 * visual language. */
.site-menu-close {
  position: relative;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: var(--color-surface-inverse);
  color: var(--color-text-inverse);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  /* Optical alignment with the chrome behind it:
   *   - margin-top:-10px → button centre lands on the brand's
   *     first-line optical centre (button is 44px tall vs brand
   *     line-height 24px).
   *   - margin-right:-10px → button centre lands on the same x
   *     as the sticky page header's 24px hamburger toggle, so
   *     the X mark sits exactly where the bars sat. */
  margin-top: -10px;
  margin-right: -10px;
  transition: background 320ms var(--ease-smooth),
              transform 700ms cubic-bezier(0.22, 1, 0.36, 1);
}

.site-menu-close-bar {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 1.5px;
  background: currentColor;
  border-radius: 1px;
}

.site-menu-close-bar:nth-child(1) { transform: translate(-50%, -50%) rotate(45deg); }
.site-menu-close-bar:nth-child(2) { transform: translate(-50%, -50%) rotate(-45deg); }

.site-menu-close:hover {
  transform: rotate(90deg);
}

.site-menu-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-surface-page),
              0 0 0 4px var(--btn-focus-ring);
}

/* WebKit scrollbar hide for the inner — paired with scrollbar-width
 * for Firefox. */
.site-menu-inner::-webkit-scrollbar { display: none; }

/* Menu body maps directly to the site's 12-column grid so the
 * three menu columns (nav / projects / contact) snap to the same
 * column edges as the rest of the page. */
.site-menu-inner {
  flex: 1 1 auto;
  /* min-height:0 lets a flex item shrink below its content so its
   * own overflow can engage. Without it, the inner would grow to
   * content height and push the footer off-screen instead of
   * scrolling internally. */
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* Hide the scroll affordance — the menu reads as a static layout
   * even when the list is taller than the viewport. */
  scrollbar-width: none;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--space-4);
  row-gap: var(--space-12);
  /* Pin content into the upper third instead of centring it. With
   * `center` the columns landed visually low, around the 50% line
   * and below the natural reading start point. `start` + a viewport
   * top-pad puts them where the eye lands first. */
  align-content: start;
  width: 100%;
  padding: 18vh 0 var(--space-20);
}

/* Selected work owns the first third (4 of 12); the three secondary
 * columns — Photography and art · On this page · Connect — share the
 * remaining two-thirds contiguously, separated only by the normal
 * column gutter. The first two get 3 cols each (room for longer labels
 * without truncating); Connect's short links sit comfortably in 2.
 * Order: Selected projects · Services · On this page · Connect. */
.site-menu-inner > .menu-col:nth-child(1) { grid-column: 1 / span 4; }
.site-menu-inner > .menu-col:nth-child(2) { grid-column: 5 / span 3; }
.site-menu-inner > .menu-col:nth-child(3) { grid-column: 8 / span 3; }
.site-menu-inner > .menu-col:nth-child(4) { grid-column: 11 / span 2; }

.menu-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

.menu-col-label {
  font-family: "Suisse Int'l Book", Inter, sans-serif;
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--color-border-subtle);
}

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

/* Tonal current marker: non-current links sit at the tertiary text
 * tone, current and hover both rise to heading. The current item
 * is always the most prominent label in its column without needing
 * a graphic marker. Dark mode comes free via the token swap. */
.menu-link {
  font-size: 28px;
  line-height: 34px;
  letter-spacing: -0.03em;
  font-weight: 600;
  color: var(--color-text-tertiary);
  display: inline-block;
  transition: color 320ms var(--ease-smooth);
}

.menu-link:hover,
.menu-link.is-current {
  color: var(--color-text-heading);
}

/* Numbered "On this page" — counter renders as a small leading
 * "01 / 02 / …" in the tertiary tone, weight-light, with tabular
 * digits. Lives in a ::before so the digit-scramble (which mutates
 * textContent) doesn't strip the prefix. */
.menu-col-list-numbered { counter-reset: section-num; }
.menu-col-list-numbered .menu-link {
  counter-increment: section-num;
  /* Long section labels (e.g. art project titles like "How To Measure
   * Without A Ruler") truncate with an ellipsis instead of wrapping —
   * the column width is the truncation budget, not the label length. */
  display: block;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.menu-col-list-numbered .menu-link::before {
  content: counter(section-num, decimal-leading-zero);
  display: inline-block;
  margin-right: var(--space-3);
  font-size: 1em;
  font-weight: var(--font-light);
  letter-spacing: 0;
  color: currentColor;
  font-variant-numeric: tabular-nums;
}

.site-menu-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-6);
  padding-top: var(--space-4);
  color: var(--color-text-tertiary);
  font-size: 12px;
  line-height: 16px;
  font-weight: 300;
}

/* Audit toggles live in the menu footer (and the site footer). The
 * .grid-toggle primitive (defined further down) handles per-button
 * styling; this group just spaces them. */
.audit-toggles {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
}

/* Tools row inside the menu footer — bundles audit toggles with the
 * theme toggle so they share one cluster opposite the copyright. */
.site-menu-tools {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
}

/* Content stagger — each column lifts in after the overlay fades. */
.site-menu .menu-col,
.site-menu .site-menu-brand,
.site-menu .site-menu-close,
.site-menu .site-menu-footer {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 600ms var(--ease-smooth),
              transform 600ms var(--ease-smooth);
}

.site-menu[aria-hidden="false"] .site-menu-brand,
.site-menu[aria-hidden="false"] .site-menu-close {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 120ms;
}

.site-menu[aria-hidden="false"] .menu-col:nth-child(1) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 220ms;
}

.site-menu[aria-hidden="false"] .menu-col:nth-child(2) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 340ms;
}

.site-menu[aria-hidden="false"] .menu-col:nth-child(3) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 460ms;
}

.site-menu[aria-hidden="false"] .menu-col:nth-child(4) {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 580ms;
}

.site-menu[aria-hidden="false"] .site-menu-footer {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 680ms;
}

@media (prefers-reduced-motion: reduce) {
  .site-menu .menu-col,
  .site-menu .site-menu-brand,
  .site-menu .site-menu-close,
  .site-menu .site-menu-footer {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Hide the hamburger while menu is open — the close button
 * inside the menu takes over. */
body.menu-open .header .menu-toggle {
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms var(--ease-smooth);
}

/* ── Main ─────────────────────────────── */
/* Horizontal padding is the canonical page gutter. Every other
 * container (.site-menu, .footer, .layout-grid) mirrors this value
 * so content lines up edge-for-edge across them. */
.main {
  padding: var(--space-32) var(--space-12) var(--space-20);
}

/* ── Audience tabs ───────────────────────
 * Render at the same Display M / mono scale as the surrounding hero
 * text so the intro + prompt + tabs + response all read as one
 * continuous mono block. Tabs are tertiary by default and lift to
 * primary on hover/active — colour-only state change, no weight shift,
 * so the line doesn't reflow as the visitor moves across the options. */
.audience-tabs {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: 0;
  margin: 0 0 var(--space-8);
  padding: 0;
}

.audience-tab {
  font-family: inherit;
  font-size: var(--font-size-body-large);
  /* Body Large is 16/20, but these tabs are interactive controls with no
   * vertical padding, so the line box IS the hit target. 20px would fall
   * under the WCAG 2.5.8 (AA) 24px minimum target size — bump line-height
   * to 24px (--space-6, a clean 1.5× ratio that also satisfies 1.4.12). */
  line-height: var(--space-6);
  letter-spacing: var(--letter-spacing-body-large);
  font-weight: var(--font-regular);
  color: var(--color-text-disabled);
  padding: 0 0 0 2ch;
  background: transparent;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  text-align: left;
  position: relative;
  transition: color 320ms var(--ease-smooth);
}

.audience-tab::before {
  content: "•";
  position: absolute;
  left: 0;
}

.audience-tab > span {
  display: block;
}

.audience-tab:hover,
.audience-tab.is-active {
  color: var(--color-text-primary);
}

/* ── Hero ─────────────────────────────── */
.hero {
  margin-bottom: var(--space-48);
  /* Cap the hero to a readable measure rather than the full column.
   * The block is set in monospace, so 1ch ≈ one character — 66ch keeps
   * the long response line inside the ~45–75 character readability band
   * (Bringhurst's ideal is 66) instead of stretching the full width. */
  max-width: 66ch;
}

.hero-headline {
  font-size: var(--font-size-body-large);
  line-height: var(--line-height-body-large);
  letter-spacing: var(--letter-spacing-body-large);
  font-weight: var(--font-regular);
  color: var(--color-text-primary);
  margin-bottom: var(--space-8);
  font-variant-numeric: tabular-nums;
  white-space: pre-line;
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
  will-change: opacity, filter, transform, height;
  transition:
    opacity 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    filter 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    height 340ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* Only clip during the FLIP height animation so descenders aren't cut at rest */
.hero-headline[style*="height"] {
  overflow: hidden;
}

.hero-headline.is-fading {
  opacity: 0;
  filter: blur(6px);
  transform: translateY(4px);
}

/* Static intro — one empty line below before the prompt. --space-8
 * (32px) is the closest grid-aligned approximation to the Display S
 * line-height (29px), so the gap reads as roughly one blank line. */
.hero-intro {
  margin: 0 0 var(--space-8);
}

/* Prompt below the intro — same Display S / mono scale and primary
 * colour as the rest of the hero sentence. One empty line below
 * before the audience tabs, same spacing rule as the intro. */
.hero-prompt {
  font-size: var(--font-size-body-large);
  line-height: var(--line-height-body-large);
  letter-spacing: var(--letter-spacing-body-large);
  font-weight: var(--font-regular);
  margin: 0 0 var(--space-8);
  /* Share the hero-headline blur-fade so the salutation transitions in step
     with the swapping copy when switching audience tabs. */
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
  will-change: opacity, filter, transform;
  transition:
    opacity 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    filter 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 340ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.hero-prompt.is-fading {
  opacity: 0;
  filter: blur(6px);
  transform: translateY(4px);
}

/* Hero CTA — matches the surrounding mono block (Body Large) instead
 * of falling back to the small link-arrow scale. */
.hero .link-arrow {
  font-size: var(--font-size-body-large);
  line-height: var(--line-height-body-large);
  letter-spacing: var(--letter-spacing-body-large);
}

/* Extra blank line below the inline CTA so the sign-off reads as a
 * separate "paragraph" of the email, not the next line of the CTA. */
.hero-cta {
  margin-bottom: var(--space-8);
  /* Share the hero-headline blur-fade so the CTA label transitions in step
     with the response and sign-off when switching audience tabs. */
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
  will-change: opacity, filter, transform;
  transition:
    opacity 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    filter 340ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 340ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.hero-cta.is-fading {
  opacity: 0;
  filter: blur(6px);
  transform: translateY(4px);
}

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-semi-bold);
  color: var(--color-text-primary);
}

.link-arrow::after {
  content: '→';
  display: inline-block;
  transition: transform 320ms var(--ease-smooth);
}

.link-arrow:hover::after {
  transform: translateX(var(--btn-arrow-offset));
}

/* ── Scroll cue (shared) ──────────────────
 * Caption-size "Scroll" label above a 2px hairline that traces a bead from
 * top to bottom on a 2.4s loop. Used at the bottom of full-viewport heroes
 * to invite the user into the page.
 *
 * Position is owned by the parent (the homepage .hero positions it bottom-
 * center; case-study .cs-hero-overlay flex-aligns it bottom-center). This
 * component only owns its own type, line, and animation.
 *
 * Modifier:
 *   .scroll-cue--inverse  → over dark surface (case-study video hero).
 *                           Uses inverse text colour and waits longer to
 *                           appear so the hero video settles first.
 */
.scroll-cue {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: fit-content;
  gap: var(--space-2);
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
  text-decoration: none;
  pointer-events: auto;
  cursor: pointer;
  opacity: 0.9;
  transition: opacity 260ms var(--ease-smooth), color 260ms var(--ease-smooth);
  animation: scroll-cue-enter 900ms cubic-bezier(0.22, 1, 0.36, 1) 1200ms backwards;
}

.scroll-cue:hover,
.scroll-cue:focus-visible {
  opacity: 1;
  color: var(--color-text-primary);
}

/* Toggled by initScrollCueFade in main.js once the user scrolls past the
 * first ~24px. Mirrors the .cs-hero-overlay.is-hero-hidden behaviour on
 * case studies so both surfaces fade their cue at the same scroll point. */
.scroll-cue.is-hidden {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 320ms cubic-bezier(0.22, 1, 0.36, 1),
              visibility 0s linear 320ms;
}

.scroll-cue--inverse {
  color: var(--color-text-inverse);
  opacity: 0.7;
  animation-delay: 2000ms;
}

.scroll-cue--inverse:hover,
.scroll-cue--inverse:focus-visible {
  color: var(--color-text-inverse);
  opacity: 1;
}

@keyframes scroll-cue-enter {
  from { opacity: 0; }
}

.scroll-cue-line {
  display: block;
  width: 2px;
  height: var(--space-10);
  background-color: color-mix(in srgb, currentColor 35%, transparent);
  background-image: linear-gradient(
    180deg,
    transparent                                          40%,
    color-mix(in srgb, currentColor 25%, transparent)    46%,
    currentColor                                         50%,
    color-mix(in srgb, currentColor 25%, transparent)    54%,
    transparent                                          60%
  );
  background-size: 2px 250%;
  background-repeat: no-repeat;
  background-position: 0 100%;
  animation: scroll-cue-trace 2400ms cubic-bezier(0.45, 0, 0.55, 0.6) infinite;
}

@keyframes scroll-cue-trace {
  0%   { background-position: 0 100%; }
  100% { background-position: 0 0%; }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-cue-line {
    animation: none;
    background-position: 0 50%;
  }
  .scroll-cue {
    animation: none;
  }
}

/* Homepage hero owns the cue's placement: pinned to the bottom-centre of
 * the viewport so it reads as a "scroll down" affordance independent of
 * the hero's natural height. Case-study heroes place it via the
 * .cs-hero-overlay flex layout (overlay is itself absolute-bottom). */
.hero .scroll-cue {
  position: fixed;
  left: 50%;
  bottom: var(--space-12);
  transform: translateX(-50%);
  z-index: 5;
}

/* ── Section label (shared) ───────────── */
.section-label {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
  margin-bottom: var(--space-5);
}

.media-placeholder {
  width: 100%;
  height: 100%;
  background: var(--color-surface-tertiary);
  transition: transform var(--duration-luxury) var(--ease-smooth);
}

/* Media wrappers paint a tertiary-surface skeleton block immediately, so the
 * page grid lands as solid shapes before images/videos arrive. The media
 * element then paints over it. No animation — the skeleton is a quiet placeholder,
 * not a shimmer. */
.featured-media,
.case-media,
.work-media,
.past-media {
  background: var(--color-surface-tertiary);
}

/* Any <img> or <video> dropped into a media container fills it edge-to-edge
 * and crops via object-fit so aspect-ratio of the container is authoritative.
 * Shares the same transform transition as .media-placeholder so the hover
 * scale applies uniformly whether the slot holds a placeholder, image, or
 * video. */
.featured-media > img,
.featured-media > video,
.featured-media > canvas,
.case-media > img,
.case-media > video,
.case-media > canvas,
.work-media > img,
.work-media > video,
.work-media > canvas,
.past-media > img,
.past-media > video,
.past-media > canvas {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
  transition: transform var(--duration-luxury) var(--ease-smooth);
}

/* ── Card hover (shared) ───────────────── */
/* Applies to .featured-card, .case-card, .past-card — the three clickable
 * project surfaces. Uses motion + colour tokens only; no shadows / blur. */
.featured-card,
.case-card,
.past-card {
  cursor: pointer;
}

.case-meta,
.past-meta,
.featured-desc {
  transition: color 320ms var(--ease-smooth);
}

.featured-card:hover .media-placeholder,
.featured-card:hover .featured-media > img,
.featured-card:hover .featured-media > video,
.featured-card:hover .featured-media > canvas,
.case-card:hover .media-placeholder,
.case-card:hover .case-media > img,
.case-card:hover .case-media > video,
.case-card:hover .case-media > canvas,
.case-card:hover .case-media > .gms-claude-panel,
.case-card:hover .case-media > .ap-mark-loop,
.past-card:hover .media-placeholder,
.past-card:hover .past-media > img,
.past-card:hover .past-media > video,
.past-card:hover .past-media > canvas {
  transform: scale(1.02);
}

/* ── GMS Site rebuild card — Claude Code "side panel" hero ─────
 * Mimics the VS Code Claude Code panel: a stream of tool calls
 * and thoughts above a sticky status line. Driven by
 * js/gms-site-hero.js. Locked to a dark editor palette regardless
 * of site theme — this is a representational mockup of an IDE
 * panel, not site chrome, so it stays "in character" (same
 * precedent as signal-chain-hero.js which defines its own palette
 * inline). Local CSS variables hold the palette so any future
 * tweak happens in one place. */
.gms-claude-panel {
  --gms-bg:           #1e1e1e;  /* VS Code editor background */
  --gms-text:         #e4e4e4;  /* primary text */
  --gms-text-dim:     #a0a0a0;  /* result lines (secondary) */
  --gms-text-dimmest: #6e6e6e;  /* arg parens, thoughts (tertiary) */
  --gms-accent:       #d97757;  /* Claude orange — bullet, star */
  --gms-add:          #7cac63;  /* additions in diff (Claude green) */
  --gms-del:          #c25f5f;  /* deletions in diff (Claude red)   */

  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: var(--space-3) var(--space-4);
  gap: var(--space-2);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px;
  line-height: 1.55;
  color: var(--gms-text);
  background: var(--gms-bg);
  overflow: hidden;
  pointer-events: none;
  transition: transform var(--duration-luxury) var(--ease-smooth);
}

.gms-claude-stream {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  overflow: hidden;
}

.gms-claude-line {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  animation: gms-claude-line-in 380ms var(--ease-smooth) both;
}

.gms-claude-line--tool {
  color: var(--gms-text);
}

.gms-claude-bullet {
  color: var(--gms-accent);
  font-weight: var(--font-semi-bold);
  margin-right: 0.4em;
}

.gms-claude-arg {
  color: var(--gms-text-dimmest);
}

.gms-claude-line--result {
  color: var(--gms-text-dim);
  padding-left: 1em;
}

.gms-claude-line--think {
  color: var(--gms-text-dimmest);
  font-style: italic;
}

/* Diff lines under an Edit/Write tool call. The +/- prefix sits in
 * the indent so the code itself stays at a stable left edge. */
.gms-claude-line--add,
.gms-claude-line--del {
  padding-left: 2em;
  position: relative;
}

.gms-claude-line--add { color: var(--gms-add); }
.gms-claude-line--del { color: var(--gms-del); }

.gms-claude-line--add::before,
.gms-claude-line--del::before {
  position: absolute;
  left: 1em;
  opacity: 0.85;
}
.gms-claude-line--add::before { content: '+'; color: var(--gms-add); }
.gms-claude-line--del::before { content: '−'; color: var(--gms-del); }

@keyframes gms-claude-line-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.gms-claude-status {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  font-size: 12px;
  line-height: 1.4;
  color: var(--gms-text);
}

.gms-claude-status__meta {
  color: var(--gms-text-dimmest);
}

/* Hero variant — same panel, scaled up to fill a .cs-hero-full wrapper
 * on the GMS case-study page. Bumps typography and padding so the
 * IDE-panel metaphor reads at viewport scale instead of card scale. */
.gms-claude-panel--hero {
  padding: var(--space-10) var(--space-12);
  gap: var(--space-4);
  font-size: 14px;
  line-height: 1.6;
}

.gms-claude-panel--hero .gms-claude-stream {
  gap: 4px;
}

.gms-claude-panel--hero .gms-claude-status {
  font-size: 15px;
}

@media (min-width: 1280px) {
  .gms-claude-panel--hero {
    font-size: 15px;
  }
  .gms-claude-panel--hero .gms-claude-status {
    font-size: 16px;
  }
}

@media (max-width: 768px) {
  .gms-claude-panel--hero {
    padding: var(--space-6) var(--space-5);
    font-size: 12px;
  }
  .gms-claude-panel--hero .gms-claude-status {
    font-size: 13px;
  }
}

/* ── Artpilot case card — looping mark construction ───────────────
 * Card-scale reuse of the badge-construct kinetic specimen from the
 * Artpilot case study. The mark zooms in from a blurred pull-focus
 * pose, the grid/diagonals/circles snap into place around it, holds,
 * then reverses. Driven by js/artpilot-mark-loop.js toggling
 * data-bc-state on the panel. Background is the Artpilot brand red
 * in both themes — same precedent as the badge-construct slide. */
.ap-mark-loop {
  --ap-mark: #DB3334;
  --ap-guide: var(--color-text-tertiary);
  /* Smooth in-out for synchronized transitions; the soft ease-out
   * is reserved for arrivals (outline, fill, zoom). */
  --ap-ease: cubic-bezier(0.45, 0, 0.55, 1);
  --ap-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--color-surface-tertiary);
  display: grid;
  place-items: center;
  overflow: hidden;
  pointer-events: none;
  transition: transform var(--duration-luxury) var(--ease-smooth);
}

/* Continuous very-slow breathe on the whole construction. Imperceptible
 * frame-to-frame — drives quiet engagement across the loop. Runs
 * independent of the data-bc-state choreography because it lives on the
 * outer SVG, not the inner master/badge groups. */
.ap-mark-loop-svg {
  display: block;
  width: 58%;
  max-width: 240px;
  height: auto;
  transform-origin: 50% 50%;
  animation: ap-mark-breathe 16s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
  will-change: transform;
}

@keyframes ap-mark-breathe {
  from { transform: scale(1); }
  to   { transform: scale(1.035); }
}

/* Cycle-synced overall zoom. Sits on top of the breathe (which lives
 * on the SVG) and the final mark zoom (which lives on the badge group)
 * — so the whole construction slowly scales up across each cycle, the
 * mark lifts a touch at the end, and the breathe pulses through it
 * all. Three subtle motions compounding into one quiet pull-in. */
.ap-mark-loop-master {
  transform-box: view-box;
  transform-origin: 50% 50%;
  transform: scale(1);
  /* Exit transition (state → "") — gentle return to rest. */
  transition: transform 1400ms var(--ap-ease);
}

.ap-mark-loop[data-bc-state="badge"] .ap-mark-loop-master {
  transform: scale(1.04);
  transition: transform 2400ms var(--ap-ease);
}

.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-master {
  transform: scale(1.085);
  transition: transform 2200ms var(--ap-ease);
}

/* ── Guide lines ─────────────────────────────────────────────────────
 * Choreography across three states:
 *   ""       → everything hidden
 *   "badge"  → guides cascade in (diagonals → horizontals → verticals
 *              & circles), then the mark outline appears
 *   "filled" → all guides fade out together as the mark fills in solid
 *
 * Default transition is the exit transition (used when state returns
 * to ""). Each state below overrides it to fit that phase. */
.ap-mark-loop-grid line,
.ap-mark-loop-diag line,
.ap-mark-loop-circ circle {
  fill: none;
  stroke: var(--ap-guide);
  stroke-opacity: 0.55;
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  opacity: 0;
  transition: opacity 560ms var(--ap-ease);
}

.ap-mark-loop-grid line { stroke-dasharray: 1 5; }
.ap-mark-loop-diag line { stroke-dasharray: 1 5; }
.ap-mark-loop-circ circle { stroke-dasharray: 1 4; }

/* Entry stagger — diagonals first, then horizontals (top to bottom),
 * then verticals overlapping with circles ("verticals and radius"). */
.ap-mark-loop-diag line:nth-child(1)   { --d: 0ms; }
.ap-mark-loop-diag line:nth-child(2)   { --d: 80ms; }
.ap-mark-loop-grid-h line:nth-child(1) { --d: 280ms; }
.ap-mark-loop-grid-h line:nth-child(2) { --d: 340ms; }
.ap-mark-loop-grid-h line:nth-child(3) { --d: 400ms; }
.ap-mark-loop-grid-h line:nth-child(4) { --d: 460ms; }
.ap-mark-loop-grid-h line:nth-child(5) { --d: 520ms; }
.ap-mark-loop-grid-h line:nth-child(6) { --d: 580ms; }
.ap-mark-loop-grid-v line:nth-child(1) { --d: 680ms; }
.ap-mark-loop-grid-v line:nth-child(2) { --d: 740ms; }
.ap-mark-loop-grid-v line:nth-child(3) { --d: 800ms; }
.ap-mark-loop-grid-v line:nth-child(4) { --d: 860ms; }
.ap-mark-loop-grid-v line:nth-child(5) { --d: 920ms; }
.ap-mark-loop-circ circle:nth-child(1) { --d: 720ms; }
.ap-mark-loop-circ circle:nth-child(2) { --d: 800ms; }
.ap-mark-loop-circ circle:nth-child(3) { --d: 880ms; }

/* Entry to "badge" — staggered fade-in per line. */
.ap-mark-loop[data-bc-state="badge"] .ap-mark-loop-grid line,
.ap-mark-loop[data-bc-state="badge"] .ap-mark-loop-diag line,
.ap-mark-loop[data-bc-state="badge"] .ap-mark-loop-circ circle {
  opacity: 1;
  transition: opacity 520ms var(--ap-ease) var(--d, 0ms);
}

/* Transition to "filled" — every guide fades out together. */
.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-grid line,
.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-diag line,
.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-circ circle {
  opacity: 0;
  transition: opacity 720ms var(--ap-ease);
}

/* ── Mark — outline then fill, then a small zoom before exit ────────
 * Outline starts as a light grey inside-only border in the same key as
 * the construction dashes (clip-path on the path keeps the stroke
 * inside the silhouette). Once the guides fade out, the stroke colour
 * transitions to brand red and the fill comes in solid. Mark sits at
 * scale(1) through the whole construction so the outline lines up with
 * the grid; only once the guides are gone does it zoom in a touch. */
.ap-mark-loop-badge {
  transform-box: view-box;
  transform-origin: 50% 50%;
  transform: scale(1);
  transition: transform 720ms var(--ap-ease);
}

.ap-mark-loop-frame,
.ap-mark-loop-wing {
  fill: var(--ap-mark);
  fill-opacity: 0;
  stroke: var(--ap-guide);
  stroke-width: 3;
  stroke-opacity: 0;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  /* Default/exit transition — used when returning to state "" */
  transition:
    fill-opacity   560ms var(--ap-ease),
    stroke-opacity 560ms var(--ap-ease),
    stroke         560ms var(--ap-ease);
}

/* "badge" — light grey inside-border appears once the guides have
 * settled. Same stroke colour as the dashes for visual continuity. */
.ap-mark-loop[data-bc-state="badge"] .ap-mark-loop-frame,
.ap-mark-loop[data-bc-state="badge"] .ap-mark-loop-wing {
  stroke: var(--ap-guide);
  stroke-opacity: 0.55;
  fill-opacity: 0;
  transition:
    stroke-opacity 720ms var(--ap-ease-out) 1440ms,
    stroke         720ms var(--ap-ease-out) 1440ms,
    fill-opacity   560ms var(--ap-ease);
}

/* "filled" — guides fade out, the inside-border warms from grey to
 * red and the fill comes in solid behind it. */
.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-frame,
.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-wing {
  fill-opacity: 1;
  stroke: var(--ap-mark);
  stroke-opacity: 1;
  transition:
    fill-opacity   760ms var(--ap-ease-out),
    stroke         600ms var(--ap-ease),
    stroke-opacity 600ms var(--ap-ease);
}

/* Barely-perceptible final lift — most of the cycle's zoom now lives
 * on the master group above; this just nudges the mark a hair after
 * the fill settles so the moment doesn't go entirely still. */
.ap-mark-loop[data-bc-state="filled"] .ap-mark-loop-badge {
  transform: scale(1.008);
  transition: transform 880ms var(--ap-ease-out) 1200ms;
}

/* Reduced motion — park on the final state, no looping construction
 * and no continuous breathe. */
@media (prefers-reduced-motion: reduce) {
  .ap-mark-loop-svg {
    animation: none;
  }
  .ap-mark-loop-master,
  .ap-mark-loop-badge,
  .ap-mark-loop-frame,
  .ap-mark-loop-wing,
  .ap-mark-loop-grid line,
  .ap-mark-loop-diag line,
  .ap-mark-loop-circ circle {
    transition: none;
  }
}

.case-card:hover .case-meta,
.past-card:hover .past-meta {
  color: var(--color-text-secondary);
}

.featured-card:focus-visible,
.case-card:focus-visible,
.past-card:focus-visible {
  outline: none;
  border-radius: var(--radius-sharp);
  box-shadow: 0 0 0 2px var(--color-surface-page),
              0 0 0 4px var(--btn-focus-ring);
}

/* ── Featured Project ──────────────────── */
.featured {
  margin-top: var(--space-20);
  margin-bottom: var(--space-24);
}

.featured-card {
  display: block;
}

/* Full-bleed: break out of .main's horizontal padding to the viewport edges.
 * `overflow-x: hidden` on <body> contains any scrollbar-width slop. */
.featured-media {
  margin: 0 calc(50% - 50vw) var(--space-5);
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.featured-meta {
  display: flex;
  flex-direction: column;
  max-width: 720px;
}

.featured-title {
  font-size: var(--font-size-h3);
  line-height: var(--line-height-h3);
  letter-spacing: var(--letter-spacing-h3);
  font-weight: var(--font-semi-bold);
  color: var(--color-text-heading);
  margin-bottom: var(--space-1);
}

.featured-desc {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
  margin-bottom: var(--space-3);
}

/* Homepage hero + featured caption use a quieter Light-weight link-arrow per
 * Figma. The bold .link-arrow treatment is kept for primary inline CTAs
 * elsewhere on the site. */
.hero .link-arrow,
.featured-meta .link-arrow {
  font-weight: var(--font-light);
  color: var(--color-text-primary);
}

/* ── Case Studies Grid ─────────────────── */
.case-studies { margin-bottom: var(--space-24); }

/* All work / case layouts share the page's 12-column grid so cards
 * snap to the same column edges as the overlay. Column gap matches
 * the overlay (var(--space-4)); row gap keeps an editorial breath. */
.case-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--space-4);
  row-gap: var(--space-6);
}

.case-grid > * { grid-column: span 6; }

.case-card { display: block; }

.case-media {
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-sharp);
  overflow: hidden;
  margin-bottom: var(--space-4);
}

.case-title {
  font-size: var(--font-size-h3);
  line-height: var(--line-height-h3);
  letter-spacing: var(--letter-spacing-h3);
  font-weight: var(--font-semi-bold);
  color: var(--color-text-heading);
  margin-bottom: 0;
}

.case-meta {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
}

/* ── Selected work (asymmetric, 7-row mixed grid) ───────────────
 * Case studies intermixed with past projects in a single section.
 * Each row pins its card(s) to specific 12-col positions to match
 * the Figma layout. Aspect-ratio is set per-card via the
 * .work-card--{square|landscape|portrait} modifiers. */
.work-feature {
  margin-bottom: var(--space-24);
  padding-top: var(--space-8);
}

.work-row {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--space-5);
  margin-bottom: var(--space-32);
}

.work-row:last-child { margin-bottom: 0; }

/* Per-row card placements — each mirrors a slot in the Figma frame. */
.work-row--solo-right-lg > .work-card { grid-column: 5 / -1; }
.work-row--solo-center-lg > .work-card { grid-column: 3 / span 8; }
.work-row--solo-center-sm > .work-card { grid-column: 5 / span 4; }
.work-row--solo-left-lg > .work-card { grid-column: 1 / span 8; }
.work-row--solo-left-tall > .work-card { grid-column: 1 / span 6; }
.work-row--solo-right-sm > .work-card { grid-column: 9 / -1; }

/* The one paired row: large square anchors left, small landscape
 * sits flush-right and bottom-aligns with its tall neighbour so
 * the captions snap to the same baseline. */
.work-row--pair-asym { align-items: end; }
.work-row--pair-asym > .work-card:nth-child(1) { grid-column: 1 / span 8; }
.work-row--pair-asym > .work-card:nth-child(2) { grid-column: 9 / -1; }

.work-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}

.work-media {
  width: 100%;
  border-radius: var(--radius-sharp);
  overflow: hidden;
}

.work-card--square    .work-media { aspect-ratio: 1 / 1; }
.work-card--landscape .work-media { aspect-ratio: 1.16 / 1; }
.work-card--portrait  .work-media { aspect-ratio: 3 / 4; }
/* Natural-ratio variants — used when a card should mirror the
 * source media's intrinsic proportions instead of a generic crop. */
.work-card--ar-16x9   .work-media { aspect-ratio: 16 / 9; }
.work-card--ar-3x2    .work-media { aspect-ratio: 3 / 2; }
.work-card--ar-4x5    .work-media { aspect-ratio: 4 / 5; }

.work-meta {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.work-title {
  font-size: var(--font-size-h3);
  line-height: var(--line-height-h3);
  letter-spacing: var(--letter-spacing-h3);
  font-weight: var(--font-semi-bold);
  color: var(--color-text-heading);
}

.work-desc {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
  transition: color 320ms var(--ease-smooth);
}

/* Stack everything full-width at tablet and below until the
 * asymmetric layout earns its keep on a wider canvas. */
@media (max-width: 1024px) {
  .work-row > .work-card { grid-column: 1 / -1; }
  .work-row--pair-asym { align-items: stretch; }
  .work-row--pair-asym > .work-card:nth-child(1),
  .work-row--pair-asym > .work-card:nth-child(2) { grid-column: 1 / -1; }
  .work-row { margin-bottom: var(--space-16); }
}

.work-card:hover .media-placeholder,
.work-card:hover .work-media > img,
.work-card:hover .work-media > video { transform: scale(1.02); }
.work-card:hover .work-desc { color: var(--color-text-primary); }

.work-card:focus-visible {
  outline: none;
  border-radius: var(--radius-sharp);
  box-shadow: 0 0 0 2px var(--color-surface-page),
              0 0 0 4px var(--btn-focus-ring);
}

/* ── Scroll reveal ─────────────────────────────────────────
 * Base: section/container fade + translate.
 * Selected work cards layer a cinematic clip-path wipe on the
 * media plus a staggered lift on the title and description so
 * the content assembles in sequence as you scroll. */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 1100ms var(--ease-smooth),
    transform 1100ms var(--ease-smooth);
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .reveal,
  .work-card.reveal .work-media,
  .work-card.reveal .work-title,
  .work-card.reveal .work-desc {
    opacity: 1;
    transform: none;
    clip-path: none;
    transition: none;
  }
}

/* ── Past Projects carousel ────────────── */
.past-projects {
  margin-bottom: var(--space-24);
}

/* Track bleeds to BOTH edges of the viewport so cards never clip at the
 * page padding. scroll-padding keeps snap points aligned with the content
 * column so the first card settles flush with the section label above it.
 * `proximity` snap lets flicks glide naturally instead of snapping hard. */
.past-track {
  display: flex;
  gap: var(--space-5);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  scroll-padding-inline: var(--space-8);
  margin-inline: calc(var(--space-8) * -1);
  padding-inline: var(--space-8);
  padding-bottom: var(--space-2);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

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

.past-card {
  flex: 0 0 400px;
  scroll-snap-align: start;
  display: block;
}

.past-media {
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: var(--radius-sharp);
  overflow: hidden;
  margin-bottom: var(--space-4);
}

/* Unified with .case-title so card titles read consistently across sections */
.past-title {
  font-size: var(--font-size-h3);
  line-height: var(--line-height-h3);
  letter-spacing: var(--letter-spacing-h3);
  font-weight: var(--font-semi-bold);
  color: var(--color-text-heading);
  margin-bottom: var(--space-1);
}

.past-meta {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
}

/* ── Contact ──────────────────────────── */
.contact {
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
  padding-top: var(--space-20);
  padding-bottom: var(--space-20);
  margin-bottom: var(--space-24);
}

.contact-intro {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
}

.contact-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.contact-required-legend {
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  font-weight: 300;
  color: var(--color-text-tertiary);
}

.contact-required-legend .field-required { color: var(--color-text-error); }

/* Honeypot — visually offscreen, ignored by AT, kept in DOM so bots fill it. */
.contact-honeypot {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.field-required {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  color: var(--color-text-error);
  margin-left: var(--space-1);
}

/* Inside the legend the asterisk drops back to the legend's own
 * Caption size so the line reads as one tonal block. */
.contact-required-legend .field-required {
  font-size: inherit;
  line-height: inherit;
  letter-spacing: inherit;
}

.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--color-border-error);
}

.field input[aria-invalid="true"]:focus,
.field textarea[aria-invalid="true"]:focus {
  border-color: var(--color-border-error);
  box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);
}

.field-error {
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  font-weight: 300;
  color: var(--color-text-error);
  min-height: 16px;
}

.field-error:empty { min-height: 0; }

.contact-form-error {
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  font-weight: 300;
  color: var(--color-text-error);
}

.contact-success {
  border: 1px solid var(--color-border-default);
  padding: var(--space-8);
  border-radius: var(--radius-sharp);
}

.contact-success:focus { outline: none; }

.contact-success-heading {
  font-size: var(--font-size-h3);
  line-height: var(--line-height-h3);
  letter-spacing: var(--letter-spacing-h3);
  font-weight: 600;
  color: var(--color-text-heading);
  margin-bottom: var(--space-2);
}

.contact-success-body {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  font-weight: 450;
  color: var(--color-text-secondary);
}

.contact-success-body a { color: var(--color-text-primary); }

.contact-heading {
  font-size: var(--font-size-display-l);
  line-height: var(--line-height-display-l);
  letter-spacing: var(--letter-spacing-display-l);
  font-weight: var(--font-regular);
  color: var(--color-text-heading);
}

.contact-lede {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  font-weight: var(--font-regular);
  color: var(--color-text-secondary);
}

.contact-direct {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  font-weight: var(--font-regular);
}

.contact-direct a { color: var(--color-text-primary); overflow-wrap: anywhere; }

/* ── Page variant ─────────────────────
 * Used on /contact.html where the page already carries the title and
 * lede. Drops the left-rail intro entirely; the section becomes a plain
 * block that participates in .cs-page--centered's text band (see
 * case-study.css — the .cs-page--centered > .contact--page selector
 * adds it to the same 6-col/4-col/8-col band math the rest of the
 * article uses, so the form lines up with the lede above it). */
.contact--page {
  display: block;
  padding-top: var(--space-10);
}

.contact--page .contact-form-wrap {
  grid-column: auto;
}

.contact-direct--inline {
  margin-bottom: var(--space-8);
  color: var(--color-text-secondary);
}


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

.field-label {
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  font-weight: var(--font-light);
  color: var(--color-text-primary);
}

.field-hint {
  color: var(--color-text-tertiary);
  font-weight: 300;
}

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

.field input,
.field textarea {
  width: 100%;
  font-family: inherit;
  font-size: var(--font-size-input);
  line-height: var(--line-height-input);
  letter-spacing: var(--letter-spacing-input);
  font-weight: var(--font-regular);
  color: var(--color-text-primary);
  background: var(--color-surface-page);
  border: 0.5px solid var(--color-border-input);
  border-radius: var(--radius-sharp);
  padding: var(--space-3);
  height: auto;
  transition: border-color 260ms var(--ease-smooth),
              box-shadow 260ms var(--ease-smooth);
}

.field textarea {
  height: 144px;
  resize: vertical;
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--color-text-tertiary);
}

.field input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--color-border-focus);
  box-shadow: 0 0 0 3px rgba(0, 154, 254, 0.15);
}

.form-actions {
  display: flex;
}

.form-actions > .btn {
  flex: 1 0 0;
}

/* ── Theme toggle ───────────────────────
 * Sun / moon icon button. Both icons are rendered in the markup;
 * we cross-fade the one matching the current theme via aria-pressed
 * so the swap reads as a single icon morphing rather than a label
 * change. Lives in the footer-tools row and the menu's tools row. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 24px;
  height: 24px;
  color: var(--color-text-tertiary);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  transition: color 320ms var(--ease-smooth);
}

.theme-toggle:hover { color: var(--color-text-heading); }

.theme-toggle-icon {
  position: absolute;
  inset: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: opacity 320ms var(--ease-smooth),
              transform 320ms var(--ease-smooth);
}

/* Light mode (aria-pressed=false): show the sun (current state).
 * Dark mode  (aria-pressed=true):  show the moon. The hidden icon
 * shrinks slightly so the morph reads as a swap, not a fade. */
.theme-toggle .icon-sun { opacity: 1; transform: scale(1); }
.theme-toggle .icon-moon { opacity: 0; transform: scale(0.85); }
.theme-toggle[aria-pressed="true"] .icon-sun { opacity: 0; transform: scale(0.85); }
.theme-toggle[aria-pressed="true"] .icon-moon { opacity: 1; transform: scale(1); }

.theme-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-surface-page),
              0 0 0 4px var(--btn-focus-ring);
  border-radius: var(--radius-pill);
}

/* ── Buttons ──────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: var(--btn-height-md);
  padding: var(--btn-pad-md);
  border-radius: var(--btn-radius);
  font-family: inherit;
  font-size: var(--btn-font-md);
  font-weight: var(--btn-weight);
  letter-spacing: var(--btn-tracking);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--btn-transition),
              color var(--btn-transition),
              border-color var(--btn-transition);
}

.btn .btn-icon {
  flex-shrink: 0;
  display: block;
}

.btn-primary {
  background: var(--btn-p-bg);
  color: var(--btn-p-text);
  border-color: var(--btn-p-bg);
}

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

.btn-ghost {
  background: transparent;
  color: var(--color-text-primary);
  border-color: var(--btn-g-border);
  text-decoration: none;
}

.btn-ghost:hover {
  background: var(--btn-g-bg-hover);
  color: var(--btn-g-text-hover);
  border-color: var(--btn-g-bg-hover);
}

.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-surface-page), 0 0 0 4px var(--btn-focus-ring);
}

/* ── Footer ───────────────────────────────
 * Editorial footer. Mirrors the fullscreen menu's vocabulary —
 * three columns on the page's 12-col grid (4 / 5 / 3), with small
 * tertiary labels under hairlines and large 28px semi-bold links
 * sitting at tertiary tone. The wordmark + status form a quiet
 * masthead row up top; copyright + tools sit in a small bottom
 * row separated by a hairline. Colours come from the page tokens
 * so the footer themes automatically. */
.footer {
  background: var(--color-surface-page);
  color: var(--color-text-primary);
  padding: var(--space-32) var(--space-12) var(--space-6);
  border-top: 1px solid var(--color-border-default);
}

.footer-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-20);
}

.footer-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-6);
}

/* Brand masthead. Matches the nav's semi-bold treatment, scaled up to
 * Display M so the footer reads as the larger sibling of the nav brand. */
.footer-wordmark {
  font-size: var(--font-size-display-m);
  line-height: var(--line-height-display-m);
  letter-spacing: var(--letter-spacing-display-m);
  font-weight: var(--font-semi-bold);
  color: var(--color-text-primary);
  transition: opacity 320ms var(--ease-smooth);
}

.footer-wordmark:hover { opacity: 0.6; }

.footer-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--space-4);
  row-gap: var(--space-10);
}

/* Four columns mirroring the fullscreen menu's layout exactly: Selected
 * work owns the first third, then Photography and art · On this page ·
 * Connect share the remaining two-thirds (3 / 3 / 2). */
.footer-grid > .footer-col:nth-child(1) { grid-column: 1 / span 4; }
.footer-grid > .footer-col:nth-child(2) { grid-column: 5 / span 3; }
.footer-grid > .footer-col:nth-child(3) { grid-column: 8 / span 3; }
.footer-grid > .footer-col:nth-child(4) { grid-column: 11 / span 2; }

.footer-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

/* Mirrors .menu-col-label so the footer reads as the same family
 * of editorial chrome as the fullscreen takeover. */
.footer-col-label {
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--color-border-subtle);
}

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

/* Tonal hover, like .menu-link: at rest the links sit at tertiary,
 * lifting to heading on hover. No underlines — the column already
 * provides the visual frame. Body size is the secondary default; the
 * Selected work column steps up via the hierarchy rules below. */
.footer-link {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  font-weight: var(--font-regular);
  color: var(--color-text-tertiary);
  display: inline-block;
  overflow-wrap: anywhere;
  transition: color 320ms var(--ease-smooth);
}

.footer-link:hover {
  color: var(--color-text-heading);
}

/* Footer visual hierarchy — mirrors the fullscreen menu so the two read
 * as a matched pair. The Selected work column (col 1) is the primary
 * index at Display S (23px); Photography and art + Connect sit at the
 * body base (14px). "All projects" is the catch-all at the foot of col 1,
 * so it drops back to body, is set off from the project titles above it,
 * and carries the same trailing arrow (via ::after so the digit-scramble,
 * which rewrites textContent, can't strip it). */
.footer-grid > .footer-col:nth-child(1) .footer-link {
  font-size: var(--font-size-display-s);
  line-height: var(--line-height-display-s);
  letter-spacing: var(--letter-spacing-display-s);
}
.footer-grid > .footer-col:nth-child(1) .footer-col-list > li:last-child .footer-link {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
}
.footer-grid > .footer-col:nth-child(1) .footer-col-list > li:last-child {
  margin-top: var(--space-2);
}
.footer-grid > .footer-col:nth-child(1) .footer-col-list > li:last-child .footer-link::after {
  content: "→";
  margin-left: var(--space-2);
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: var(--space-6);
  border-top: 1px solid var(--color-border-subtle);
  font-size: var(--font-size-small);
  line-height: var(--line-height-small);
  letter-spacing: var(--letter-spacing-small);
  font-weight: var(--font-light);
  color: var(--color-text-tertiary);
}

.footer-copy { color: var(--color-text-tertiary); }

.footer-top-link {
  color: var(--color-text-tertiary);
  transition: color 320ms var(--ease-smooth);
}

.footer-top-link:hover { color: var(--color-text-heading); }

.footer-tools {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
}

/* Audit toggles now live in the menu footer (.audit-toggles); the
 * .grid-toggle primitive itself owns the tertiary text colour and
 * hover-to-heading lift, so no per-host override is needed. */

/* ── Case-study editorial footer ────────────
 * Light-background variant used on all case study pages.
 * Injected by footer.js, styled here as a shared component. */
.cs-footer {
  margin-top: var(--space-32);
  padding-top: var(--space-16);
  border-top: 1px solid var(--color-border-default);
}

.cs-footer-thanks {
  margin-bottom: var(--space-20);
}

.cs-footer-thanks-text {
  font-size: var(--font-size-display-m);
  line-height: var(--line-height-display-m);
  letter-spacing: var(--letter-spacing-display-m);
  color: var(--color-text-heading);
  font-weight: var(--font-book);
  margin-bottom: var(--space-8);
}

.cs-footer-nav-grid {
  display: flex;
  gap: var(--space-20);
  margin-bottom: var(--space-16);
  padding-top: var(--space-10);
  border-top: 1px solid var(--color-border-default);
}

.cs-footer-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 180px;
}

/* Overline label — matches .cs-category / .cs-toc-label pattern */
.cs-footer-col-label {
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: 0.06em;
  color: var(--color-text-tertiary);
  font-weight: var(--font-light);
}

.cs-footer-col-links {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.cs-footer-col-links a {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  color: var(--color-text-primary);
  font-weight: var(--font-book);
  text-decoration: none;
  transition: opacity 320ms var(--ease-smooth);
}

.cs-footer-col-links a:hover {
  opacity: 0.5;
}

.cs-footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-6);
  padding-top: var(--space-8);
  border-top: 1px solid var(--color-border-default);
}

/* Mirrors .footer-tools and .site-menu-tools — bundles the audit
 * toggles next to the theme button so all three live as a single
 * cluster opposite the copyright. */
.cs-footer-tools {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
}

.cs-footer-copy {
  font-size: var(--font-size-caption);
  line-height: var(--line-height-caption);
  letter-spacing: var(--letter-spacing-caption);
  color: var(--color-text-tertiary);
  font-weight: var(--font-light);
  margin: 0;
}

@media (max-width: 768px) {
  .cs-footer-thanks-text {
    font-size: var(--font-size-h1);
    line-height: var(--line-height-h1);
    letter-spacing: var(--letter-spacing-h1);
  }

  .cs-footer-nav-grid {
    flex-direction: column;
    gap: var(--space-12);
  }
}

/* ── TEMP: Audit toggle primitive ───────────
 * Used by the menu's "Layout" and "Type" buttons. Each combined toggle
 * flips a pair of body classes:
 *   `L` / Layout — show-grid + show-spacing
 *   `T` / Type   — show-type + show-baseline
 * Remove before shipping. */
.grid-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-text-tertiary);
  opacity: 0.7;
  font-family: inherit;
  font-size: 12px;
  line-height: 16px;
  font-weight: 300;
  letter-spacing: 0;
  padding: 0;
  transition: opacity 320ms var(--ease-smooth),
              color 320ms var(--ease-smooth);
}

.grid-toggle:hover { color: var(--color-text-heading); opacity: 1; }

.grid-toggle-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  border: 1px solid currentColor;
  display: inline-block;
  background: transparent;
  transition: background 320ms var(--ease-smooth);
}

.grid-toggle[aria-pressed="true"] { opacity: 1; }
.grid-toggle[aria-pressed="true"] .grid-toggle-dot { background: currentColor; }

/* Sticky "Close audits" pill. Hidden by default; JS toggles .is-visible
 * whenever any of the four audit body classes (show-grid, show-spacing,
 * show-type, show-baseline) is present. Sits above every audit overlay
 * so it remains clickable while audits are painting. */
/* Bottom-right placement keeps the close clear of the menu's own
 * close button (top-right) when both happen to be visible. */
.audit-close {
  position: fixed;
  bottom: var(--space-4);
  right: var(--space-4);
  z-index: calc(var(--z-modal) + 100);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface-inverse);
  color: var(--color-text-inverse);
  border: 1px solid var(--color-surface-inverse);
  border-radius: var(--radius-sharp);
  font-family: inherit;
  font-size: 12px;
  line-height: 16px;
  font-weight: 300;
  letter-spacing: 0;
  cursor: pointer;
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity 220ms var(--ease-smooth),
              transform 220ms var(--ease-smooth);
}

.audit-close::before {
  content: "×";
  font-size: 14px;
  line-height: 1;
}

.audit-close.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.audit-close:hover { opacity: 0.9; }

.audit-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-surface-page),
              0 0 0 4px var(--color-border-focus);
}

/* Audit overlay. The fixed wrapper covers the viewport; alignment
 * comes from the inner cols container, which is positioned to the
 * exact rect of the active page container (.main / .cs-page) via
 * vars written by initLayoutGridGeometry(). No scrollbar math, no
 * max-width duplication — the overlay literally inherits the
 * container's geometry. */


/* The wrapper is always present and inert; visibility is gated per-overlay
 * so Columns and Baseline can be toggled independently. */
.layout-grid-cols,
.layout-grid-baseline {
  opacity: 0;
  transition: opacity 320ms var(--ease-smooth);
}

body.show-grid .layout-grid-cols { opacity: 1; }
body.show-baseline .layout-grid-baseline { opacity: 1; }

/* Geometry mirrors the active page container (.main / .cs-page) via
 * vars set by initLayoutGridGeometry(). Left + width are the container's
 * exact rendered rect, so the overlay can never drift from content. */
.layout-grid-cols {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--page-grid-left, 0);
  width: var(--page-grid-width, 100%);
  padding-left: var(--page-grid-pl, var(--space-12));
  padding-right: var(--page-grid-pr, var(--space-12));
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--space-4);
}

.layout-grid-cols > span {
  display: block;
  background: rgba(0, 154, 254, 0.08);
  border-left: 1px dashed rgba(0, 154, 254, 0.4);
  border-right: 1px dashed rgba(0, 154, 254, 0.4);
  height: 100%;
}

/* Baseline is mapped to the page container, not the viewport. As the
 * page scrolls, --page-grid-top goes negative and the overlay translates
 * with it, so each rule stays locked to a fixed y-coordinate of the
 * document. Width matches the container too, so lines never paint into
 * the gutter. */
/* 4px baseline. With body line-height bumped to 28 and h3 to 32, every
 * line-height in the type scale is now a multiple of 4, so the rules
 * actually correlate with text rhythm. Each fourth rule (16px) is drawn
 * heavier as a major tick so the eye can navigate without counting. */
.layout-grid-baseline {
  position: absolute;
  top: var(--page-grid-top, 0);
  left: var(--page-grid-left, 0);
  width: var(--page-grid-width, 100%);
  height: var(--page-grid-height, 100vh);
  background-image:
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 15px,
      rgba(192, 57, 43, 0.28) 15px,
      rgba(192, 57, 43, 0.28) 16px
    ),
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 3px,
      rgba(192, 57, 43, 0.12) 3px,
      rgba(192, 57, 43, 0.12) 4px
    );
}

/* Dark-mode overlays — brighter dashes so they read on black. */
:root[data-theme="dark"] .layout-grid-cols > span {
  background: rgba(79, 184, 255, 0.08);
  border-left-color: rgba(79, 184, 255, 0.45);
  border-right-color: rgba(79, 184, 255, 0.45);
}

:root[data-theme="dark"] .layout-grid-baseline {
  background-image:
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 15px,
      rgba(239, 133, 119, 0.32) 15px,
      rgba(239, 133, 119, 0.32) 16px
    ),
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 3px,
      rgba(239, 133, 119, 0.14) 3px,
      rgba(239, 133, 119, 0.14) 4px
    );
}

/* Padding is now picked up live from the active container, so no
 * per-breakpoint padding overrides are needed. The only responsive
 * concern left is column count: collapse 12→6 at the mobile break. */
@media (max-width: 768px) {
  .layout-grid-cols {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
  .layout-grid-cols > span:nth-child(n + 7) {
    display: none;
  }
}

/* ── Spacing audit overlay ──────────────────
 * Toggle from the footer or `S` key. Paints a labeled band in every
 * vertical gap between sibling blocks within audited containers.
 * Viewport-fixed; JS recomputes positions in viewport coordinates on
 * every scroll/resize tick, so bands track elements pixel-perfectly
 * the way devtools' inspector does. */
/* Spacing bands sit above the menu (z-modal=200) so the audit stays
 * visible when the menu is open and JS re-targets bands to menu content.
 * The wireframes, by contrast, drop below the menu — they only ever mask
 * page images and the menu's solid surface should hide them. */
.spacing-overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: calc(var(--z-modal) + 50);
  opacity: 0;
  transition: opacity 200ms var(--ease-smooth);
}

body.show-spacing .spacing-overlay { opacity: 1; }

/* Bucket palette — each band reads `--band` for its color, defined
 * per bucket below. Magnitude buckets help distinguish paragraph
 * rhythm (rhythm) from section breaks (section) at a glance. The
 * off-grid bucket overrides regardless of size. */
.spacing-band {
  --band: 0, 154, 254;            /* fallback: accent blue */
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  background: rgba(var(--band), 0.1);
  border-top: 1px dashed rgba(var(--band), 0.6);
  border-bottom: 1px dashed rgba(var(--band), 0.6);
  box-sizing: border-box;
  padding-right: var(--space-2);
}

/* Tight: 1–12px — intra-component / leading-level rhythm. Teal. */
.spacing-band.is-tight    { --band: 38, 166, 154; }
/* Rhythm: 16–32px — paragraph / inline-block rhythm. Blue. */
.spacing-band.is-rhythm   { --band: 0, 154, 254; }
/* Section: 40–80px — between sections / major content blocks. Violet. */
.spacing-band.is-section  { --band: 138, 99, 210; }
/* Macro: 96px+ — page-level / chapter breaks. Magenta. */
.spacing-band.is-macro    { --band: 217, 70, 160; }
/* Off-grid: any size that isn't a multiple of 4. Amber, regardless
 * of magnitude — design-system breach. */
.spacing-band.is-offgrid  { --band: 232, 144, 47; }
.spacing-band.is-offgrid {
  background: rgba(var(--band), 0.14);
  border-top-style: solid;
  border-bottom-style: solid;
}

.spacing-band-label {
  font-family: 'Suisse Int\'l Book', Inter, sans-serif;
  font-size: 11px;
  line-height: 14px;
  font-weight: 450;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-inverse);
  background: rgba(var(--band), 0.95);
  padding: 1px 6px;
  border-radius: var(--radius-sharp);
  white-space: nowrap;
}

/* Dark-mode bumps the band fill opacity so tints stay legible on
 * black; the label chips already read fine on both themes. */
:root[data-theme="dark"] .spacing-band {
  background: rgba(var(--band), 0.16);
  border-top-color: rgba(var(--band), 0.7);
  border-bottom-color: rgba(var(--band), 0.7);
}

:root[data-theme="dark"] .spacing-band.is-offgrid {
  background: rgba(var(--band), 0.22);
}

/* ── Type audit ─────────────────────────────
 * Toggle from the footer or `T` key. JS replaces the contents of
 * every text-bearing block element with its role name (`H1: Suisse,
 * 600, 72/76`, etc.) rendered in the element's own type styling.
 * Off-system elements get an amber outline. Images / video / svg
 * are covered by viewport-fixed wireframe rectangles so the page
 * reads as a pure structural diagram of the design system. */
body.show-type .type-replaced.is-offsystem {
  outline: 1px dashed rgba(232, 144, 47, 0.7);
  outline-offset: 2px;
}

/* Wireframes mask page images only. They sit BELOW the menu and below
 * the baseline grid: when the menu opens, its solid surface hides them
 * naturally; when the audit is on, baseline rules paint cleanly over
 * the masked image without the wireframe fighting them. */
.type-wireframes {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: var(--z-overlay);
  opacity: 0;
  transition: opacity 200ms var(--ease-smooth);
}

body.show-type .type-wireframes { opacity: 1; }

/* Each .type-wireframe is positioned absolutely over an image's
 * rendered rect. Solid page-coloured fill hides the image content;
 * the X (two diagonal gradients) and dashed outline make it read as
 * a wireframe placeholder. Diagonals use calc(±0.5px) so the line
 * stays 1px regardless of element size. */
.type-wireframe {
  position: absolute;
  box-sizing: border-box;
  background-color: var(--color-surface-page);
  background-image:
    linear-gradient(
      to bottom right,
      transparent calc(50% - 0.5px),
      var(--color-text-tertiary) calc(50% - 0.5px),
      var(--color-text-tertiary) calc(50% + 0.5px),
      transparent calc(50% + 0.5px)
    ),
    linear-gradient(
      to top right,
      transparent calc(50% - 0.5px),
      var(--color-text-tertiary) calc(50% - 0.5px),
      var(--color-text-tertiary) calc(50% + 0.5px),
      transparent calc(50% + 0.5px)
    );
  outline: 1px dashed var(--color-text-tertiary);
  outline-offset: -1px;
}

/* ── Responsive ────────────────────────── */
@media (max-width: 1199px) {
  /* Page chrome shares one horizontal padding token at tablet so
   * the header brand, main content, footer, and the fullscreen
   * menu all line up to the same gutter — opening the menu doesn't
   * shift the brand. */
  .header { padding: var(--space-6) var(--space-6); }
  .main { padding: var(--space-32) var(--space-6) var(--space-16); }
  .hero { margin-bottom: var(--space-32); }
  .past-track {
    scroll-padding-inline: var(--space-6);
    margin-inline: calc(var(--space-6) * -1);
    padding-inline: var(--space-6);
  }
  .footer { padding: var(--space-16) var(--space-6) var(--space-6); }
}

@media (max-width: 900px) {
  .contact {
    gap: var(--space-8);
    padding-top: var(--space-16);
    margin-bottom: var(--space-20);
  }
  .footer-grid {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-10);
  }
  .footer-grid > .footer-col:nth-child(1),
  .footer-grid > .footer-col:nth-child(2),
  .footer-grid > .footer-col:nth-child(3),
  .footer-grid > .footer-col:nth-child(4) {
    grid-column: 1 / -1;
  }
  /* "On this page" is laptop-only — drop it from the stacked footer so
   * the small-screen footer keeps to the three evergreen columns. */
  .footer-grid > .footer-col--sections { display: none; }
}

/* Tablet menu: at narrower viewports the 12-col menu grid squeezes
 * each column tight. Tighten the gutter and padding so all the columns
 * still breathe before they collapse to a single stack on mobile
 * (≤768px). Link sizing is owned by the hierarchy rules below, which
 * apply at every width. */
@media (max-width: 1199px) and (min-width: 769px) {
  .site-menu { padding: var(--space-6) var(--space-6); }
  .site-menu-inner {
    column-gap: var(--space-3);
    row-gap: var(--space-10);
    padding: 16vh 0 var(--space-10);
  }
}

@media (max-width: 768px) {
  .header { padding: var(--space-5) var(--space-5); }
  .main { padding: var(--space-24) var(--space-5) var(--space-16); }
  .audience-tabs {
    margin: 0 0 var(--space-8);
  }
  .hero-headline { margin-bottom: var(--space-8); }
  .hero { margin-bottom: var(--space-24); }
  .site-menu { width: 100%; padding: var(--space-5); }
  .site-menu-inner {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-8);
    /* Generous bottom pad so the lower links sit in a comfortable
     * thumb zone instead of jammed against the pinned footer.
     * Adds the iOS safe-area inset on devices with a home bar. */
    padding: var(--space-6) 0 calc(var(--space-32) + env(safe-area-inset-bottom, 0px));
  }
  /* Drop the dark pill backer on small screens — the chip reads
   * as heavy chrome at this size. Keep the 44px hit area, lose
   * the fill so just the X mark sits on the page surface. */
  .site-menu-close {
    background: transparent;
    color: var(--color-text-primary);
  }
  .site-menu-inner > .menu-col:nth-child(1),
  .site-menu-inner > .menu-col:nth-child(2),
  .site-menu-inner > .menu-col:nth-child(3),
  .site-menu-inner > .menu-col:nth-child(4) {
    grid-column: 1 / -1;
  }
  .menu-col { gap: var(--space-4); }
  .site-menu-footer { padding-top: var(--space-3); }

  /* Switch hero to portrait aspect so the portrait source fills the frame */
  .featured-media { aspect-ratio: 9 / 16; }

  .case-grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-10); }
  .work-row,
  .work-row-offset-right,
  .work-row-offset-left,
  .work-row-pair {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-10);
  }
  .case-grid > *,
  .work-row-offset-right > .work-card,
  .work-row-offset-left > .work-card,
  .work-row-pair > .work-card { grid-column: 1; }
  .work-row { margin-bottom: var(--space-16); }
  .work-meta { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
  .past-card { flex-basis: 300px; }
  .past-track {
    scroll-padding-inline: var(--space-5);
    margin-inline: calc(var(--space-5) * -1);
    padding-inline: var(--space-5);
  }
  .footer { padding: var(--space-20) var(--space-5) var(--space-5); }
  .footer-inner { gap: var(--space-12); }
  .footer-bottom { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
}



.work-card:hover .media-placeholder,

.work-card:hover .work-media > img,

.work-card:hover .work-media > video {

  transform: scale(1.006);

}


/* Layout grid wrapper sits above the menu so column + baseline overlays
 * stay visible when the menu is open and JS re-targets their geometry
 * to .site-menu-inner. Cols are first child, baseline is second — DOM
 * order keeps baseline rules on top of the column shading. */
.layout-grid {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  z-index: calc(var(--z-modal) + 50);
}

/* === EXPERIMENT: Suisse Mono is gradually becoming the display face site-wide.
   Current scope: hero, audience tabs, hero arrow, case-study title/lede/pull-quote,
   menu, footer links. Delete this block (and the .menu-link + .site-menu-brand
   blocks below) to revert everything to Suisse Int'l Book.

   Previous scope (before lede + pull-quote joined — restore this selector list
   if the mono treatment on body-scale type starts to feel like too much):
     .hero-headline, .hero-prompt, .audience-tab, .hero .link-arrow,
     .cs-title, .site-menu, .footer .footer-link, .footer .footer-copy,
     .footer .grid-toggle-label
=== */
.hero-headline,
.hero-prompt,
.audience-tab,
.hero .link-arrow,
.cs-title,
.cs-tagline,
.cs-lede,
.cs-quote,
.site-menu,
.footer .footer-link,
.footer .footer-copy,
.footer .grid-toggle-label {
  font-family: "Suisse Int'l Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 400;
}
.menu-link {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  font-weight: 400;
}
/* Menu visual hierarchy — applies at every width, including the single
 * stacked column on mobile, so the Selected work titles always read as
 * the primary index. The "Selected work" column (column 1) reads at
 * Display S (23px); every other column — Photography and art, On this
 * page, Connect — sits well down at body size (14px), a quiet supporting
 * index. "All projects" is the catch-all at the foot of column 1, so it
 * drops back to body size to sit with the secondary links rather than
 * the project titles above it. */
.site-menu-inner > .menu-col:nth-child(1) .menu-link {
  font-size: var(--font-size-display-s);
  line-height: var(--line-height-display-s);
  letter-spacing: var(--letter-spacing-display-s);
}
.site-menu-inner > .menu-col:nth-child(1) .menu-col-list > li:last-child .menu-link {
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
}
/* Set "All projects" off from the project titles above it. The list's
 * own flex gap is space-2; this margin lifts the total break to space-4
 * so the catch-all link reads as a separate, lower-tier action without
 * floating too far from the list. */
.site-menu-inner > .menu-col:nth-child(1) .menu-col-list > li:last-child {
  margin-top: var(--space-2);
}
/* "All projects" is a go-elsewhere link, not a project title — give it a
 * trailing arrow (via ::after so the digit-scramble, which rewrites
 * textContent, can't strip it). Inherits the link's colour, so it lifts
 * to heading tone on hover with the label. */
.site-menu-inner > .menu-col:nth-child(1) .menu-col-list > li:last-child .menu-link::after {
  content: "→";
  margin-left: var(--space-2);
}
.site-menu-brand {
  font-family: "Suisse Int'l Book", Inter, sans-serif;
  font-weight: 600;
}
/* === END EXPERIMENT === */