/* ==========================================================================
   Site styles built on css/tokens.css — follow STYLE_GUIDE.md.
   No raw colors, sizes, or spacing here: tokens only.

   Page structure: rounded panels stacked on a dark frame. Every top-level
   block (name, nav, each section, footer) is a .panel; the 2px frame shows
   between them. Inside a panel, content sits on a 12-column grid.
   ========================================================================== */

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--space-8);
  background: var(--frame);
}

body {
  display: flex;
  flex-direction: column;
  gap: var(--frame-gap);
  min-height: 100vh;
  padding: var(--frame-gap);
  background: var(--frame);
  color: var(--fg);
  font-family: var(--font-text);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking-body);
  -webkit-font-smoothing: antialiased;
}

/* Theme change: the browser cross-fades a snapshot of the page (View Transitions,
   js/main.js). Never transition colors per element here: nested elements fight
   over inherited colors and the text flickers. */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: var(--dur-base);
  animation-timing-function: ease;
}

/* ---------- Page transitions (cross-document View Transitions) ----------
   Moving between pages, the nav bubble holds still and the rest of the page fades out
   while the new one rises in.
   Browsers without support just load the page. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: page;
  }
}

.site-header { view-transition-name: site-nav; }
main { view-transition-name: page-main; }

/* The old page fades out; the new page's frame and footer fade in, and its content panel
   (main) rises in from below as one block. Nothing inside the block animates on its own. */
html:active-view-transition-type(page)::view-transition-old(root),
html:active-view-transition-type(page)::view-transition-old(page-main) {
  animation: page-out 0.25s ease both;
}

html:active-view-transition-type(page)::view-transition-new(root) {
  animation: page-fade-in 0.4s ease 0.1s both;
}

html:active-view-transition-type(page)::view-transition-new(page-main) {
  animation: page-in 0.6s var(--ease-out) 0.1s both;
}

/* The block doesn't travel from its old spot: it only rises in */
html:active-view-transition-type(page)::view-transition-group(page-main) { animation: none; }

html:active-view-transition-type(page)::view-transition-group(*) {
  animation-duration: 0.6s;
  animation-timing-function: var(--ease-out);
}

@keyframes page-out { to { opacity: 0; } }
@keyframes page-in { from { opacity: 0; transform: translateY(var(--space-7)); } }
@keyframes page-fade-in { from { opacity: 0; } }

::selection {
  background: var(--accent-bg);
  color: var(--fg);
}

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

/* Every <img>/<video> carries its real width and height, so its space is reserved before it
   loads (nothing below it jumps); height: auto keeps that ratio at any width. */
img,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

/* Loading wireframe: until an image or video has loaded, its reserved box shows a softly
   pulsing --surface block instead of a blank gap or a half-painted image (js/main.js adds
   .is-loading). object-position pushes the partial picture out of the box, so only the
   block shows. Loaded media simply replaces it, in place, with no entrance animation. */
:is(img, video).is-loading {
  object-position: -99999px 0;
  background-color: var(--surface);
  animation: skeleton 1.6s ease-in-out infinite;
}

@keyframes skeleton { 50% { background-color: var(--skeleton-peak); } }

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

/* ---------- Type roles ---------- */
/* Name: Archivo Black weight, caps, sized by .site-hero to fill the panel */
.t-name {
  font-family: var(--font-display);
  font-weight: 900;
  line-height: var(--leading-name);
  letter-spacing: var(--tracking-name);
  text-transform: uppercase;
  white-space: nowrap;
}

/* Head: nav links, panel titles, footer — Archivo Black caps */
.t-head {
  font-family: var(--font-head);
  font-size: var(--text-head);
  font-weight: 400;
  line-height: var(--leading-head);
  letter-spacing: var(--tracking-head);
  text-transform: uppercase;
}

.t-h1,
.t-h2 {
  font-weight: 700;
  line-height: var(--leading-heading);
}

.t-h1 { font-size: var(--text-h1); letter-spacing: var(--tracking-h1); }
.t-h2 { font-size: var(--text-h2); letter-spacing: var(--tracking-h2); }

.t-h3 {
  font-size: var(--text-h3);
  font-weight: 600;
  line-height: var(--leading-h3);
  letter-spacing: var(--tracking-h3);
}

.t-impact {
  font-size: var(--text-impact);
  font-weight: 700;
  line-height: 1;
  letter-spacing: var(--tracking-h1);
  font-variant-numeric: tabular-nums;
}

