/* ==========================================================================
   grossimatte.com — site styles
   Palette and type lifted from the original Wix build:
     page bg        #1A1A1A
     header bg      #262928
     accent red     #FF4242 / #FF4040
     body text      #DBDBDB
     muted text     #6B6B6B
     headings       Titillium Web Bold (was a custom-uploaded Titillium on Wix)
   ========================================================================== */

/* Self-hosted rather than pulled from Google Fonts: that was a render-blocking
   stylesheet plus DNS + TLS to two external hosts before any text could paint.
   These two files are 24KB combined and cached for a year by _headers.
   Only 400 and 700 are shipped - the stylesheet never asks for anything else. */
@font-face {
  font-family: 'Titillium Web';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/titillium-web-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Titillium Web';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/titillium-web-700.woff2') format('woff2');
}

:root {
  --bg:            #141414;
  --bg-header:     #1c1e1e;
  --bg-elev:       #202222;
  --accent:        #ff4242;
  --accent-soft:   #ff5a5a;
  --text:          #d6d6d6;
  --text-muted:    #7d7d7d;
  --text-bright:   #ffffff;
  --rule:          #ff4242;
  --maxw:          980px;
  --header-h:      63px;
  --font:          'Titillium Web', 'Trebuchet MS', Arial, Helvetica, sans-serif;

  /* hairlines and lifted surfaces - tiles and cards need to read as objects
     sitting on the page, which a flat #1a1a1a on #1a1a1a never did */
  --line:          rgba(255, 255, 255, .09);
  --line-strong:   rgba(255, 255, 255, .18);

  /* one easing curve everywhere, so nothing feels like it came from a
     different site. Slight overshoot-free ease-out; two speeds only. */
  --ease:          cubic-bezier(.2, .6, .2, 1);
  --t-fast:        .2s;
  --t-slow:        .45s;

  /* Type scale. Six steps, and every non-heading size in the stylesheet maps
     to one of them - there were eleven ad-hoc values (10, 11, 11.5, 12, 12.5,
     13, 14, 15...) doing barely distinguishable jobs. Headings are deliberately
     NOT on this scale; they set their own fluid sizes. */
  --fs-micro:      10px;   /* tracked caps: scroll cue, stat labels          */
  --fs-caption:    11px;   /* brand tagline, small section labels            */
  --fs-meta:       12px;   /* tile labels, roles, footer, back links         */
  --fs-ui:         14px;   /* nav, buttons, form fields                      */
  --fs-body:       clamp(16px, .55vw + 14.2px, 19px);
  --fs-lead:       clamp(17px, .8vw + 14.5px, 21px);
}

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

html { scroll-behavior: smooth; scroll-padding-top: var(--header-h); }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

::selection { background: var(--accent); color: #fff; }

/* Dark scrollbar - the default light one was a bright stripe down the side of
   an otherwise black page. */
* { scrollbar-color: #3a3a3a transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #333; border: 2px solid var(--bg); border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: #464646; }

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

/* Sections fade up as they arrive. JS adds .is-in; without JS, or with reduced
   motion, the fallback below leaves everything visible. */
[data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
[data-reveal].is-in { opacity: 1; transform: none; }

.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 20px; }

/* Available to screen readers and to crawlers, off-screen for everyone else.
   Used for the page h1 where the visible title is an image or the reel. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--accent); color: #fff; padding: 10px 16px; font-weight: 700;
}
.skip-link:focus { left: 0; }

/* ---------- header ------------------------------------------------------ */

/* Translucent + blurred rather than a solid bar with a heavy drop shadow:
   the showreel scrolling underneath now reads through it. Falls back to the
   opaque colour where backdrop-filter isn't supported. */
.site-header {
  position: sticky; top: 0; z-index: 100;
  background: var(--bg-header);
  border-bottom: 1px solid var(--line);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .site-header {
    background: rgba(20, 22, 22, .72);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
  }
}

.site-header__inner {
  max-width: 1200px; margin: 0 auto; padding: 0 16px;
  min-height: var(--header-h);
  display: flex; align-items: center; gap: 16px;
}

.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; margin-right: auto; }

/* The MG monogram, same mark as the favicon. Replaced the LinkedIn avatar that
   Wix used here - a third-party logo is not branding. */
.brand__mark {
  width: 39px; height: 39px; flex: 0 0 39px;
  display: block;
  transition: transform var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease);
}
.brand:hover .brand__mark { transform: translateY(-1px) scale(1.04); }
.brand__mark img { width: 100%; height: 100%; }

.brand__text { display: flex; flex-direction: column; justify-content: center; }

.brand__name {
  margin: 0;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: 700;
  font-size: 27px;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--accent);
  white-space: nowrap;
}

/* The tagline was the same shouty weight as the name. Dropped to a small
   tracked-out caption so the name is clearly the primary mark. */
