/* ==========================================================================
   Jason Valdovinos — portfolio
   Two surfaces: dark (work index, contact) and light (case studies, about).
   Set via <html data-surface="dark|light">.
   ========================================================================== */

/* --- reset -------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body, h1, h2, h3, h4, p, figure, blockquote, ul, ol, dl, dd { margin: 0; padding: 0; }
ul, ol { list-style: none; }
img, video { display: block; max-width: 100%; }
button { font: inherit; }

/* --- tokens ------------------------------------------------------------- */
:root {
  --ink:        #0A0A0A;
  --paper:      #FAFAF8;
  --muted-dark: #8C8C8C;
  --muted-light:#6E6E6B;
  --rule-dark:  rgba(255,255,255,.16);
  --rule-light: rgba(10,10,10,.16);
  /* Page-level rules divide sections; soft rules divide rows inside one. Two
     weights is what keeps a dense spread readable instead of gridded. */
  --rule-dark-soft:  rgba(255,255,255,.085);
  --rule-light-soft: rgba(10,10,10,.085);

  --font-display: 'Syne', ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
  --font-mono:    'Syne Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --gutter: clamp(1.25rem, 4vw, 3.25rem);
  --header-h: 76px;
  --ease: cubic-bezier(.2, .7, .2, 1);
}

html[data-surface="dark"] {
  --bg: var(--ink);
  --fg: var(--paper);
  --muted: var(--muted-dark);
  --rule: var(--rule-dark);
  --rule-soft: var(--rule-dark-soft);
  --inverse-bg: var(--paper);
  --inverse-fg: var(--ink);
  color-scheme: dark;
}
html[data-surface="light"] {
  --bg: var(--paper);
  --fg: var(--ink);
  --muted: var(--muted-light);
  --rule: var(--rule-light);
  --rule-soft: var(--rule-light-soft);
  --inverse-bg: var(--ink);
  --inverse-fg: var(--paper);
  color-scheme: light;
}

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1rem, .95rem + .2vw, 1.0625rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

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

a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--fg); color: var(--bg); padding: .75rem 1rem;
}
.skip:focus { left: 0; }

/* --- shared type -------------------------------------------------------- */
.mono {
  font-family: var(--font-mono);
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1.4;
}
.label {
  font-family: var(--font-mono);
  font-size: .6875rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}
.display {
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -.03em;
  line-height: .92;
  text-wrap: balance;
}

.wrap { padding-inline: var(--gutter); }
.measure { max-width: 62ch; }

hr.rule { border: 0; border-top: 1px solid var(--rule); margin: 0; }

/* --- header ------------------------------------------------------------- */
.site-header {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  height: var(--header-h);
  display: flex; align-items: center; gap: 1.5rem;
  padding-inline: var(--gutter);
  color: var(--fg);
  transition: background-color .3s var(--ease), backdrop-filter .3s var(--ease);
}
.site-header.is-stuck {
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
}
/* The brand lockup: name + a one-line positioning statement. Below 1100px
   they stack — NAME / thesis, one authored signature — which keeps the
   tagline visible down through tablet widths, where it still fits on its
   own line. At 1100px and up there's finally room to run them side by side
   on the name's baseline; separation there comes from real space (a wider
   gap than a word-space), plus the same weight/size/brightness contrast
   that does the work at every width — not a rule, dot or vertical bar.
   Below 720px (phones) the tagline is dropped entirely — see .brand-role's
   max-width:720px rule — because it no longer fits on one line there and a
   wrapped second line overflows the header's fixed height. */
.brand {
  display: flex; flex-direction: column; gap: .1rem;
  min-width: 0; margin-right: auto;
}
@media (min-width: 1100px) {
  .brand { flex-direction: row; align-items: baseline; gap: 1.5rem; }
}
.brand-name {
  font-family: var(--font-display);
  font-weight: 700; font-size: .9375rem;
  letter-spacing: .07em; text-transform: uppercase;
  line-height: 1.15; white-space: nowrap;
}
/* ~70% of the name's size and a real (medium) weight, not just smaller —
   size and weight carry the hierarchy, not opacity. Bright enough to read
   as a second authored line rather than fine print: mixed toward --fg
   rather than the desaturated --muted grey used for actual metadata
   elsewhere in the nav, and it stays theme-correct on both surfaces since
   it mixes with --fg rather than a fixed colour. Wraps at its own word
   breaks below 1100px (stacked) so it's never at risk of collision; paired
   with the name on one line above that, where there's room for it. */
.brand-role {
  font-family: var(--font-display);
  font-weight: 500; font-size: .6875rem;
  letter-spacing: .04em; text-transform: uppercase;
  line-height: 1.25; color: color-mix(in srgb, var(--fg) 78%, transparent);
}
@media (min-width: 1100px) { .brand-role { white-space: nowrap; } }

/* Phone-only reprise of the tagline, dropped from .brand-role above. Same
   DOM element always exists; only one of the two is ever display:block at a
   given width, so a screen reader never hears it twice. Shown as a third
   row spanning the full header width — see .site-header's flex-wrap rule
   at 720px — under the name and nav rather than squeezed beside either. */
.brand-tagline { display: none; }

.nav { display: flex; align-items: center; gap: clamp(.9rem, 2.2vw, 1.75rem); }
.nav a {
  font-family: var(--font-mono);
  font-size: .6875rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted);
  padding-block: .35rem;
  border-bottom: 1px solid transparent;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.nav a:hover, .nav a:focus-visible { color: var(--fg); }