.t-body { font-size: var(--text-body); }

.t-small {
  font-size: var(--text-small);
  line-height: var(--leading-body);
}

/* Labels and tags: Archivo Narrow caps. Meta (FURI ★ HANDED OFF 2026) uses .t-meta */
.t-label,
.t-meta {
  font-family: var(--font-label);
  font-weight: 500;
  text-transform: uppercase;
}

.t-label {
  font-size: var(--text-label);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking-label);
}

.t-meta {
  font-size: var(--text-tag);
  line-height: 1;
}

/* The star in "FURI ★ HANDED OFF" sits a size down */
.star { font-size: 0.875em; }

.muted { color: var(--fg-muted); }
.accent { color: var(--accent); }
.num { font-variant-numeric: tabular-nums; }

/* ---------- Panels & grid ---------- */
main {
  display: flex;
  flex-direction: column;
  gap: var(--frame-gap);
  flex: 1;
}

.panel {
  background: var(--bg);
  border-radius: var(--panel-radius);
  padding: var(--panel-pad);
}

/* Content panel: head at top, 100px to the content, 100px below it */
.panel--content {
  display: flex;
  flex-direction: column;
  gap: var(--panel-pad-end);
  padding-bottom: var(--panel-pad-end);
}

.panel-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
}

.panel-head .spacer { flex: 1; }

.grid {
  display: grid;
  grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
  column-gap: var(--gutter);
}

/* ---------- Hero: name fills the width, theme switch on its right ---------- */
.site-hero {
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.site-hero-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

/* 8.82em is the width of "SCARLETT WANG" at this tracking; the name gets everything
   except the switch and the gap. On desktop the switch is sized from the name
   (0.345em + 4px wide, so its knob is half its length), which folds into the
   divisor: 8.82 + 0.345 = 9.165. */
.site-hero {
  --name-size: calc((100cqw - 4px - var(--space-3)) / 9.165);
}

.site-hero .t-name {
  font-size: var(--name-size);
  /* trim to cap height so the switch and subtitle line up with the letters */
  margin: -0.035em 0 -0.06em;
}

.site-hero-sub {
  grid-column: 1 / span 9;
}

/* Case-study name banner: the same panel without the tagline; the name links home */
.site-hero a.t-name { display: block; }

/* Theme switch: vertical pill, knob up = light, knob down = dark.
   As tall as the name's caps; the round knob fills its width and half its length. */
.theme-switch {
  font-size: var(--name-size);
  --knob: calc(0.345em - 4px);
  flex: none;
  position: relative;
  width: calc(var(--knob) + 8px);
  height: calc(2 * var(--knob) + 8px);   /* = 0.69em, the caps height */
  padding: 4px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--fg);
  cursor: pointer;
  transition: background-color var(--dur-base) ease;
}

/* Larger hit area on small screens, where the pill is only 32px wide */
.theme-switch::before {
  content: '';
  position: absolute;
  inset: -6px;
}

.theme-switch::after {
  content: '';
  position: absolute;
  top: 4px;
  left: 4px;
  width: var(--knob);
  height: var(--knob);
  border-radius: 50%;
  background: var(--bg);
  transition:
    top var(--dur-base) var(--ease-spring),
    background-color var(--dur-base) ease;
}

.theme-switch[aria-checked='true']::after { top: calc(100% - var(--knob) - 4px); }

/* ---------- Nav panel ---------- */
/* Sticks to the top; hides on scroll down, returns on scroll up (js/main.js).
   It's a bubble on its own strip of frame: the header box is frame-colored and spans the
   gap around the nav, ::before draws the rounded nav panel, and ::after draws the rounded
   top corners of the panel below. So when the nav slides back over the page, the content
   reads as its own bubble pushed down beneath it, with the frame gap between them. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  margin: calc(-1 * var(--frame-gap)) calc(-1 * var(--frame-gap)) calc(-1 * var(--frame-gap));
  padding: calc(var(--frame-gap) + var(--panel-pad));
  background: var(--frame);
  border-radius: 0;
  transition: transform 0.6s var(--ease-out);
}

.site-header::before {
  content: '';
  position: absolute;
  inset: var(--frame-gap);
  z-index: -1;
  border-radius: var(--panel-radius);
  background: var(--bg);
}

.site-header::after {
  --r: var(--panel-radius);
  content: '';
  position: absolute;
  top: 100%;
  left: var(--frame-gap);
  right: var(--frame-gap);
  height: var(--r);
  pointer-events: none;
  background:
    radial-gradient(circle at 100% 100%, transparent calc(var(--r) - 0.5px), var(--frame) var(--r)) left top / var(--r) var(--r) no-repeat,
    radial-gradient(circle at 0 100%, transparent calc(var(--r) - 0.5px), var(--frame) var(--r)) right top / var(--r) var(--r) no-repeat;
}

/* Hidden: only the content panel's rounded top corners stay at the top edge */
.site-header.is-hidden { transform: translateY(-100%); }