.brand__tagline {
  margin: 4px 0 0;
  font-family: var(--font);
  font-weight: 700;
  font-size: var(--fs-caption);
  line-height: 1;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}

.nav { display: flex; align-items: center; gap: 30px; }

.nav a {
  position: relative;
  font-size: var(--fs-ui); font-weight: 700; letter-spacing: .13em; text-transform: uppercase;
  color: var(--text-muted); text-decoration: none;
  padding: 6px 0;
  transition: color var(--t-fast) var(--ease);
}
/* underline wipes out from the centre instead of the colour just fading */
.nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px; background: var(--accent);
  transform: scaleX(0); transform-origin: 50% 50%;
  transition: transform var(--t-slow) var(--ease);
}
.nav a:hover,
.nav a:focus-visible { color: var(--text-bright); }
.nav a:hover::after { transform: scaleX(1); }
.nav a[aria-current="page"] { color: var(--text-bright); }
.nav a[aria-current="page"]::after { transform: scaleX(1); background: var(--accent); }

/* The hire-me button. It sits outside .nav on purpose: the nav collapses into
   the hamburger below 860px, and the one link that is meant to be impossible
   to miss cannot be the one hidden behind a menu. Smaller than a .btn in the
   page body - it has to live inside a 66px bar without crowding the brand. */
.btn.header__cta {
  flex: none;
  padding: 10px 22px;
  font-size: var(--fs-meta); letter-spacing: .12em;
}

.nav-toggle {
  display: none;
  background: none; border: 0; padding: 8px; cursor: pointer;
  color: var(--text-muted);
}
.nav-toggle svg { display: block; }

/* ---------- hero -------------------------------------------------------- */

.hero {
  position: relative;
  height: clamp(420px, 100svh, 1022px);
  background: #242323;
  overflow: hidden;
}

.hero__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* The video used to stop dead at a hard horizontal edge. This scrim dissolves
   it into the page colour so the reel bleeds into Client Work. */
.hero::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 34%;
  background: linear-gradient(to top, var(--bg) 0%, rgba(20, 20, 20, .55) 45%, transparent 100%);
  pointer-events: none;
}

/* One cue, used at the foot of the hero and of every full-height panel, so it
   is always obvious there is another section below. */
.scroll-cue {
  position: absolute; left: 50%; bottom: 22px; z-index: 3;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  color: rgba(255, 255, 255, .45);
  text-decoration: none; font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: .26em; text-transform: uppercase; white-space: nowrap;
  transition: color var(--t-fast) var(--ease);
}
.scroll-cue::after {
  content: ""; width: 1px; height: 30px;
  background: linear-gradient(to bottom, var(--accent), transparent);
  animation: scroll-cue 2.4s var(--ease) infinite;
}
.scroll-cue:hover { color: #fff; }
.hero__scroll { bottom: 26px; color: rgba(255, 255, 255, .6); }
.hero__scroll::after { height: 34px; }
@keyframes scroll-cue {
  0%   { opacity: 0; transform: scaleY(.4); transform-origin: top; }
  40%  { opacity: 1; transform: scaleY(1);  transform-origin: top; }
  100% { opacity: 0; transform: scaleY(1);  transform-origin: bottom; }
}


/* Same cue at the foot of the last panel, pointing the other way: the line
   sits above the label and the light travels upward, so it reads as "back"
   rather than "more below". */
.scroll-cue--up { flex-direction: column-reverse; }
.scroll-cue--up::after {
  background: linear-gradient(to top, var(--accent), transparent);
  animation-name: scroll-cue-up;
}
@keyframes scroll-cue-up {
  0%   { opacity: 0; transform: scaleY(.4); transform-origin: bottom; }
  40%  { opacity: 1; transform: scaleY(1);  transform-origin: bottom; }
  100% { opacity: 0; transform: scaleY(1);  transform-origin: top; }
}
/* ---------- section headings ------------------------------------------- */

/* relative so the scroll cue can sit against the foot of the panel */
.section { position: relative; padding: 56px 0 64px; }

/* One line of context under a section heading. */
.section-intro {
  max-width: 62ch; margin: -14px auto 30px; text-align: center;
  font-size: var(--fs-body); color: var(--text-muted);
}

/* Was two solid 8px red slabs running to the edges. Now hairlines that fade
   out as they travel away from the title, so the eye lands on the word. */
.section-heading {
  display: flex; align-items: center; gap: clamp(16px, 3vw, 34px);
  margin: 0 0 clamp(28px, 4vh, 48px);
}
.section-heading::before,
.section-heading::after {
  content: ""; flex: 1 1 0; height: 1px; min-width: 24px;
  background: linear-gradient(to right, transparent, var(--line-strong));
}
.section-heading::after { background: linear-gradient(to left, transparent, var(--line-strong)); }
.section-heading h2 {
  position: relative;
  margin: 0; flex: 0 1 auto; text-align: center;
  font-size: clamp(26px, 4.4vw, 46px);
  font-weight: 700; line-height: 1.05;
  letter-spacing: .01em;
  color: var(--text-bright);
  text-transform: uppercase;
  padding-bottom: 12px;
}
/* short accent rule under the title carries the red instead of the slabs */
.section-heading h2::after {
  content: ""; position: absolute; left: 50%; bottom: 0;
  width: 46px; height: 3px; margin-left: -23px;
  background: var(--accent);
}

/* ---------- work grid --------------------------------------------------- */

.grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px;
  justify-items: center;
}
/* Dim the rest of the grid while one tile is hovered, so the hovered poster
   is the only fully-lit thing on screen. Pointer-only: on touch there is no
   hover, and :has() degrades to nothing where unsupported. */