.nav a[aria-current="page"] { color: var(--fg); border-bottom-color: currentColor; }
.nav .icon-link { display: grid; place-items: center; padding: .35rem; border-bottom: 0; }
.nav .icon-link svg { width: 17px; height: 17px; display: block; }
.nav .icon-link[aria-current="page"] { color: var(--fg); }
.nav-sep { width: 1px; height: 14px; background: var(--rule); }

@media (max-width: 720px) {
  :root { --header-h: 84px; }
  /* Below this width the tagline wraps to two lines against the name's one,
     and the header's fixed height doesn't grow to fit — the wrapped line
     spills out and overlaps the section below it. Phone header width also
     has to share space with the nav on the same row, unlike the >=1100px
     layout where the tagline gets its own baseline next to the name. So on
     phones .brand-role (name-adjacent) drops out, and .brand-tagline takes
     over as a third row of its own: name + nav keep one line between them
     via .brand's margin-right:auto (unchanged), and the auto margin eating
     all the line's free space is exactly what pushes .brand-tagline, next
     in source order, onto a wrapped second line at full width below both.
     align-content is flex-start, not center: centering the whole two-line
     block made the name+tagline pair and the single-line nav read as two
     things balancing against each other. Pinning to the top gives name and
     nav one shared row instead, with the tagline reading as a subordinate
     line under it and the header's leftover height landing below the
     tagline as breathing room before the page content starts, not split
     evenly above and below. */
  .site-header {
    flex-wrap: wrap; align-content: flex-start; align-items: flex-end;
    padding-top: 1.125rem; row-gap: .2rem; column-gap: .75rem;
  }
  .nav { gap: .85rem; }
  .brand-role { display: none; }
  .brand-tagline {
    display: block;
    flex-basis: 100%;
    font-family: var(--font-display);
    font-weight: 500; font-size: .6875rem;
    letter-spacing: .04em; text-transform: uppercase;
    line-height: 1.2; color: color-mix(in srgb, var(--fg) 78%, transparent);
  }
}
@media (max-width: 520px) {
  /* Contact keeps its text link; the icon is a direct mailto, not a duplicate */
  .nav-sep { display: none; }
  .nav a[href$="contact.html"]:not(.icon-link) { display: none; }
}
@media (max-width: 430px) {
  /* Phones: with the tagline gone (see .brand-role above), the name alone
     still doesn't share the row with the full-size nav — name + nav
     together overflow the header at standard iPhone widths (375–430px),
     and since brand-name is white-space:nowrap the overflow used to render
     as text bleeding into the nav rather than a clean wrap. Shrink both
     sides so they actually fit, up through the widest iPhones. Tight
     enough that they also fit at 320px (the narrowest supported width)
     without .site-header's flex-wrap kicking the nav down to its own
     line — flex-wrap decides line-breaks off each item's un-shrunk
     hypothetical width, so unlike a single-line flex row this doesn't
     quietly flex-shrink to fit; it wraps instead unless it actually fits.
     (.site-header's own inter-item gap is already tightened for the whole
     <=720px tier above — 431px, the narrow end of the unshrunk 431-720
     range, is exactly as tight a fit as 320px is here.) */
  .brand-name { font-size: .75rem; letter-spacing: .03em; }
  .nav { gap: .4rem; }
  .nav a { font-size: .5938rem; letter-spacing: .07em; }
  .nav .icon-link { padding: .2rem 0; }
  .nav .icon-link svg { width: 14px; height: 14px; }
  .brand-tagline { font-size: .625rem; letter-spacing: .03em; }
}

/* WORK filter dropdown — typography-led, no boxes/pills/borders. "Work"
   stays a plain link (clicking it always reaches the full grid); the caret
   is its own tappable control so touch and keyboard users can reach the
   menu without a :hover that touch can't produce. Mouse users get the menu
   on hover, via :hover below; keyboard/touch get it from the JS-driven
   .is-open class instead. Deliberately NOT :focus-within — the toggle
   button itself is a descendant of .nav-item--work, so that would force
   the menu open the instant the button merely receives focus (before it's
   activated at all), which also defeats Escape since focus lands right
   back on the toggle. .is-open persists across Tab once opened, so keyboard
   users still keep the menu visible while tabbing through its items. */
.nav-item--work { position: relative; display: flex; align-items: center; }
.nav-work-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; margin: 0; padding: .35rem .2rem;
  color: var(--muted); cursor: pointer; line-height: 1;
  font-family: var(--font-mono);
  transition: color .2s var(--ease);
}
.nav-work-toggle:hover, .nav-work-toggle:focus-visible,
.nav-item--work:hover .nav-work-toggle,
.nav-item--work.is-open .nav-work-toggle { color: var(--fg); }
.nav-work-caret {
  display: inline-block; font-size: .75rem; transition: transform .2s var(--ease);
}
.nav-item--work:hover .nav-work-caret,
.nav-item--work.is-open .nav-work-caret { transform: rotate(180deg); }

/* The outer box is the hover bridge: it starts flush against the nav item
   (top: 100%, no gap) so the mouse never crosses dead space on the way down.
   The visual gap lives inside, as the list's own padding-top. */