.nav {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
}

.nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  transition: color var(--dur-fast) ease;
}

.nav a:hover,
.nav a[aria-current='page'] { color: var(--accent-text); }

/* Text roll on hover (spans injected by js/main.js) */
.nav a .roll {
  display: block;
  overflow: hidden;
  line-height: 1.1;
}

.nav a .roll > span {
  display: inline-block;
  text-shadow: 0 1.1em currentColor;
  transition: transform var(--dur-base) var(--ease-out);
}

.nav a:hover .roll > span,
.nav a:focus-visible .roll > span {
  transform: translateY(-1.1em);
}

/* ---------- Links, tags, buttons ---------- */
/* Every button and call-to-action link is set in label caps */
.text-link,
.btn,
.cursor-pill span {
  font-family: var(--font-label);
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

/* Text link: underline draws in from the left; hover turns scarlet ink */
.text-link {
  padding-bottom: 2px;
  background:
    linear-gradient(currentColor, currentColor) left bottom / 0% 1px no-repeat,
    linear-gradient(var(--fg), var(--fg)) left bottom / 100% 1px no-repeat;
  transition:
    background-size var(--dur-base) var(--ease-out),
    color var(--dur-fast) ease;
}

.text-link:hover {
  color: var(--accent-text);
  background-size: 100% 1px, 100% 1px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* Tag: outlined pill, Archivo Narrow 16 caps */
.tag {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-2);
  border: var(--hairline) solid var(--fg);
  border-radius: var(--radius-pill);
  font-family: var(--font-label);
  font-size: var(--text-tag);
  font-weight: 500;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
}

.tag-dot::before {
  content: '';
  width: 6px;
  height: 6px;
  margin-right: var(--space-2);
  border-radius: 50%;
  background: var(--accent);
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 44px;
  padding: 0 var(--space-2);
  border: var(--hairline) solid var(--fg);
  border-radius: var(--radius-pill);
  transition:
    background var(--dur-fast) ease,
    color var(--dur-fast) ease,
    border-color var(--dur-fast) ease;
}

.btn-primary { background: var(--fg); color: var(--bg); }
.btn-secondary { background: transparent; }
.btn-secondary:hover { background: var(--fg); color: var(--bg); }
.btn-accent { background: var(--accent); border-color: var(--accent); color: var(--paper); }
.btn-accent:hover { background: var(--accent-text); border-color: var(--accent-text); }

/* ---------- Marker: hand-drawn scarlet stroke, once per page ---------- */
/* The stroke shape is an SVG mask so its color still comes from --accent */
.marker {
  position: relative;
  display: inline-block;
  white-space: nowrap;
}

.marker::after {
  content: '';
  position: absolute;
  left: -3%;
  right: -4%;
  bottom: -0.1em;
  height: 0.2em;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 20' preserveAspectRatio='none'%3E%3Cpath d='M4 14 C 38 9, 74 8, 108 8.5 S 170 6.5, 196 4.5' fill='none' stroke='%23000' stroke-width='5.5' stroke-linecap='round'/%3E%3Cpath d='M22 15.5 C 60 12, 120 11.5, 172 9' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' opacity='.55'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 20' preserveAspectRatio='none'%3E%3Cpath d='M4 14 C 38 9, 74 8, 108 8.5 S 170 6.5, 196 4.5' fill='none' stroke='%23000' stroke-width='5.5' stroke-linecap='round'/%3E%3Cpath d='M22 15.5 C 60 12, 120 11.5, 172 9' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' opacity='.55'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  transform: rotate(-2deg);
  transform-origin: left center;
}

/* ---------- Grain (image wells, paper panels only) ---------- */
.grain {
  position: relative;
  isolation: isolate;
}

.grain::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------- Facts: columns on desktop, label | value rows on mobile ---------- */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-6);
}