@media (hover: hover) {
  .grid:has(.tile:hover) .tile:not(:hover) { opacity: .55; }
  .grid .tile { transition: transform var(--t-slow) var(--ease), box-shadow var(--t-slow) var(--ease), opacity var(--t-slow) var(--ease); }
}
.grid--personal { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 700px; margin: 0 auto; }

.tile {
  position: relative; display: block;
  width: 100%; max-width: 214px; aspect-ratio: 214 / 287;
  overflow: hidden; text-decoration: none;
  /* matches the colour the posters were flattened onto, so the placeholder
     and any anti-aliased poster edge are the same tone */
  background: var(--bg-elev);
  border-radius: 3px;
  box-shadow: 0 0 0 1px var(--line);
  transition: transform var(--t-slow) var(--ease), box-shadow var(--t-slow) var(--ease);
  /* stops the scaled poster flickering over the rounded corner in Safari */
  isolation: isolate;
}
.tile img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}

/* A red edge-light that wipes up the poster on hover - reads as a cinematic
   key light rather than a border, and ties the grid to the accent. */
.tile::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: var(--accent); z-index: 3;
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--t-slow) var(--ease);
}
.tile:hover, .tile:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 0 0 1px var(--line-strong), 0 14px 30px -10px rgba(0, 0, 0, .75);
}
.tile:hover::after,
.tile:focus-visible::after { transform: scaleX(1); }
.tile__hover { opacity: 0; }
.tile:hover .tile__hover,
.tile:focus-visible .tile__hover { opacity: 1; }
/* Only cross-fade where there is hover art to fade *to* - four projects have
   no second poster, and fading their base out just turned the tile black. */
.tile--swap:hover .tile__base,
.tile--swap:focus-visible .tile__base { opacity: 0; }
.tile:hover img,
.tile:focus-visible img { transform: scale(1.04); }
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Label slides up out of the scrim rather than just appearing. Left-aligned
   and tracked out - centred bold caps in a black box was the Wix look. */
.tile__label {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 40px 12px 13px;
  background: linear-gradient(to top, rgba(0, 0, 0, .92) 15%, rgba(0, 0, 0, .55) 55%, transparent);
  color: #fff; font-size: var(--fs-meta); font-weight: 700; letter-spacing: .11em;
  line-height: 1.25;
  text-transform: uppercase; text-align: left;
  opacity: 0; transform: translateY(8px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.tile:hover .tile__label,
.tile:focus-visible .tile__label { opacity: 1; transform: none; }

/* ---------- personal work split ---------------------------------------- */

/* The About panel's two-column split, mirrored: posters left, copy right.
   .split__media comes first in the markup so the stacked mobile order is
   thumbnails-then-text, which is the way round that reads. */
.split {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 5vw, 56px); align-items: center;
}
.split__text p { margin: 0 0 1.05em; font-size: var(--fs-lead); color: var(--text); }
.split__text p:first-child { color: var(--text-bright); font-size: calc(var(--fs-lead) * 1.1); }
.split__text p:last-child { margin-bottom: 0; }
/* the grid's own centring margin would fight the column it now lives in */
.split__media .grid--personal { max-width: none; margin: 0; }

/* ---------- about ------------------------------------------------------- */

.about__portrait {
  width: 128px; height: 128px; border-radius: 50%;
  object-fit: cover; margin: 0 auto 26px;
  box-shadow: 0 0 0 1px var(--line-strong), 0 10px 30px -12px rgba(0, 0, 0, .8);
}

.about__body {
  display: grid; grid-template-columns: 1fr 1fr; gap: clamp(32px, 5vw, 56px); align-items: start;
}
.about__text p { margin: 0 0 1.05em; font-size: var(--fs-lead); color: var(--text); }
.about__text p:first-child { color: var(--text-bright); font-size: calc(var(--fs-lead) * 1.1); }
.about__text p:last-child { margin-bottom: 0; }

/* Three figures under the About copy. dt/dd are markup order (label, value);
   column-reverse puts the number on top where it belongs. */
.stats {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px;
  margin: 30px 0 0; padding: 24px 0 0;
  border-top: 1px solid var(--line);
}
.stats > div { display: flex; flex-direction: column-reverse; gap: 6px; }
.stats dd {
  margin: 0;
  font-size: clamp(24px, 2.6vw, 34px); font-weight: 700; line-height: 1;
  letter-spacing: -.02em; color: var(--text-bright);
}
.stats dt {
  font-size: var(--fs-micro); font-weight: 700; letter-spacing: .18em;
  text-transform: uppercase; color: var(--text-muted);
}

/* Logos were full-brightness colour marks fighting the posters for attention.
   Knocked back to a uniform grey that lifts on hover - reads as a credit list,
   not a sponsor wall. */
.clients {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 30px 24px; align-items: center; justify-items: center;
}
.clients img {
  max-height: 74px; width: auto; object-fit: contain;
  opacity: .5; filter: grayscale(1);
  transition: opacity var(--t-slow) var(--ease), filter var(--t-slow) var(--ease);
}
.clients:hover img { opacity: .32; }
.clients img:hover { opacity: 1; filter: none; }

/* ---------- contact ----------------------------------------------------- */

/* The pitch that used to be the /hire-me-freelance page, now sitting beside
   the form it always pointed at. Two columns rather than a stack: this is a
   full-height panel, and the copy would otherwise push SEND off the fold. */
.contact {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 5vw, 60px); align-items: center;
  max-width: 1060px; margin: 0 auto;
}