.work-menu {
  position: absolute; top: 100%; left: 0; right: auto;
  padding-top: .75rem;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  pointer-events: none;
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s .2s;
  z-index: 90;
}
.nav-item--work:hover .work-menu,
.nav-item--work.is-open .work-menu {
  opacity: 1; visibility: visible; transform: none; pointer-events: auto;
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s;
}
.work-menu__list {
  background: color-mix(in srgb, var(--bg) 90%, transparent);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  border-top: 1px solid var(--rule);
  padding-block: .75rem;
  display: grid;
  width: max-content;
  max-width: min(19rem, calc(100vw - 2 * var(--gutter)));
}
.work-menu__list a {
  display: block;
  padding: .6rem 1.1rem;
  border-bottom: 1px solid transparent;
  white-space: normal;
  transition: color .2s var(--ease), padding-left .2s var(--ease);
}
.work-menu__list a:hover, .work-menu__list a:focus-visible {
  color: var(--fg); padding-left: 1.4rem;
}
.work-menu__list a[aria-current="true"] { color: var(--fg); border-bottom-color: currentColor; }

@media (max-width: 1099.98px) {
  /* Below the row-layout breakpoint, "Work" sits close enough to the right
     edge of the header that the menu's own width (up to 19rem) runs past
     the viewport if it opens rightward from the link's left edge — clipped
     by the page's overflow-x:hidden, so the wider filter labels get cut off
     rather than wrapping. Anchoring it to the link's right edge instead
     keeps the whole menu on screen at every width this affects (confirmed
     down to 375px and up through 1024px, where it's tightest). */
  .work-menu { left: auto; right: -.2rem; }
}

/* --- footer --------------------------------------------------------------
   A two-column CTA, not a nav bar: Let's work and Contact mirror each other
   — same label treatment, same line treatment — with hierarchy coming from
   position (left leads at the far edge, right trails at the far edge), not
   from a size difference. Work/About already live in the persistent header,
   so the footer doesn't repeat them. */
.site-footer {
  /* No margin — the footer stacks against the page's last section exactly
     like one .sec stacks against the next: its own bottom padding (or the
     work grid's, see below) plus this top padding is the whole gap. */
  padding: clamp(1.5rem, 3vw, 2rem) var(--gutter);
  border-top: 1px solid var(--rule);
}
.footer-cols { display: grid; gap: clamp(1.5rem, 4vw, 3rem); }
@media (min-width: 640px) {
  .footer-cols { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); align-items: start; }
}
.footer-col { display: grid; gap: .6rem; align-content: start; }
@media (min-width: 640px) {
  .footer-col--contact { justify-items: end; text-align: right; }
}
/* the plain lines on the left mirror the linked lines on the right — same
   family, size, tracking and case, both running on one line — only the
   link's own affordance differs. */
.footer-lines {
  font-family: var(--font-mono); font-size: .75rem;
  letter-spacing: .12em; text-transform: uppercase;
}
.site-footer a { border-bottom: 1px solid transparent; transition: color .2s var(--ease), border-color .2s var(--ease); }
.site-footer a:hover { color: var(--fg); border-bottom-color: currentColor; }

/* --- page intro --------------------------------------------------------- */
.page-top { padding-top: calc(var(--header-h) + clamp(1.5rem, 3vw, 2rem)); }
/* About's first child is itself a .sec — border-top rule plus its own top
   padding — so the page-top cushion above would double up with it. Clear
   the header and nothing more; the first section's own padding does the
   same job every later section's does. */
.page-top.about { padding-top: var(--header-h); }

/* ==========================================================================
   WORK INDEX
   ========================================================================== */
.work-hero { position: relative; }
.work-hero__media { aspect-ratio: 21 / 9; background: #000; overflow: hidden; }
.work-hero__media > * { width: 100%; height: 100%; object-fit: cover; }

.work-intro { padding: 0 var(--gutter) 1rem; }

.work-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2px;
  padding-inline: 2px;
  /* Matches .sec's own bottom padding — the same rhythm the footer picks up
     from the last About section, so both pages hand the footer an equal gap. */
  padding-bottom: clamp(2.25rem, 4.5vw, 3.5rem);
}
/* 1 / 2 / 4 columns. The case tiles fill the grid in order. */
@media (min-width: 560px)  { .work-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .work-grid { grid-template-columns: repeat(4, 1fr); } }