.fact {
  border-top: var(--hairline) solid var(--border-strong);
  padding-top: var(--space-3);
  display: grid;
  gap: var(--space-2);
  align-content: start;
}

.fact dt {
  font-family: var(--font-label);
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.fact dd {
  font-size: var(--text-small);
  line-height: 1.45;
}

/* ---------- Project cards ---------- */
/* Two per row (6 columns each); image, title, then tags | meta */
.work-grid {
  display: grid;
  grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
  gap: var(--space-7) var(--gutter);
}

.work-card {
  grid-column: span 6;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.work-card-image {
  aspect-ratio: 2512 / 1448;
  overflow: hidden;
  background: var(--surface);
}

.work-card-image img,
.work-card-image video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}

.work-card:hover .work-card-image :is(img, video) { transform: scale(1.03); }


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

.work-card-tagline { transition: color var(--dur-fast) ease; }

.work-card:hover .work-card-tagline { color: var(--accent-text); }

/* Tags left, "FURI ★ HANDED OFF 2026" right */
.work-card-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  padding-top: var(--space-1);
}

/* Narrow cards: the pills stack; the meta stays on the right */
.work-card-row .tags {
  flex: 1 1 auto;
  min-width: 0;
}

.work-card-eyebrow {
  flex: none;
  color: var(--fg-muted);
  white-space: nowrap;
}

/* ---------- Gallery carousel ---------- */
.marquee {
  --marquee-h: 400px;
  display: block;
  overflow: hidden;
}

.marquee-track {
  display: flex;
  align-items: flex-start;
  gap: var(--gutter);
  width: max-content;
  will-change: transform;
}

.marquee-item {
  flex: none;
  height: var(--marquee-h);
  overflow: hidden;
  background: var(--surface);
}

.marquee-item img,
.marquee-item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.is-placeholder { background: var(--surface); }

/* ---------- Cursor pill: [data-cursor] zones (js/main.js) ---------- */
.has-cursor-pill [data-cursor] { cursor: none; }

.cursor-pill {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 60;
  pointer-events: none;
}

.cursor-pill span {
  display: inline-flex;
  align-items: center;
  height: 44px;
  padding: 0 var(--space-5);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--paper);
  white-space: nowrap;
  transform: translate(-50%, -50%) scale(0);
  transition: transform var(--dur-base) var(--ease-spring);
}

.cursor-pill.is-active span { transform: translate(-50%, -50%) scale(1); }

.cursor-pill span::after {
  content: '';
  width: 0.8em;
  height: 0.8em;
  margin-left: var(--space-2);
  background: currentColor;
  -webkit-mask: var(--icon-arrow-right) center / contain no-repeat;
  mask: var(--icon-arrow-right) center / contain no-repeat;
}

/* Links meant only for devices without hover */
@media (hover: hover) and (pointer: fine) {
  .touch-only { display: none; }
}

/* ---------- Lightbox (js/main.js) ---------- */
.is-expandable { cursor: zoom-in; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: var(--panel-pad);
  background: var(--scrim);
  cursor: zoom-out;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.lightbox.is-open { opacity: 1; }
.lightbox.is-closing { opacity: 0; transition-duration: 0.3s; }
.lightbox:focus { outline: none; }

.lightbox-img {
  display: block;
  max-width: none;
  object-fit: contain;
  background: var(--surface);
  transition: transform 0.55s var(--ease-out);
}

/* Enlarged video: the frame is sized by js/main.js; the video fills it */
.lightbox-media { position: relative; cursor: default; }

.lightbox-media video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}

/* Pause / replay stay visible while enlarged (no hover on the frame to reveal them) */
.lightbox-media .cs-video-controls {
  opacity: 1;
  transform: none;
}

.lightbox.is-closing .lightbox-img {
  transform: scale(0.97);
  transition-duration: 0.3s;
}