.contact__pitch p { margin: 0 0 1.05em; font-size: var(--fs-body); color: var(--text); }
.contact__pitch p:last-child { margin-bottom: 0; }

/* The lead line. Bigger and brighter than the copy under it so the eye lands
   here first and the paragraphs read as the detail behind it. It carried a
   short red rule under it while it was a shouted headline; as a sentence that
   read like a heading pretending to be one, so the rule came out. */
.contact__pitch p.contact__cta {
  font-size: clamp(20px, 2.3vw, 30px);
  font-weight: 700; line-height: 1.25; letter-spacing: .01em;
  color: var(--text-bright);
  margin: 0 0 26px;
}

.contact-form { max-width: 558px; margin: 0 auto; }
.contact-form__row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

.contact-form input,
.contact-form textarea {
  width: 100%;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--text);
  font-family: inherit; font-size: var(--fs-ui);
  padding: 14px 14px;
  margin-bottom: 16px;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.contact-form textarea { min-height: 126px; resize: vertical; }
.contact-form input:hover,
.contact-form textarea:hover { border-color: var(--line-strong); }
.contact-form input:focus,
.contact-form textarea:focus {
  outline: none; background: #262828;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 66, 66, .16);
}
.contact-form ::placeholder { color: #6e6e6e; }

.contact-form__actions { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }

.btn {
  display: inline-block; position: relative; overflow: hidden;
  background: var(--accent); color: #fff;
  border: 0; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: var(--fs-ui); font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase;
  padding: 15px 40px; text-decoration: none;
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.btn:hover {
  background: #ff5a5a;
  transform: translateY(-2px);
  box-shadow: 0 10px 22px -10px rgba(255, 66, 66, .85);
}
.btn:active { transform: translateY(0); }

.form-note { font-size: var(--fs-meta); color: var(--text-muted); margin: 0; }
.form-note--ok { color: #7bd67b; }
.form-note--err { color: var(--accent); }

/* ---------- project pages ---------------------------------------------- */

.project { position: relative; min-height: calc(100svh - var(--header-h)); padding: 48px 0 80px; }

.project__bg {
  position: fixed; inset: 0; z-index: -1;
  background-size: cover; background-position: center; background-repeat: no-repeat;
}
/* Vignette over the fixed key art. Several backgrounds ship at opacity 1 and
   the body copy was sitting straight on top of them. */
.project__bg::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 50% 35%, transparent 20%, rgba(10, 10, 10, .72) 100%),
    linear-gradient(to bottom, rgba(10, 10, 10, .45), rgba(10, 10, 10, .25) 40%, rgba(10, 10, 10, .8));
}

.project__title { margin: 0 auto 30px; display: block; }
.project__title img { margin: 0 auto; }

.video-frame {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  margin: 0 auto 38px; background: #000;
  border-radius: 4px; overflow: hidden;
  box-shadow: 0 0 0 1px var(--line), 0 30px 60px -25px rgba(0, 0, 0, .95);
}
.video-frame iframe,
.video-frame video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* Was accent-red body-size text; now a small tracked-out credit line so it
   reads as metadata and the paragraph below is the thing you read. */
.project__role {
  margin: 0 0 20px; text-align: center;
  font-size: var(--fs-meta); font-weight: 700; letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--accent-soft);
}
.project__body { text-align: center; max-width: 760px; margin: 0 auto; }
.project__body p { margin: 0 0 1.1em; font-size: var(--fs-lead); color: var(--text); }
.project__body p:last-child { margin-bottom: 0; }
.project__body a { color: var(--accent-soft); }