.tile { position: relative; display: flex; flex-direction: column; overflow: hidden; background: #050505; container-type: inline-size; }
/* The WORK filter hides non-matching tiles with the plain [hidden] attribute
   — removed from layout, tab order and the a11y tree in one move, and the
   grid closes the gap on its own since a display:none tile takes no cell.
   .tile already declares its own display, which would otherwise beat the
   UA stylesheet's [hidden] rule, so it needs restating here explicitly. */
.tile[hidden] { display: none; }
.tile__frame { position: relative; flex: 1 1 auto; min-height: 0; aspect-ratio: 16 / 10; overflow: hidden; container-type: inline-size; }
@media (max-width: 560px) { .tile__frame { aspect-ratio: 4 / 3; } }

/* --zoom scales past black bars baked into a letterboxed source, so every tile
   fills its frame the same way. 1 for normal footage. */
.tile__media { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform: scale(var(--zoom, 1));
  transition: transform .7s var(--ease), filter .5s var(--ease), opacity .5s var(--ease); }

/* typographic placeholder shown until real media is dropped in */
.tile__placeholder {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background:
    radial-gradient(120% 120% at 25% 15%, color-mix(in srgb, var(--accent) 62%, #000) 0%, transparent 62%),
    radial-gradient(140% 140% at 85% 90%, color-mix(in srgb, var(--accent) 34%, #000) 0%, transparent 58%),
    #0B0B0B;
  transition: filter .5s var(--ease);
}
.tile__placeholder span {
  font-weight: 800; text-transform: uppercase; letter-spacing: -.04em;
  /* --ph-size is set per case in build.py so long brand names never clip */
  font-size: max(1rem, min(3rem, var(--ph-size, 12cqw))); line-height: .9;
  color: rgba(255,255,255,.13);
  padding-inline: 1rem; text-align: center;
  max-width: 100%; overflow: hidden;
}

.tile__overlay {
  position: absolute; inset: 0; z-index: 2;
  /* Items stay stretched to the tile width and centre via text-align; centring
     them with justify-items would size each to max-content and spill. */
  display: grid; align-content: center; justify-items: stretch;
  gap: .75rem; padding: 1.5rem; text-align: center;
  background: rgba(6,6,6,.62);
  opacity: 0; transition: opacity .35s var(--ease);
}
.tile__overlay h2 {
  font-weight: 800; text-transform: uppercase; letter-spacing: -.035em; line-height: .95;
  /* --overlay-size is set per case in build.py from the longest word */
  font-size: max(.95rem, min(2.25rem, var(--overlay-size, 9cqw)));
  color: #fff; text-wrap: balance; overflow-wrap: break-word;
}
.tile__overlay .mono { color: rgba(255,255,255,.72); }

@media (hover: hover) {
  .tile:hover .tile__media, .tile:focus-visible .tile__media { transform: scale(calc(var(--zoom, 1) * 1.04)); }
  .tile:hover .tile__overlay, .tile:focus-visible .tile__overlay { opacity: 1; }
}
@media (hover: none) {
  /* touch: no hover, so keep the title permanently legible over the media */
  .tile__overlay { opacity: 1; background: linear-gradient(to top, rgba(6,6,6,.78) 0%, rgba(6,6,6,.15) 60%, transparent 100%);
    align-content: end; text-align: left; }
  .tile__caption { display: none; }
}

.tile__caption {
  display: grid; gap: .2rem;
  padding: .75rem .25rem 1.75rem;
  color: var(--muted);
}
@container (min-width: 380px) {
  .tile__caption {
    grid-auto-flow: column; justify-content: space-between;
    align-items: baseline; gap: 1rem;
  }
}
.tile__caption .name { color: var(--fg); }
@media (hover: hover) {
  .tile:hover .tile__caption .name { text-decoration: underline; text-underline-offset: .25em; }
}

/* ==========================================================================
   CASE STUDY — editorial: mono body copy, hairline structure, tight rhythm.
   Everything aligns to the same page gutters, top to bottom.
   ========================================================================== */
/* One shared gap for every real section change — hero to hook, hook to band,
   band to the wide blocks, gallery to the pager. Closely-related lines within
   a cluster (relationship + title + disciplines) use a smaller, tighter gap
   of their own instead — proximity is what marks them as one group. Each
   transition used to be built from two stacked paddings (one on the block
   above, one on the block below); tokenizing it to a single source is what
   keeps the page from reading as a pile of separately-tuned gaps. */
.case { --case-gap: clamp(1.75rem, 3.5vw, 2.75rem); }
.case-head { container-type: inline-size; }

/* --- the client lockup ---------------------------------------------------
   Painted as CSS masks, exactly like the About marquee: only the alpha of the
   source matters, so a white wordmark and a coloured one come out the same ink
   and both invert on a dark surface. --w/--h arrive unitless from build.py so
   the whole lockup can be scaled in one place. Each mark keeps its own height
   from logos.json — that file sizes marks by equal optical WEIGHT, not equal
   height, so forcing a common box here would both float them below the label
   and flatten a square mark against a long wordmark.
   ------------------------------------------------------------------------- */
.clientmark { --k: 1; display: flex; flex-direction: column; align-items: flex-start; gap: clamp(1rem, 2vw, 1.5rem); }
.clientmark__logo {
  flex: 0 1 auto;
  width: calc(var(--w) * 1px * var(--k)); height: calc(var(--h) * 1px * var(--k));
  max-width: 100%;
  background-color: var(--fg);
  -webkit-mask-size: contain;     mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: left center; mask-position: left center;
}
@media (max-width: 1023.98px) { .clientmark { --k: .85; } }
@media (max-width: 560px)     { .clientmark { --k: .7; } }

/* Masthead line: the relationship, small, above the title. Tight on purpose —
   relationship, title and disciplines read as one identifying cluster, so
   proximity does the grouping instead of a rule. */
.case-meta {
  display: flex; flex-wrap: wrap; gap: .35rem 1.5rem; justify-content: space-between;
  margin-bottom: clamp(.75rem, 1.5vw, 1.125rem);
}

/* Scope, small, under the title — a credit line, not a row of tags. */
.case-disciplines {
  margin-top: clamp(.75rem, 1.5vw, 1.125rem);
  font-family: var(--font-mono); font-size: .6875rem;
  letter-spacing: .12em; text-transform: uppercase; line-height: 1.8;
  color: var(--muted);
}

/* Two numbers from build.py, both container-relative:
     --title-size  where the whole title sets on ONE line
     --title-max   the hard ceiling past which a word would break mid-character
   Take the smaller. On a wide screen that is --title-size, so the title fills
   its measure on one line instead of stacking three deep with the right half
   empty. On a phone --title-max wins and the title wraps, whole words only.
   The 1.75rem floor is deliberately *inside* the min() — it must never beat
   --title-max, or a long word splits. */
.case-title {
  font-weight: 700;
  font-size: min(3.25rem, var(--title-max, 12cqw), max(1.75rem, var(--title-size, 4cqw)));
  overflow-wrap: break-word;
}


/* Hero sits under the title at every width, aligned to the same margins. */
.case-hero { margin-top: clamp(1.25rem, 2.5vw, 2rem); }

.media-frame { position: relative; overflow: hidden; background: color-mix(in srgb, var(--fg) 6%, var(--bg)); }
/* --ratio is set per case in build.py: cinematic films are letterboxed inside
   16:9 and vertical films are 9:16, so a fixed frame would bar or crop them. */
.media-frame.is-hero { aspect-ratio: var(--ratio, 16 / 9); }
@media (max-width: 640px) {
  .media-frame.is-hero:not([style*="--ratio"]) { aspect-ratio: 4 / 3; }
}
/* A tall hero would swallow the screen — cap it and let cover do the rest. */
.media-frame.is-hero { max-height: 82vh; }
.media-frame > img, .media-frame > video { width: 100%; height: 100%; object-fit: cover; }
.media-frame__placeholder {
  position: absolute; inset: 0; display: grid; place-items: center; gap: .5rem;
  background:
    radial-gradient(110% 110% at 30% 20%, color-mix(in srgb, var(--accent) 55%, #000) 0%, transparent 65%),
    radial-gradient(130% 130% at 80% 85%, color-mix(in srgb, var(--accent) 30%, #000) 0%, transparent 60%),
    #0C0C0C;
}
.media-frame__placeholder .mono { color: rgba(255,255,255,.42); }

/* The one line that has to land. Bottom padding is 0 on purpose — the gap
   down to the band comes from .case-band's own margin-top, one source
   instead of two stacked ones. */
.case-hook { padding-block: var(--case-gap) 0; }
/* Runs the full measure, like the film above it and the band below. It was
   capped at 22ch, which set it at about a third of the page with the rest
   empty. Smaller than the title, so the hierarchy still reads. */
.case-hook p {
  font-weight: 700; letter-spacing: -.025em; line-height: 1.15;
  font-size: clamp(1.25rem, 2.4vw, 2rem);
  text-wrap: pretty;
}

/* --- the case body -------------------------------------------------------
   From here down the page is the same module as About: a large label on the
   left, the content on the right, a hairline between each section. .sec and
   .sec--wide are defined once, further down, and shared by both pages.
   ------------------------------------------------------------------------- */
/* --- the case band ------------------------------------------------------
   Three columns split by vertical rules: PARTNER (the marks), PROJECT (the
   opportunity and what came of it) and the role. Modelled on
   teamepiphany.com/delta-ces, which Jason sent as the reference.

   Three text columns need real width, so the band only splits at 1100px and
   stacks below that. The proportions are set by what each column holds, not by
   symmetry: PARTNER needs room for a mark and nothing else, PROJECT carries
   the body copy and gets the most, the role list sits between them.
   ------------------------------------------------------------------------- */
.case-band { display: grid; margin-top: var(--case-gap); row-gap: var(--case-gap); }
/* No column padding — at 1100px+ the three sit in one row, so any padding
   here would only pile onto .case-band's own margin-top above them. Stacked
   below that, row-gap alone is what separates Partner / Project / My Role;
   the eyebrow label at the top of each is what marks the seam visually. */
.case-band__col { padding-block: 0; }
/* Stacked, a column has the whole page and the copy would run past 90
   characters a line. Cap it so the stacked layout reads as well as the split
   one; above the breakpoint the columns are already narrow enough. */
@media (max-width: 1099.98px) {
  .case-band__col--project, .case-band__col--role { max-width: 44rem; }
}
@media (min-width: 1100px) {
  /* No rule between columns at this width either — column-gap carries the
     separation the border used to. */
  .case-band {
    grid-template-columns: minmax(0, 2.5fr) minmax(0, 5.5fr) minmax(0, 4fr);
    column-gap: clamp(2.5rem, 4vw, 4rem);
  }
  /* A list in a column this narrow reads better a step down from the running
     text — it is an index of what he did, not prose. */
  .case-band__col--role .contrib { font-size: .9375rem; }
}

/* The label for every block on a case study: a small eyebrow with a rule under
   it. Drawn as a border rather than text-decoration so it never reads as a
   link. Case pages set these small on purpose — the head already carries the
   display type, and About's large labels fought the title. */
.case-label {
  display: inline-block;
  font-family: var(--font-mono); font-weight: 400;
  font-size: .6875rem; letter-spacing: .12em; text-transform: uppercase;
  line-height: 1.4; color: var(--fg);
  margin-bottom: clamp(1.625rem, 2.75vw, 2.25rem);
}

/* Full-width blocks under the band — the partner ecosystem and the gallery.
   No rule above them; the gap and the section's own eyebrow label mark the
   transition instead. Same shared gap as every other section change. */
.case-wide { margin-top: var(--case-gap); }

.case-band p + p { margin-top: .9rem; }
/* The opening and the payoff take the ink; the detail between them steps back,
   so one long Project column still reads as a shape. */
.case-band p:not(.case-lead):not(.case-outcome__text):not(.credit) { color: var(--muted); }
/* Outcome follows Opportunity inside the same column — give it air. */
.case-band .case-outcome__text { margin-top: clamp(1.5rem, 3vw, 2.25rem); }

/* The payoff, and the opening of the opportunity — a touch larger than the
   body copy. One step up, used twice; every other paragraph stays at reading
   size, which is what keeps a long case from reading as a wall. */
.case-outcome__text, .case-lead { font-size: clamp(.9375rem, .82rem + .35vw, 1.125rem); }

/* Contribution: a coherent group of statements, not a ruled table. Spacing
   alone separates one from the next — no rule, no bullet, nothing hanging in
   the margin. */
.contrib { display: grid; gap: .85rem; }
.credit { margin-top: 1.5rem; color: var(--muted); }

/* watch link */
.watch { margin-top: 1.75rem; }
.link-btn {
  display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--font-mono); font-size: .6875rem; letter-spacing: .12em; text-transform: uppercase;
  padding-bottom: .4rem;
  border-bottom: 1px solid var(--rule);
  transition: border-color .25s var(--ease), gap .25s var(--ease);
}
.link-btn:hover, .link-btn:focus-visible { border-bottom-color: currentColor; gap: .8rem; }
.link-btn svg { width: 12px; height: 12px; }

/* The partner ecosystem: named sub-parts of a case as columns. No rules
   between them — the gap, and each column's own mark/kicker/heading, is what
   separates one from the next. */
.breakdown-grid { display: grid; row-gap: clamp(1.75rem, 3vw, 2.5rem); }
.breakdown {
  padding-block: 0 clamp(1.75rem, 3vw, 2.5rem);
  display: grid; gap: .6rem; align-content: start;
}

@media (min-width: 640px) and (max-width: 1023.98px) {
  .breakdown-grid { grid-template-columns: repeat(2, 1fr); column-gap: clamp(1.75rem, 3vw, 2.5rem); }
}
@media (min-width: 1024px) {
  .breakdown-grid { grid-template-columns: repeat(3, 1fr); column-gap: clamp(1.75rem, 3vw, 2.5rem); }
}
.breakdown__kicker { display: block; }
/* A small partner mark at the head of each column — recognition before the
   name is read. Same mask technique as the client lockup, one size for all. */
.breakdown__mark {
  display: block; margin-bottom: .75rem;
  width: calc(var(--w) * 1px); height: 36px; max-width: 100%;
  background-color: color-mix(in srgb, var(--fg) 78%, transparent);
  -webkit-mask-size: contain;     mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: left center; mask-position: left center;
}
.breakdown h3 {
  font-weight: 800; text-transform: uppercase; letter-spacing: -.02em; line-height: 1.05;
  font-size: clamp(1.0625rem, 1.3vw, 1.3125rem); overflow-wrap: break-word;
}
.breakdown p { color: var(--muted); }

/* A 12-column media grid. Each item declares how wide it sits and its own
   aspect ratio, so a portrait still stays portrait and a film can run full
   width without anything being cropped to a single house shape. */
.gallery {
  display: grid; grid-template-columns: 1fr;
  gap: clamp(.5rem, 1vw, .875rem);
  padding-bottom: clamp(.75rem, 1.5vw, 1.125rem);
  align-items: start;
}
@media (min-width: 640px)  { .gallery { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .gallery { grid-template-columns: repeat(12, 1fr); } }

.gallery figure { margin: 0; }
@media (min-width: 640px) and (max-width: 1023.98px) {
  .gallery figure.is-full { grid-column: span 2; }
}
@media (min-width: 1024px) {
  .gallery figure         { grid-column: span 6; }
  .gallery figure.is-full    { grid-column: span 12; }
  .gallery figure.is-half    { grid-column: span 6; }
  .gallery figure.is-third   { grid-column: span 4; }
  .gallery figure.is-quarter { grid-column: span 3; }
}
.gallery .media-frame { aspect-ratio: var(--ratio, 4 / 3); }

/* prev / next — no rule between them; each link's own padding plus the
   Previous/Next label and hover state carry the separation. Same shared
   gap above it as every other section change on the page. */
.pager { margin-top: var(--case-gap); display: grid; }
@media (min-width: 760px) { .pager { grid-template-columns: 1fr 1fr; column-gap: clamp(2rem, 4vw, 3rem); } }
.pager a { padding: clamp(1.5rem, 3vw, 2.5rem) var(--gutter); display: grid; gap: .5rem; transition: background-color .3s var(--ease); }
@media (min-width: 760px) {
  .pager a + a { text-align: right; }
  /* First and last case studies only have one neighbour — keep it on its own side. */
  .pager--next-only a { grid-column: 2; text-align: right; }
}
.pager a:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); }
.pager .pager-title { font-weight: 700; text-transform: uppercase; letter-spacing: -.02em; line-height: 1.05; font-size: clamp(1.125rem, 2.2vw, 1.625rem); }
.pager .label { color: var(--muted); }

/* ==========================================================================
   ABOUT — a stack of sections: a small editorial marker on the left, the
   structured content on the right. One grid, shared by every section, so the
   right column starts on the same line top to bottom — no hairline marks the
   change from one to the next; grid position, spacing and the label
   reappearing in the left rail do that instead. Case studies deliberately do
   NOT use this; see .case-band above.
   ========================================================================== */

/* Running text on these pages is Syne Mono at reading size, not display Syne.
   That is what keeps them compact rather than stretched. Reserved for short
   label-register lists that read as an index, not a paragraph — dates, and
   metadata elsewhere on the site (case labels, captions, the footer). */
.prose-mono {
  font-family: var(--font-mono);
  font-size: clamp(.875rem, .8rem + .3vw, 1.0625rem);
  line-height: 1.75;
  letter-spacing: 0;
}

/* Actual reading copy — About's intro and capability/experience lists, a
   case's Project/Outcome/My Role statements, partner breakdown descriptions
   — sets in Syne at regular weight instead of Syne Mono. Syne Mono stays
   reserved for labels, metadata, dates, navigation, captions and short
   editorial notes; giving it real paragraphs too was what made the type
   system read as more than two fonts. */
.prose {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(.9375rem, .84rem + .3vw, 1.125rem);
  line-height: 1.6;
  letter-spacing: -.005em;
}

.editorial { max-width: 84rem; margin-inline: auto; padding-inline: var(--gutter); }

.about-intro { padding-bottom: clamp(2rem, 4vw, 3rem); }
.about-intro h1 { font-size: clamp(2rem, 5vw, 3.75rem); }

/* No border, no margin between one .sec and the next — they stack directly,
   so the only separation is each one's own padding-block plus its label
   reappearing in the left rail. Every section shares this one grid, so the
   right column begins on the same vertical line the whole way down the
   page — Capabilities included, which used to run its own narrower ratio. */
.sec {
  padding-block: clamp(2rem, 4vw, 3.25rem) clamp(2.25rem, 4.5vw, 3.5rem);
  display: grid; gap: clamp(1.25rem, 2.5vw, 1.75rem);
}
@media (min-width: 860px) {
  .sec { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: clamp(2rem, 5vw, 4rem); }
}
/* Section markers — About, Capabilities, Experience, Brands. Quiet authority
   instead of a size play: Syne Mono, uppercase, tracked, held in place by
   the grid column it sits in rather than by scale. Small enough now that it
   never needs to fit itself to the column — no container query, no
   per-content sizing, just one fixed register shared by every label. */
.sec__label {
  font-family: var(--font-mono); font-weight: 700;
  font-size: clamp(.8125rem, .5vw + .7rem, .9375rem);
  letter-spacing: .1em; text-transform: uppercase; line-height: 1.3;
}
.sec__body > * + * { margin-top: 1rem; }
/* The page's own opening line ("Hi, I'm Jason.") — one step up from the
   section markers so it still reads as the lead, but in the same mono,
   editorial register. Not a hero headline. */
.sec__lead {
  font-family: var(--font-mono); font-weight: 700;
  font-size: clamp(1.0625rem, 1.1vw + .875rem, 1.375rem);
  letter-spacing: .02em; text-transform: uppercase; line-height: 1.35;
  margin-bottom: clamp(1rem, 2vw, 1.5rem);
}

/* ---- the About bio: INTRODUCTION -> STORY -> PAYOFF. "Hi, I'm Jason."
   (.sec__lead) is the only display-style headline in the section; identity,
   thesis, both proof paragraphs and even the punchline's regular half all
   share the SAME `prose` typography (family, weight, size, line-height) —
   nothing here is a second headline. Hierarchy comes only from spacing, plus
   one weight change on the positioning line. No cards, rules, colour or new
   components. The measure is capped so the thesis/punchline lines don't run
   into monster line lengths at the section's full column width. */
.sec--about .sec__body { max-width: 55rem; }

/* Identity — first beat of the story. Plain body copy; importance comes
   from the gap that follows the headline, not from size or weight. */
.sec__lead + .sec__identity { margin-top: clamp(1.5rem, 3vw, 2rem); }

/* The thesis is left to wrap naturally rather than broken into lines by
   hand — forced short lines read as a second headline and fight the page's
   horizontal rhythm. Same body copy as everything around it; a restrained
   gap is what marks it as its own beat. */
.sec__identity + .sec__thesis { margin-top: clamp(1rem, 2vw, 1.5rem); }

/* Proof: the two paragraphs where the reader settles in — one narrative
   section, a normal paragraph gap between them, not a separate beat each. */
.sec__thesis + .sec__proof { margin-top: clamp(1rem, 2vw, 1.5rem); }
.sec__proof p + p { margin-top: 1rem; }

/* The punchline gets a touch more air above it than the gaps elsewhere in
   the section, so it still reads as a resolution rather than another
   paragraph — but restrained, not a dramatic break. Setup and delivery
   inside it: the positioning line steps up to a stronger weight (still the
   same body family and size), the joke stays at regular so it reads as
   delivery, not another thing being sold. */
.sec__proof + .sec__punchline { margin-top: clamp(1.5rem, 3vw, 2rem); }
.sec__punchline-lead { font-weight: 600; }
.sec__punchline-lead + .sec__punchline-tail { margin-top: .75rem; }

/* capabilities — grouped lists, not cards. Experience and Contact reuse this
   same module at two columns; Capabilities itself runs three (.cap-cols--3),
   all on one baseline with identical header treatment and column width. */
.cap-cols { display: grid; gap: clamp(1.75rem, 3vw, 2.5rem); }
@media (min-width: 640px) { .cap-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Three text columns need real width before they split — same call the case
   band makes (see .case-band above): stay stacked past 640px, then past 860px
   too, until 1150px actually has room. Below that, three narrow columns start
   wrapping long phrases mid-word ("Talent Identification + Packaging" is the
   long pole); a single stacked column at that width has no such problem. The
   gap is tighter than the two-column case as well, so the width it frees goes
   to the columns instead. */
@media (min-width: 640px) { .cap-cols--3 { grid-template-columns: 1fr; } }
@media (min-width: 1150px) {
  .cap-cols--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(1rem, 1.4vw, 1.5rem); }
}
/* Category headers (New Business + Growth, etc.) stay Syne Mono metadata —
   unchanged register from the section markers above. */
.cap-col h3 {
  font-family: var(--font-mono); font-size: .6875rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); margin-bottom: .85rem;
}
/* Each heading is also a link into the homepage WORK filter. Identical at
   rest — colour and no underline both already come from the global `a`
   reset — so this only ever reveals itself on interaction. */
.cap-col h3 a {
  display: inline-block; border-bottom: 1px solid transparent;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.cap-col h3 a:hover, .cap-col h3 a:focus-visible {
  color: var(--fg); border-bottom-color: currentColor;
}
.cap-col li { line-height: 1.9; }
/* A three-across list needs a size step down from running-text prose
   (the same move .case-band__col--role makes for its narrower column) so
   the longer phrases clear one line instead of wrapping. */
@media (min-width: 1150px) {
  .cap-cols--3 .cap-col ul { font-size: .8125rem; letter-spacing: -.01em; }
}

/* experience + education share one section */
.cv-rows { display: grid; gap: clamp(1.1rem, 2vw, 1.5rem); }
.cv-row { display: grid; gap: .15rem; }
/* Jobs read as one line — company then dates, set together rather than pushed
   to opposite edges of the column. */
.cv-row--job { display: flex; flex-wrap: wrap; align-items: baseline; gap: .1rem .75rem; }
.cv-row--job .cv-role { flex: 1 0 100%; }
.cv-row .cv-company { font-weight: 700; letter-spacing: -.015em; font-size: 1.0625rem; text-transform: uppercase; }
.cv-row .cv-role, .cv-row .cv-years { color: var(--muted); }


/* brands — a continuous marquee under a full-width label, same small marker
   register as every other section label. */
.sec--brands { display: block; }
.sec--brands .sec__label { margin-bottom: clamp(1.25rem, 2.5vw, 2rem); }
.marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); }
/* The gap lives on the group, not the track. With a gap between the two
   groups instead, translating -50% lands half a gap short and the loop jumps.
   Giving each group a trailing margin makes the repeating unit exactly half
   the track, so the seam is invisible. */
.marquee__track {
  display: flex; width: max-content; gap: 0;
  animation: marquee-scroll 48s linear infinite;
}
.marquee:hover .marquee__track, .marquee:focus-within .marquee__track { animation-play-state: paused; }
.marquee__item {
  font-weight: 800; text-transform: uppercase; letter-spacing: -.02em; white-space: nowrap;
  font-size: clamp(1.125rem, 2.2vw, 1.75rem);
  color: color-mix(in srgb, var(--fg) 38%, transparent);
}

/* Logos are painted as a mask, so the colour inside the source file is
   irrelevant — a white wordmark and a magenta one both come out the same ink,
   and they invert automatically on a dark surface. --m and the box size are
   set per logo from assets/logos/logos.json. */
.marquee__logo {
  flex: 0 0 auto;
  background-color: color-mix(in srgb, var(--fg) 72%, transparent);
  -webkit-mask-size: contain;   mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  transition: background-color .3s var(--ease);
}
.marquee:hover .marquee__logo { background-color: var(--fg); }
.marquee__group { align-items: center; }
@keyframes marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .marquee { mask-image: none; -webkit-mask-image: none; }
  .marquee__track { animation: none; width: auto; flex-wrap: wrap; gap: .75rem 2rem; }
  .marquee__group + .marquee__group { display: none; }
}
.marquee__group {
  display: flex; gap: clamp(2rem, 5vw, 4rem);
  margin-right: clamp(2rem, 5vw, 4rem);
}

/* footer contact links — run on one line, mirroring the footer lines */
.contact-links a {
  display: inline-flex; align-items: baseline; gap: .45rem;
  font-family: var(--font-mono); font-size: .75rem; letter-spacing: .12em; text-transform: uppercase;
  padding-bottom: .35rem; border-bottom: 1px solid var(--rule);
  transition: border-color .25s var(--ease), gap .25s var(--ease);
}
.contact-links a:hover { border-bottom-color: currentColor; gap: .8rem; }

/* --- entrance ----------------------------------------------------------- */
.js .reveal { opacity: 0; transform: translateY(14px); }
.js .reveal.is-in { opacity: 1; transform: none; transition: opacity .7s var(--ease), transform .7s var(--ease); }
@media (prefers-reduced-motion: reduce) { .js .reveal { opacity: 1; transform: none; } }