.lightbox-caption,
.lightbox-hint {
  font-family: var(--font-label);
  font-size: var(--text-label);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

.lightbox-caption { color: var(--paper); }

.lightbox-hint {
  position: absolute;
  top: var(--space-5);
  right: var(--panel-pad);
  color: var(--ink-3);
}

/* ---------- Footer panel: © on the left, contact links end columns 9 and 12 ---------- */
.site-footer {
  row-gap: var(--space-3);
}

.site-footer > :first-child { grid-column: 1 / span 6; }

.site-footer a {
  grid-column: span 3;
  justify-self: end;
  transition: color var(--dur-fast) ease;
}

.site-footer a:hover { color: var(--accent-text); }

/* ---------- Page head panels (About, Gallery) ---------- */
.page-intro {
  max-width: 44ch;
  color: var(--fg-muted);
}

/* ---------- About page ---------- */
.about-grid {
  row-gap: var(--space-8);
}

.about-photo {
  grid-column: 1 / span 4;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--surface);
}

.about-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.about-content {
  grid-column: 6 / span 7;
  display: grid;
  gap: var(--space-8);
  align-content: start;
}

.about-block h2 {
  margin-bottom: var(--space-4);
}

.about-block p {
  max-width: var(--measure);
}

.about-block p + p { margin-top: var(--space-4); }

/* Label | value rows (experience, contact) */
.rows > div {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: var(--space-5);
  padding: var(--space-3) 0;
  border-top: var(--hairline) solid var(--border-strong);
}

.rows > div:last-child { border-bottom: var(--hairline) solid var(--border-strong); }

/* ---------- Gallery page ---------- */
.g-section + .g-section { margin-top: var(--space-9); }
.g-section[hidden] + .g-section { margin-top: 0; }

.g-projects {
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}

.g-project-image {
  aspect-ratio: 1360 / 378;
  overflow: hidden;
  background: var(--surface);
}

.g-project-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.g-project-caption {
  display: flex;
  justify-content: space-between;
  gap: var(--space-5);
  margin-top: var(--space-2);
}

.g-masonry {
  display: flex;
  gap: var(--gutter);
  align-items: flex-start;
}

.g-masonry-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gutter);
}

.g-masonry-item {
  overflow: hidden;
  background: var(--surface);
}