.project__clips { display: grid; gap: 24px; margin-top: 44px; }
.project__clips video { width: 100%; height: auto; display: block; background: #000; }

.back-link {
  display: inline-flex; align-items: center; gap: 10px; margin-top: 52px;
  font-size: var(--fs-meta); font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-muted); text-decoration: none;
  transition: color var(--t-fast) var(--ease);
}
.back-link::before {
  content: ""; width: 22px; height: 1px; background: currentColor;
  transition: width var(--t-slow) var(--ease);
}
.back-link:hover { color: var(--accent); }
.back-link:hover::before { width: 34px; }

/* ---------- simple text pages (hire me, blog, 404) ---------------------- */

.page { padding: 64px 0 80px; }
.page h1 {
  margin: 0 0 34px; text-align: center; text-transform: uppercase;
  font-size: clamp(28px, 4.6vw, 46px); font-weight: 700; line-height: 1.05;
  color: var(--text-bright);
}
.page__body { max-width: 640px; margin: 0 auto 52px; text-align: center; }
.page__body p { margin: 0 0 1.1em; font-size: var(--fs-lead); }

.post-list { display: grid; gap: 28px; max-width: 760px; margin: 0 auto; list-style: none; padding: 0; }
.post-list a { text-decoration: none; display: block; padding: 22px 24px; border: 1px solid #333; transition: border-color .25s ease; }
.post-list a:hover { border-color: var(--accent); }
.post-list h2 { margin: 0 0 6px; font-size: 24px; color: var(--text-bright); }
.post-list time { font-size: var(--fs-meta); color: var(--text-muted); }

/* ---------- footer ------------------------------------------------------ */

.site-footer {
  border-top: 1px solid var(--line);
  padding: 30px 0 40px;
  font-size: var(--fs-meta); letter-spacing: .02em; color: var(--text-muted);
}
.site-footer__inner { display: flex; flex-wrap: wrap; gap: 12px 26px; align-items: center; justify-content: space-between; }
.site-footer a {
  color: var(--text-muted); text-decoration: none;
  transition: color var(--t-fast) var(--ease);
}
.site-footer a:hover { color: var(--text-bright); }

/* The last ask on the page. Muted like its neighbours it read as small print;
   in the accent it stays a call to action without becoming a second button. */
.site-footer a.site-footer__cta { color: var(--accent); font-weight: 700; }
.site-footer a.site-footer__cta:hover { color: #ff7a7a; }

/* ---------- responsive -------------------------------------------------- */

@media (max-width: 900px) {
  .about__body,
  .split,
  .contact { grid-template-columns: 1fr; gap: 44px; }
  .about__text,
  .split__text { text-align: center; }
  /* stacked, the pitch is centred like every other single column */
  .contact__pitch { text-align: center; }
  .split__media .grid--personal { max-width: 700px; margin: 0 auto; }
  .clients { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 860px) {
  .nav-toggle { display: block; }
  .nav {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--bg-header);
    box-shadow: 0 12px 18px rgba(0, 0, 0, .5);
    display: none;
  }
  .nav[data-open="true"] { display: flex; }
  .nav a { padding: 16px 20px; border-top: 1px solid #333; }
  .site-header__inner { position: relative; flex-wrap: nowrap; }
  /* DOM order is toggle, nav, button; on a phone the button has to come
     first, so it reads brand - HIRE ME - menu rather than hiding behind it */
  .btn.header__cta { order: 1; padding: 9px 16px; letter-spacing: .08em; }
  .nav-toggle { order: 2; }
  .brand__name { font-size: 24px; }
  .brand__tagline { font-size: var(--fs-caption); }
}

@media (max-width: 700px) {
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
  .grid--personal { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .contact-form__row { grid-template-columns: 1fr; gap: 0; }
  .section-heading { gap: 14px; }
}

/* The header carries brand + button + hamburger on a phone, and at the sizes
   they had that came to 432px on a 375px screen: the hamburger fell off the
   right edge and took the page into horizontal scroll. Everything in the row
   gives up a little so all three fit. */
@media (max-width: 480px) {
  .site-header__inner { gap: 10px; padding: 0 12px; }
  .brand { gap: 9px; min-width: 0; }
  .brand__name { font-size: 19px; }
  .btn.header__cta { padding: 8px 12px; font-size: var(--fs-caption); letter-spacing: .06em; }
  .nav-toggle { padding: 6px; }
}

@media (max-width: 420px) {
  .brand__tagline { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  /* nothing may start life invisible when motion is off - the reveal would
     never visibly resolve */
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  .tile:hover, .tile:focus-visible { transform: none; }
}

/* ---------- earlier-work list (pages with no homepage thumbnail) --------- */

.archive { margin-top: 44px; text-align: center; }
.archive h3 {
  margin: 0 0 16px;
  font-size: var(--fs-caption); font-weight: 700; letter-spacing: .24em; text-transform: uppercase;
  color: var(--text-muted);
}
.archive ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;
}
/* Pills rather than an underlined text run - stops the list reading as a
   footer afterthought while keeping it visibly secondary to the grid. */
.archive a {
  display: inline-block; padding: 7px 15px; border-radius: 100px;
  font-size: var(--fs-meta); font-weight: 700; letter-spacing: .06em;
  color: #9a9a9a; text-decoration: none;
  border: 1px solid var(--line);
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.archive a:hover { color: var(--text-bright); border-color: var(--accent); background: rgba(255, 66, 66, .1); }

/* ==========================================================================
   Full-viewport sections
   --------------------------------------------------------------------------
   Each panel is exactly one screen tall (minus the sticky header) and scales
   its own contents to fit, so nothing is cut off and nothing needs scrolling
   within a section.

   The trick: a .fit wrapper takes the leftover vertical space via flex, is
   declared a size container, and the media inside is sized off `cqh` — the
   wrapper's resolved height. So poster tiles and video frames derive their
   size from however much room is actually left, not from a fixed guess.

   Only applied when there's genuinely room for it. On phones and short
   windows the site falls back to ordinary scrolling flow, because a 4x3
   poster grid squeezed into 600px of height is unreadable.
   ========================================================================== */

@media (min-width: 760px) and (min-height: 560px) {

  :root { --fit-h: calc(100svh - var(--header-h)); }

  /* The same six steps as the base scale, but height-fluid, because inside a
     fixed-height panel type has to give ground as the window shortens. Each
     step tops out at exactly its base value, so the two scales agree on a
     tall window and only diverge when there genuinely isn't room.
     Scoped to the panels, NOT :root - the header and footer sit outside the
     fit system and have no business shrinking with viewport height. */
  .section--fit, .project--fit, .page--fit {
    --fs-micro:   clamp(8px,  1.2vh,  10px);
    --fs-caption: clamp(9px,  1.35vh, 11px);
    --fs-meta:    clamp(10px, 1.5vh,  12px);
    --fs-ui:      clamp(11px, 1.75vh, 14px);
    --fs-body:    clamp(12px, 1.95vh, 19px);
    --fs-lead:    clamp(13px, 2.1vh,  21px);
  }

  .hero { height: var(--fit-h); }

  .section--fit,
  .project--fit {
    height: var(--fit-h);
    min-height: fit-content;      /* let a panel grow rather than clip */
    padding: 2.5vh 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* Single-panel pages - a project page, or anything in site.json's "pages"
     list - are meant to be taken in at a glance: header, one panel, footer,
     nothing to scroll. The homepage is the opposite, a stack of full-height
     panels you scroll through, so only these pages subtract the footer.

     --fit-h is 100svh minus the header and nothing else, which is right for
     the homepage and wrong here: it left the footer hanging 91px below the
     fold on every single project page. Done with flex rather than a hardcoded
     --footer-h, because the footer wraps to two rows on a narrow window and
     any fixed number would be wrong there. */
  body:has(.project--fit),
  body:has(.page--fit) {
    min-height: 100svh;
    display: flex;
    flex-direction: column;
  }

  body:has(.project--fit) > main,
  body:has(.page--fit) > main {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
  }

  body:has(.project--fit) > .site-footer,
  body:has(.page--fit) > .site-footer { flex: none; }

  .project--fit,
  .page--fit {
    height: auto;
    flex: 1 1 auto;   /* exactly the room left between header and footer */
  }

  .section--fit > .wrap,
  .project--fit > .wrap {
    display: flex; flex-direction: column;
    flex: 1 1 auto; min-height: 0; width: 100%;
    /* matters only for panels with no elastic child of their own, e.g. a
       project page with no trailer - otherwise .fit absorbs the slack */
    justify-content: center;
  }

  /* Panels that carry a scroll cue reserve room for it, so it can never sit
     on top of the content. Panels without one (contact, the last) don't. */
  /* 22px offset + 55px of cue + breathing room. Measured, not guessed - at
     56px the cue sat on top of the Earlier-work row. */
  .section--fit:has(.scroll-cue) { padding-bottom: 86px; }

  /* headings shrink with the viewport instead of eating the panel */
  .section--fit .section-heading { margin-bottom: 2.4vh; flex: none; }
  .section--fit .section-heading h2 { font-size: clamp(24px, 4.6vh, 46px); }
  .section--fit .section-intro { margin-bottom: 2.2vh; flex: none; }

  /* the elastic middle */
  .fit {
    flex: 1 1 0; min-height: 0;
    container-type: size;
    display: flex; align-items: center; justify-content: center;
  }
  .fit > * { width: 100%; }

  /* --- poster grids ----------------------------------------------------- */

  .fit .grid,
  .split__media .grid { --gap: clamp(6px, 1vh, 11px); gap: var(--gap); }

  .fit .tile,
  .split__media .tile {
    max-width: min(
      var(--tile-max, 214px),
      calc((100cqh - (var(--rows) - 1) * var(--gap)) / var(--rows) * 214 / 287)
    );
  }

  /* Sixteen posters now the four older projects have thumbnails. In a 980px
     column that would mean 4x4 at ~98px each - unreadable. Two rows of eight
     across a much wider column instead: the tiles come out LARGER than the
     old twelve did at 4x3, because height, not width, was always the binding
     constraint here. */
  #work > .wrap { max-width: min(96vw, 1560px); }
  .grid--client { grid-template-columns: repeat(8, minmax(0, 1fr)); --rows: 2; }

  /* One row, so there is height to spare - but these are 214px source art and
     pushing them past ~250px starts to look soft. */
  .grid--personal { --rows: 1; --tile-max: 246px; }

  /* Short-and-wide windows (most laptops): the posters get squeezed hardest
     here, so give the column the rest of the screen. */
  @media (max-height: 820px) and (min-width: 1100px) {
    .section--fit .wrap { max-width: 1200px; }
    #work > .wrap { max-width: 98vw; }
  }

  /* --- earlier-work list ------------------------------------------------ */

  .section--fit .archive { margin-top: 2.6vh; flex: none; }
  .section--fit .archive h3 { margin-bottom: 1.2vh; font-size: var(--fs-caption); }
  .section--fit .archive a { font-size: var(--fs-meta); }

  /* --- personal work ---------------------------------------------------- */

  /* Same job .fit does on the other panels: take the leftover height and
     become the sizing container the tiles measure themselves against. */
  .section--fit .split {
    flex: 1 1 0; min-height: 0;
    container-type: size;
    align-items: center;
    gap: clamp(20px, 3.4vw, 48px);
  }
  .section--fit .split__text p { font-size: var(--fs-lead); margin-bottom: .9em; }

  /* Wider than the About panel gets: three portrait posters squeezed into half
     of a 940px wrap came out 143px across, well under their 214px source. A
     wider wrap and a 1.3:1 split put them back near native size.

     Nested so it cannot beat the stacking rule further up the file - below
     901px this panel goes one column, exactly like About. */
  @media (min-width: 901px) {
    #personal > .wrap { max-width: min(94vw, 1180px); }
    .section--fit .split { grid-template-columns: 1.3fr 1fr; }
  }

  /* Stacked, the posters would measure themselves against the whole panel and
     blow past it - 229px tiles and 39px of overflow at 760x560. Give the row
     the leftover height and make it the container the tiles measure instead. */
  @media (max-width: 900px) {
    .section--fit .split { grid-template-rows: minmax(0, 1fr) auto; }
    .section--fit .split__media {
      align-self: stretch; min-height: 0; height: 100%;
      container-type: size;
      display: flex; align-items: center; justify-content: center;
    }
    .section--fit .split__media .grid { width: 100%; }
  }

  /* --- about ------------------------------------------------------------ */

  .section--fit .about__portrait {
    width: clamp(80px, 13vh, 136px); height: auto; aspect-ratio: 1;
    margin-bottom: 2vh; flex: none;
  }

  .section--fit .about__body {
    flex: 1 1 0; min-height: 0;
    container-type: size;
    align-items: center;
    gap: clamp(20px, 3.4vw, 48px);
  }
  .section--fit .about__text p {
    font-size: var(--fs-lead);
    margin-bottom: .9em;
  }
  /* the stats have to give ground with everything else, or they push the
     About panel past one screen on a laptop */
  .section--fit .stats {
    margin-top: 1.8vh; padding-top: 1.6vh;
    gap: clamp(8px, 1.4vw, 18px);
  }
  .section--fit .stats dd { font-size: clamp(17px, 3vh, 34px); }
  .section--fit .stats dt { font-size: var(--fs-micro); }
  .section--fit .clients {
    --logo-gap: clamp(8px, 2.4vh, 34px);
    gap: var(--logo-gap) 24px;
  }
  .section--fit .clients img {
    max-height: min(92px, calc((100cqh - 3 * var(--logo-gap)) / 4));
  }

  /* --- contact ---------------------------------------------------------- */

  /* The pitch column shrinks with the panel the same way the form does -
     otherwise the copy merged in from the old hire-me page is the one thing
     in here still sized in fixed pixels, and it is what pushes SEND under
     the fold on a short window. */
  .section--fit .contact { align-items: center; }
  .section--fit .contact__pitch p.contact__cta {
    font-size: clamp(15px, 3.1vh, 30px);
    margin-bottom: clamp(10px, 2vh, 26px);
  }
  .section--fit .contact__pitch p { margin-bottom: clamp(6px, 1.2vh, 17px); }

  /* .page--fit is kept alive for any single-panel page added back to
     content/site.json - it carried the two hire-me pages until 2026-09, whose
     textarea kept its fixed 126px until these selectors picked it up. */
  .section--fit .contact-form,
  .page--fit .contact-form { width: 100%; }
  .section--fit .contact-form input,
  .section--fit .contact-form textarea,
  .page--fit .contact-form input,
  .page--fit .contact-form textarea {
    padding: clamp(8px, 1.5vh, 14px) 12px;
    margin-bottom: clamp(8px, 1.6vh, 16px);
    font-size: var(--fs-ui);
  }
  .section--fit .contact-form textarea { min-height: clamp(80px, 22vh, 220px); }
  .page--fit .contact-form textarea { min-height: clamp(64px, 14vh, 180px); }
  .section--fit .btn,
  .page--fit .btn { padding: clamp(9px, 1.6vh, 15px) clamp(24px, 3vw, 42px); }

  /* --- project pages ---------------------------------------------------- */

  .project--fit { padding-top: 2.5vh; }

  /* Title art sits in a fixed-height slot rather than taking whatever height
     its own aspect ratio gives it. Logos are all sorts of shapes - the Star
     Wars lockup ran 135px tall while Little Hope's ran 72px - and every pixel
     of that difference came straight out of the trailer below it. The slot is
     as tall as the tallest logo already was, so no artwork got smaller; the
     short ones just centre in it. */
  .project--fit .project__title {
    margin: 0 0 2vh; flex: none;
    height: clamp(48px, 15vh, 220px);
    display: flex; align-items: center; justify-content: center;
  }
  .project--fit .project__title img {
    max-height: 100%;
    width: auto;
  }

  /* Same idea for the copy: a slot sized for the longest body on the site,
     so a short paragraph and a long one leave the trailer exactly the same
     room. Six lines is the worst case measured across every page at every
     window size tested - past that the text grows the slot and that page's
     trailer would shrink, which is the inconsistency this is here to stop.
     Expressed against --fs-body so it tracks the type scale. */
  .project--fit .project__body {
    flex: none;
    min-height: calc(5 * 1.55 * var(--fs-lead));
    /* Column width in ems, not the 760px the base rule uses, so it widens as
       the type does. With a fixed column the same sentence wrapped to 4 lines
       on a 720px-tall window and 6 on a 1080px one, which moved the slot and
       so moved the trailer. In ems the characters per line stay put and the
       line count with them. */
    max-width: calc(41 * var(--fs-lead));
  }

  /* With both slots fixed, the trailer simply takes the rest - which is now
     the same amount on every project page, and as much as the wordiest page
     can spare. */
  .video-fit {
    flex: 1 1 0; min-height: 0;   /* basis 0, not auto: container-type size makes the childs height depend on this one, so an auto basis resolves to zero */
    container-type: size;
    display: flex; align-items: center; justify-content: center;
  }
  .video-fit .video-frame {
    width: min(100%, calc(100cqh * 16 / 9));
    margin: 0 auto;
  }

  /* Two trailers (Borderlands 3) sit side by side rather than stacked, so the
     page still reads in one view. Each frame is capped both by half the width
     and by the height the container has left, whichever bites first. */
  .video-fit--pair { gap: clamp(10px, 1.4vw, 24px); }
  .video-fit--pair .video-frame {
    width: min(calc(50% - clamp(5px, .7vw, 12px)), calc(100cqh * 16 / 9));
    margin: 0;
  }

  .project--fit .project__role {
    margin: 2.2vh 0 1.1vh; flex: none;
    font-size: var(--fs-meta);
  }
  .project--fit .project__body { flex: none; }
  .project--fit .project__body p {
    font-size: var(--fs-lead);
    margin-bottom: .8em;
  }
  .project--fit .back-link { margin-top: 2.4vh; font-size: var(--fs-meta); }

  /* --- simple text pages (hire me) -------------------------------------- */

  /* height comes from the single-panel flex rule above, not --fit-h: this
     page has a footer under it that has to fit too. */
  .page--fit { min-height: fit-content; padding: 2.5vh 0;
               display: flex; flex-direction: column; justify-content: center; }
  .page--fit h1 { font-size: clamp(24px, 5.2vh, 54px); margin-bottom: 2.2vh; }
  .page--fit .page__body { margin-bottom: 2.6vh; }
  .page--fit .page__body p { font-size: var(--fs-lead); margin-bottom: .9em; }
}

/* Browsers without size containment (older Safari/Firefox) keep the fixed
   214px tiles and natural flow rather than collapsing to zero-height boxes. */
@supports not (container-type: size) {
  .section--fit, .project--fit, .page--fit { height: auto; }
  .fit, .video-fit { display: block; }
  .fit .tile,
  .split__media .tile { max-width: var(--tile-max, 214px); }
}