.g-masonry-item img,
.g-masonry-item video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- Content fade-in (js/main.js) ----------
   Content blocks (cards, About blocks, gallery tiles, everything in a case study's sections)
   fade in as they scroll into view. Page titles never animate. An animation, not a
   transition, so it never overrides a block's own transitions (hover etc.). */
.fade-block:not(.is-in) { opacity: 0; }
.fade-block.is-in { animation: block-fade 0.6s ease-out var(--fade-delay, 0s) both; }
@keyframes block-fade { from { opacity: 0; } }

/* Project card images settle from a slight zoom as their card fades in */
.work-card.fade-block .work-card-image :is(img, video) { transform: scale(1.08); }
.work-card.fade-block.is-in .work-card-image :is(img, video) { transform: none; }
.work-card.fade-block.is-in:hover .work-card-image :is(img, video) { transform: scale(1.03); }

/* Already on screen when a page transition arrives: just there (the page itself rises in) */
.fade-block.is-instant { animation: none; }
.work-card.fade-block.is-instant .work-card-image :is(img, video) { transition: none; }

/* ---------- Responsive ---------- */
@media (max-width: 860px) {
  :root {
    --switch-w: 32px;
    --panel-pad-end: var(--space-8);
  }

  /* Small screens: the switch keeps a usable fixed width (still knob = half its length) */
  .site-hero { --name-size: calc((100cqw - var(--switch-w) - var(--space-3)) / 8.82); }

  .theme-switch { --knob: calc(var(--switch-w) - 8px); }

  .site-hero-sub { grid-column: 1 / -1; }

  .work-card { grid-column: 1 / -1; }

  .about-photo,
  .about-content { grid-column: 1 / -1; }

  .about-photo { max-width: 320px; }

  .rows > div { grid-template-columns: 1fr; gap: var(--space-1); }

  .marquee { --marquee-h: 260px; }

  /* Footer: © on its own line, links below */
  .site-footer > :first-child { grid-column: 1 / -1; }
  .site-footer a { grid-column: span 6; justify-self: start; }

  /* Facts stack into label | value rows */
  .facts {
    grid-template-columns: 1fr;
    row-gap: 0;
  }

  .fact {
    grid-template-columns: 38% 1fr;
    gap: var(--space-4);
    padding: var(--space-3) 0;
  }

  .fact:last-child { border-bottom: var(--hairline) solid var(--border-strong); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
}

/* ---------- External links ----------
   Links that open a new tab (and .ext links such as Email) get a heavy ↗ arrow.
   It's an SVG mask in currentColor, because Archivo Black has no arrow glyph and
   the fallback font draws it thin. */
a[target='_blank']::after,
a.ext::after {
  content: '';
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-left: 0.2em;
  vertical-align: 0;
  background: currentColor;
  -webkit-mask: var(--icon-arrow-ext) center / contain no-repeat;
  mask: var(--icon-arrow-ext) center / contain no-repeat;
}

/* Inline arrows (→ ←): the same heavy, currentColor style as the external-link arrow */
.arrow {
  display: inline-block;
  width: 0.8em;
  height: 0.8em;
  vertical-align: -0.02em;
  background: currentColor;
  -webkit-mask: var(--icon-arrow-right) center / contain no-repeat;
  mask: var(--icon-arrow-right) center / contain no-repeat;
}

.arrow--right { margin-left: 0.25em; }

.arrow--left {
  margin-right: 0.3em;
  -webkit-mask-image: var(--icon-arrow-left);
  mask-image: var(--icon-arrow-left);
}

/* ---------- Homepage intro (js/main.js) ----------
   Once per visit: the name panel opens full-screen, the name and the two tagline
   lines rise into place one after another, then the panel settles to its normal
   height — which carries the nav and Selected works up from the bottom in turn.
   Any scroll, click or key finishes it at once. */
.rise-line { display: block; }

/* While it plays, each line is a mask with a bottom edge only, so text rises out of it */
.intro .t-name,
.intro .rise-line { clip-path: inset(-50% -5% -0.06em -5%); }

.rise { display: inline-block; }

.intro .site-hero {
  position: relative;
  z-index: 0;
}

/* The full-screen look is drawn, not laid out: the page is in its final layout from
   the start, and this panel-colored extension (behind the hero's content) fills the
   screen edge to edge, square-cornered, over the frame. Settling slides its bottom edge
   up and, at the same time, pulls it in off the screen edges and rounds its corners, so
   the page morphs into the first bubble. Transform + clip-path only, so no per-frame
   layout of the whole page (animating height dropped frames). */
.intro .site-hero::after {
  content: '';
  position: absolute;
  top: calc(-1 * var(--frame-gap));
  left: calc(-1 * var(--frame-gap));
  right: calc(-1 * var(--frame-gap));
  z-index: -1;
  /* full screen from the first paint; its bottom lands one gap below the hero once settled */
  height: max(calc(100% + 2 * var(--frame-gap)), 100svh);
  background: var(--bg);
  clip-path: inset(0 round 0);
  transition:
    transform 0.9s var(--ease-out),
    clip-path 0.9s var(--ease-out);
}

.intro .site-hero.is-settling::after {
  transform: translateY(calc(-1 * var(--intro-drop, 0px)));
  clip-path: inset(var(--frame-gap) round var(--panel-radius));
}

/* The name and tagline rise in CSS, so they start with the first paint, not when JS loads */
.intro .rise { animation: intro-rise 0.7s var(--ease-out) both; }
.intro .t-name .rise { animation-delay: 0.15s; }
.intro .rise-line:nth-child(1) .rise { animation-delay: 0.5s; }
.intro .rise-line:nth-child(2) .rise { animation-delay: 0.68s; }

.intro .theme-switch { animation: intro-fade 0.5s ease 0.3s both; }

@keyframes intro-rise { from { transform: translateY(110%); } }
@keyframes intro-fade { from { opacity: 0; } }

/* Applying the measured offset must not animate */
.intro-instant .site-header,
.intro-instant main,
.intro-instant .site-hero::after { transition: none !important; }

/* Panels below ride up with the settling edge, each starting a little further back, so
   they arrive as a stack: same start, duration and curve, so the gaps between them only
   ever close (a delay instead opened a ~300px gap, because ease-out moves fastest first).
   They stay solid (no fade): a half-transparent panel shows the frame through it and
   reads as muddy gray. What's inside them fades in instead (the reveals in js/main.js). */
.intro .site-header,
.intro main {
  transform: translateY(calc(var(--intro-drop, 100svh) + var(--intro-lag)));
  transition: transform 0.9s var(--ease-out);
}

.intro .site-header { --intro-lag: var(--space-6); }
.intro main { --intro-lag: var(--space-9); }

.intro .site-header.is-up,
.intro main.is-up { transform: none; }

