/* =========================================================
   Ana Meli Olivo — v2
   A container for someone else's pictures. The interface loses.

   Budget rule: distinctiveness goes on the FRAME — the rail,
   the credit block, the type, the sequencing. Never on the
   PLATE, which is wherever artwork sits: no texture behind art,
   no coloured borders, no overlay gradients, no hover effects
   that alter an image.
   ========================================================= */

:root {
  /* --- Field: the page and its surfaces. One hue (214). --- */
  --ink-950: #050b1e;   /* plate floor behind framed art, lightbox scrim */
  --ink-900: #001b3f;   /* chrome: masthead, footer */
  --ink-800: #002352;   /* second dark surface */
  --ink-600: #004aad;   /* PRIMARY */
  --ink-500: #2261b6;
  --ink-300: #7097ca;
  --ink-200: #97b2d3;
  --ink-100: #beccdd;
  --paper-2: #ffffff;   /* page ground */
  --paper-1: #eef4fc;   /* insets and the loading state behind artwork */
  --paper-0: #ffffff;   /* type on dark grounds */

  /* --- Lift ---
     58% of the portfolio is line work and design sheets on white
     grounds. On a white page those have no edge and dissolve into
     it, so artwork is raised off the page rather than outlined —
     a border would sit on the plate and change how the art reads,
     a shadow sits under it and does not. Two layers: a tight
     contact shadow that reads as weight, and a wide ambient one
     that reads as height. Tinted navy, never neutral grey. */
  --lift: 0 1px 2px rgba(0, 27, 63, 0.09),
          0 6px 16px -4px rgba(0, 27, 63, 0.13);
  --lift-hi: 0 2px 5px rgba(0, 27, 63, 0.11),
             0 16px 38px -10px rgba(0, 27, 63, 0.20);

  /* --- Accent. Exactly one hue, and it has one job. ---
     Sampled from the Talavera tile already in her identity, so it
     is inherited rather than invented. It marks the production
     rail and nothing else, and it is never placed beside artwork.

     Three tones of the same gold, because one value cannot be both
     a legible label and a visible rule across a light and a dark
     surface. --accent is the mark (rules, underlines); --accent-ink
     is the same hue taken to a contrast that passes as small text
     and is flipped per surface below. */
  --accent: #936f1a;        /* the mark on light — 3.53:1 as a rule */
  --accent-ink: #74591b;    /* text on light — 5.02:1 */
  --accent-soft: rgba(217, 165, 49, 0.16);

  /* --- Contextual, flipped per surface --- */
  --fg: var(--ink-900);
  --fg-body: #12336b;
  --fg-quiet: var(--ink-500);
  --fg-line: rgba(0, 74, 173, 0.24);
  --bg: var(--paper-2);

  /* --- Type ---
     Display: Poppins, carried over — it has a point of view and it
     is already hers. Utility: a mono, because a portfolio is mostly
     metadata and credits, and a mono makes a credit block read as
     production paperwork instead of leftover text. */
  --font-display: 'Poppins', system-ui, sans-serif;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --t-mono:  0.75rem;    /* 12 — rail, credits, stage numbers */
  --t-nav:   0.85rem;    /* 13.6 — header nav only; off the scale on purpose */
  --t-label: 0.8125rem;  /* 13 — kickers */
  --t-sm:    0.9375rem;  /* 15 — captions */
  --t-body:  1.0625rem;  /* 17 */
  --t-lede:  clamp(1.125rem, 1.6vw, 1.375rem);
  --t-h3:    clamp(1.25rem, 2vw, 1.5rem);
  --t-h2:    clamp(1.75rem, 3.4vw, 2.75rem);
  --t-h1:    clamp(2.25rem, 6vw, 4.5rem);
  --t-mega:  clamp(2.125rem, 5vw, 4.25rem);

  --measure: 62ch;
  --wide: 1440px;
  --mid: 1100px;
  --gutter: clamp(20px, 4vw, 56px);
  --chrome-h: 68px;
  --step: clamp(56px, 8vw, 120px);   /* vertical rhythm between blocks */
}

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

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: calc(var(--chrome-h) + 20px);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg-body);
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img, video { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--fg);
  margin: 0;
  line-height: 1.08;
  font-weight: 700;
  letter-spacing: -0.022em;
  text-wrap: balance;
}
h1 { font-size: var(--t-h1); font-weight: 800; }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }
p { margin: 0; }

::selection { background: var(--ink-600); color: var(--paper-0); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

.skip {
  position: absolute; left: 12px; top: -120px; z-index: 200;
  background: var(--paper-0); color: var(--ink-900);
  padding: 12px 18px; font-weight: 700; font-size: var(--t-sm);
}
.skip:focus { top: 12px; }

.wrap { max-width: var(--wide); margin: 0 auto; padding-inline: var(--gutter); }
.wrap--mid { max-width: var(--mid); }
.measure { max-width: var(--measure); }

/* =================== Surfaces =================== */
.on-dark {
  --fg: var(--paper-0);
  --fg-body: #cfdcf0;
  --fg-quiet: #9fb6d6;
  --fg-line: rgba(235, 242, 252, 0.20);
  --bg: var(--ink-900);
  --accent: #eac97b;
  --accent-ink: #eac97b;   /* 10.69:1 on navy */
  background: var(--ink-900); color: var(--fg-body);
}
.on-brand {
  --fg: var(--paper-0);
  --fg-body: #dbe6f7;
  --fg-quiet: #b6c9e6;
  --fg-line: rgba(235, 242, 252, 0.26);
  --bg: var(--ink-600);
  --accent: #eac97b;
  --accent-ink: #eac97b;   /* 5.09:1 on brand blue */
  background: var(--ink-600); color: var(--fg-body);
}
/* The previews band on the home page — the brand background rather than a flat
   colour: the same petal motif that sits behind the hero portrait, running from
   near-black navy at the top to bright blue at the bottom.

   Two things that background costs, both handled here:

   1. Contrast. The brightest point in the artwork is #006ddc, and against that
      the off-white tokens all drop under AA (body 4.38:1, quiet 4.10:1, gold
      4.22:1). A flat 20% #001437 wash over the image — measured, not guessed —
      puts the worst case at #005bbb, where the same tokens read 5.75 / 5.39 /
      5.55:1. It is light enough that the pattern is untouched to the eye.

   2. The tear. .rip paints var(--bg) as its torn edge, and an image can't be
      painted into that shape, so --bg is set to the image's own top band.
      `background-position: center top` pins that band to the section's top
      edge, so the rip and the background meet on the same colour.

   The file is served pre-flipped rather than flipped in CSS: transform would
   turn the whole element and its contents over, and background-image has no
   flip of its own. So the petals now run along the top, under the tear, and
   the band resolves into near-black navy as it meets the footer — which is why
   the footer's own tear is suppressed on this page.

   background-attachment stays scroll: fixed would read as parallax, which the
   motion thesis rules out. */
.on-heart {
  --fg: var(--paper-0);
  --fg-body: #eaf1fc;         /* 5.75:1 at the image's brightest point */
  --fg-quiet: #e0eafa;        /* 5.39:1 */
  --fg-line: rgba(235, 242, 252, 0.32);
  --bg: #005bc4;              /* the flipped image's top band — what the rip paints */
  --accent: #f9ecc4;
  --accent-ink: #f9ecc4;      /* 5.55:1 */
  background-color: #060d25;  /* the band's foot, and the fallback if the image fails */
  background-image:
    linear-gradient(rgba(0, 20, 55, 0.2), rgba(0, 20, 55, 0.2)),
    url('/images/brand-background-flipped.webp');
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
  color: var(--fg-body);
}
.on-paper { --bg: var(--paper-2); background: var(--paper-2); }

/* =================== Type roles =================== */
/* Section label. On several sections this is the only title the block
   gets, so it carries at full ink weight rather than as quiet metadata. */
.kicker {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fg);
  margin: 0 0 20px;
}
.lede { font-size: var(--t-lede); line-height: 1.45; color: var(--fg); max-width: 40ch; }
/* The name that opens Contact's lede. It is the half of that sentence the
   portrait beside it answers to, so it steps up out of the running text and
   takes the display face, landing between the two sizes it sits between: 40px
   against the heading's 72px above and the lede's own 22px around it.

   2.5rem is also the most the measure will take. .lede holds 40ch — about
   440px at 22px — and the name sets roughly 345px of that in the display face
   at this size. One step larger and it breaks the measure and wraps before it
   reaches the em dash, which strands the dash at the head of a line.

   The body face, not the display one. The system gives each of the three
   faces a single job — Poppins headings, Inter prose, Plex Mono every label —
   and this name is prose: it is the opening clause of a sentence, and the
   rest of that sentence is Inter. Setting it in Poppins put the heading face
   inside a running paragraph directly beneath a Poppins h1, so the name and
   the heading competed for the same role. Holding Inter and letting size and
   weight do the lifting keeps the name part of its own sentence.

   600 is bold here: it is the top of the three weights the body face loads,
   the same one the mono labels use, so nothing new is fetched for it.

   line-height comes off the lede's. Inherited, its 1.45 would compute against
   40px and open a 58px line box, which reads as a gap above the name rather
   than as the paragraph's first line.

   Colour is --ink-600, #004aad — the token .btn--solid fills with, not a
   copy of its value, so the name and the email button under it cannot drift
   apart if the brand blue is ever retuned. It measures 8.1:1 on the white of
   .about-page, so it clears AAA as body text, never mind at 40px. */
.lede__name {
  font-family: var(--font-body);
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ink-600);
}
.body { font-size: var(--t-body); color: var(--fg-body); max-width: var(--measure); }
.body + .body { margin-top: 1em; }
.note { font-family: var(--font-mono); font-size: var(--t-mono); line-height: 1.6; color: var(--fg-quiet); }

/* =================== Chrome =================== */
.chrome {
  position: sticky; top: 0; z-index: 60;
  height: var(--chrome-h);
  display: flex; align-items: center;
  background: var(--ink-900);
  border-bottom: 1px solid rgba(235, 242, 252, 0.12);
}
.chrome__inner {
  width: 100%; max-width: var(--wide); margin: 0 auto;
  padding-inline: var(--gutter);
  display: flex; align-items: center; gap: 28px;
}
/* Logo plus name. The name stays because the studio mark alone
   costs role legibility — a recruiter is hiring a person, not
   Olibee Studios, and has to be able to tell which is which. */
.chrome__brand { display: flex; align-items: center; gap: 14px; min-width: 0;
  transition: opacity 0.15s ease; }
.chrome__brand:hover { opacity: 0.72; }
.chrome__logo { height: 34px; width: auto; flex-shrink: 0; }
.chrome__name {
  display: block;
  font-family: var(--font-display); font-weight: 700;
  font-size: 0.9375rem; letter-spacing: -0.01em; color: var(--paper-0);
  white-space: nowrap;
  padding-left: 14px;
  border-left: 1px solid rgba(235, 242, 252, 0.24);
}
@media (max-width: 560px) {
  .chrome__name { border-left: 0; padding-left: 0; font-size: 0.875rem; }
  .chrome__logo { height: 28px; }
  .chrome__brand { gap: 10px; }
}
.chrome__name span { display: block; font-family: var(--font-mono); font-size: 0.625rem;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-200); font-weight: 400; margin-top: 2px; }
.chrome__nav { margin-left: auto; display: flex; gap: 22px; align-items: center; }
.chrome__nav a {
  font-family: var(--font-mono); font-size: var(--t-nav);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-200);
  padding: 6px 0; border-bottom: 1px solid transparent;
}
.chrome__nav a:hover { color: var(--paper-0); }
.chrome__nav a[aria-current] { color: var(--paper-0); border-bottom-color: var(--accent); }

.burger { display: none; margin-left: auto; background: none; border: 0; cursor: pointer; padding: 10px; }
.burger span { display: block; width: 22px; height: 2px; background: var(--paper-0);
  transition: background 0.15s ease; }
.burger:hover span { background: var(--accent); }
.burger span + span { margin-top: 5px; }

/* Four nav items need ~450px beside a 337px brand lockup — "COMICS &
   ILLUSTRATIONS" is long enough that dropping from five items to four barely
   moved the total. Below the wrap point the nav takes a second row, and
   .chrome is a fixed 68px, so that row escapes the header entirely. Collapse
   to the toggle before it can happen, with enough margin that a slow webfont
   swap can't push it over.
   Tied to --t-nav and to the item count: changing either moves this number. */
@media (max-width: 1100px) {
  .burger { display: block; }
  .chrome__nav {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: flex-start; gap: 0;
    background: var(--ink-900); padding: 6px var(--gutter) 18px;
    border-bottom: 1px solid rgba(235, 242, 252, 0.12);
    display: none;
  }
  .chrome__nav.open { display: flex; }
  .chrome__nav a { width: 100%; padding: 12px 0; }
}

/* =========================================================
   THE PRODUCTION RAIL — the signature.
   The route a film takes, used as real navigation. Stages the
   project documents are links; stages it doesn't are printed
   anyway, dimmed, so the shape of the pipeline stays legible
   and the gaps are honest rather than hidden.
   ========================================================= */
.rail {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: stretch;
  gap: 0;
  border-top: 1px solid var(--fg-line);
  border-bottom: 1px solid var(--fg-line);
  margin: var(--step) 0 0;
}
.rail__step {
  flex: 1 1 0; min-width: 96px;
  padding: 16px 14px 18px;
  display: flex; flex-direction: column; gap: 6px;
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.1em; text-transform: uppercase;
  border-left: 1px solid var(--fg-line);
  transition: background 0.15s ease, color 0.15s ease;
}
.rail__step:first-child { border-left: 0; }
.rail__step i { font-style: normal; font-size: 0.625rem; letter-spacing: 0.12em; color: var(--fg-quiet); }
.rail__step--on { color: var(--fg); }
.rail__step--on i { color: var(--accent-ink); }
.rail__step--on:hover { background: var(--accent-soft); }
.rail__step--off { color: var(--fg-quiet); opacity: 0.42; }

/* a slim variant used inside the hub cards */
.rail--mini { border: 0; margin: 14px 0 0; gap: 6px; }
.rail--mini .rail__step {
  flex: 0 0 auto; min-width: 0; border: 0; padding: 0;
  font-size: 0.625rem; letter-spacing: 0.12em;
  /* the full-size step is a column so its number can sit above the label;
     inline here, or the separator drops onto a second row of its own */
  display: inline-flex; flex-direction: row; align-items: baseline; gap: 0;
}
.rail--mini .rail__step::after { content: '·'; margin-left: 6px; color: var(--fg-quiet); }
.rail--mini .rail__step:last-child::after { content: ''; }

/* =================== Button ===================
   One affordance for every "go here" on the site. Mono, boxed, and
   filling with the brand on hover — an underlined link at 12px was
   reading as body text rather than something to press. */
.btn,
.hub__more,
.stage__link,
.sample a {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-mono);
  font-size: var(--t-mono); font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--fg-line);
  padding: 13px 20px;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.btn:hover, .sample a:hover, .stage__link:hover,
.hub__more:hover,
.hub__item:hover .hub__more,
.lead:hover .btn {
  background: var(--ink-600);
  border-color: var(--ink-600);
  color: #ffffff;
}
/* on dark grounds the fill has to go the other way.
   .on-heart is in here for the same reason: its ground is a blue close
   enough to --ink-600 that the default fill would barely register. */
.on-dark .btn:hover, .on-brand .btn:hover, .on-heart .btn:hover,
.on-dark .stage__link:hover, .on-brand .stage__link:hover, .on-heart .stage__link:hover,
.on-dark .hub__more:hover, .on-brand .hub__more:hover, .on-heart .hub__more:hover,
.on-heart .hub__item:hover .hub__more,
.on-dark .sample a:hover, .on-brand .sample a:hover, .on-heart .sample a:hover {
  background: var(--paper-0);
  border-color: var(--paper-0);
  color: var(--ink-900);
}
.btn--solid {
  background: var(--ink-600); border-color: var(--ink-600); color: #ffffff;
}
.btn--solid:hover { background: var(--ink-900); border-color: var(--ink-900); }

/* =================== About =================== */
.about-page {
  padding: var(--step) 0;
  --about-gap: 30px;
}
.about {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
  gap: var(--about-gap);
  align-items: start;
}
/* Every text element holds the design system's measure. The bio previously ran
   78 characters to the line because the column, not the measure, was setting
   the width. */
.about__text > * { max-width: var(--measure); }
/* Spacing steps up as the blocks get further apart in kind: the lede belongs to
   the heading, the buttons are their own beat, and the bio is a new section of
   the page. 11px under a 72px heading read as collision. */
.about__lede { margin-top: 37px; }
/* 20px, not the 42px the other blocks take, and not the 28px it held while the
   loop was 104px. The contact row is as tall as the loop — now 128px — while
   the buttons are ~49px, so align-items: center leaves ~39px of air under them
   inside the row before this margin is added at all. At 42px the gap under the
   buttons read as a hole, and the taller loop had already eaten most of the
   difference.

   The two clearances this controls pull against each other and cannot both be
   held: dropping to 16px would keep the buttons at the ~56px they had, but
   leaves only 16px under the loop's own shadow, which reads as a collision.
   20px splits it — buttons ~59px, loop 20px clear — both inside the range that
   read correctly at the old size. */
.about__bio { margin-top: 20px; }
/* Paragraph breaks have to beat the line gap to register as breaks — 17px
   against a 27px line-height did not. */
.about__bio .body + .body { margin-top: 26px; }
/* Left uncropped. At this column width the portrait lands within ~60px of the
   text's end on its own, and capping the height cost the top of the lettering
   for no gain in balance. */
.about__portrait img { width: 100%; height: auto; }
/* Ends on the same edge as the prose above it, not as the grid column that
   contains the prose. The 7fr track is ~826px at full width; .body caps the
   paragraphs inside it at var(--measure), which is ~580px, so matching the
   track still left the strip running a couple of hundred pixels past the
   text it belongs to.

   font-size is here only to make that match exact. --measure is 62ch, and ch
   resolves against the element's own font size, so the strip has to be told
   the body size or its 62ch would be measured in the inherited one and land
   somewhere else. Nothing inherits the value: .credits dt and dd both set
   their own size in rem, and the gaps are px.

   Compound selector on purpose: `.credits { margin: 0 }` is declared later in
   this file at equal specificity and was zeroing the top margin, leaving the
   strip's rule flush against the bottom of the portrait. */
.credits.about__facts {
  /* 38px, down from clamp(44px, 5vw, 80px) — 72px at full width. That value
     was set while the strip sat below the grid, where it needed to read as a
     separate band closing the page out. It now follows the bio inside the
     column, so it takes a paragraph break rather than a band's: a touch under
     the 42px the contact row takes, because the strip brings its own rule and
     22px of padding with it before the first label. */
  margin-top: 38px;
  font-size: var(--t-body);
  max-width: var(--measure);
  /* Two fields now — Based in and Socials — and neither spans, so this only
     has to resolve to two tracks across the measure to seat them on one row.
     220px does that from 468px of strip up to 716px, which covers the measure
     comfortably either side of the ~580px it actually computes to, so the row
     does not depend on the exact ch width of the body face. Below 468px the
     strip is narrower than two tracks and they stack, which is the right
     answer on a phone anyway.

     The earlier 140px was tuned for a different block: five units on a strip
     that ran the full 826px column. Both of those premises are gone. */
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
/* The socials read as plain text without this. They have always been anchors,
   but the reset at the top of the file sets `a { color: inherit;
   text-decoration: none }`, and every other link on the site earns its
   affordance from being a button, a card or a nav item — this is the one place
   a bare inline link appears, so it has to say so itself.

   Same --ink-600 as .lede__name and the email button, so the page has one
   colour that means "this is live". The underline is what carries it though:
   colour alone is not an accessible affordance, and at --t-sm on a strip of
   metadata it would read as styling rather than as a link. Offset clears the
   descender in "g" of Instagram and YouTube. */
.about__facts dd a {
  color: var(--ink-600);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-thickness 0.12s ease, color 0.12s ease;
}
.about__facts dd a:hover {
  color: var(--ink-900);
  text-decoration-thickness: 2px;
}

/* The résumé preview, under the facts strip and on the same edges as it.
   font-size for the same reason the strip carries one: --measure is 62ch, and
   ch is resolved against the element's own size, so without this the block
   would measure 62 characters of the inherited size and stop somewhere else.

   The page keeps .plate's treatment — paper ground, the higher lift — rather
   than the class itself, because .plate img sets width without height and this
   one needs the intrinsic ratio held to avoid a 750px reflow on load. */
.resume {
  display: block;
  margin-top: 38px;
  font-size: var(--t-body);
  max-width: var(--measure);
}
.resume__page {
  display: block;
  background: var(--paper-1);
  overflow: hidden;
  box-shadow: var(--lift);
  transition: box-shadow 0.15s ease;
}
.resume__page img { display: block; width: 100%; height: auto; }
.resume:hover .resume__page { box-shadow: var(--lift-hi); }
/* Mono label, like every other "go here" on the site, but unboxed: the page
   above it is already the button, and a bordered control under a bordered
   plate reads as two targets rather than one. --ink-600 keeps it with the
   name, the email button and the socials as the page's one live colour. */
.resume__more {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: var(--t-mono); font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-600);
  transition: color 0.15s ease;
}
.resume:hover .resume__more { color: var(--ink-900); }

@media (max-width: 860px) {
  /* The old inline grid had no breakpoint at all and held a 6fr/5fr split down
     to phone width, which left two ~150px columns. */
  /* stacked, so the 30px gutter becomes a vertical gap — needs a little more.
     The facts strip needs nothing here: it is held to the prose measure at
     every width, and once the wrap is narrower than that measure the wrap is
     what binds. */
  .about-page { --about-gap: 38px; }
  .about { grid-template-columns: 1fr; }
  /* face first when stacked — the greeting and the portrait belong together,
     and pushing a tall image below two paragraphs buries it */
  .about__portrait { order: -1; max-width: 380px; }
}

/* The contact pair on About. Left-aligned, unlike .hero__actions, which is
   ranged right to sit under the hero copy. */
/* Sits closer to the copy on both sides than the surrounding rhythm would
   suggest — the contact pair reads as part of the intro block, not as its own
   band. Still a touch clear of the 37px above the lede so the lede keeps
   grouping with the heading. */
.contact-now { margin: 42px 0 0; display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
/* The brand loop sitting in the contact row. Small on purpose — it is a
   flourish beside the two things a recruiter came for, not a piece of work.
   align-items above is what keeps it there: the row defaults to stretch, so a
   taller item would have pulled both buttons up to its own height. Width is
   fixed rather than relative, so the 480px rule below (which switches the row
   to a stretched column) cannot blow it up to full width.

   No ground and no lift. Both used to be here, and together with the loop's
   own baked-in ground they drew a soft square plate around a card that is
   meant to be falling through the page. The GIF now carries real transparency
   (see contact.ejs), so the card sits on the paper with nothing behind it. */
.contact-loop {
  flex: 0 0 auto;
  width: 128px;
}
/* Both sources are square already (see contact.ejs), so aspect-ratio and
   object-fit no longer do any cropping — they stay as the guarantee that the
   box is as tall as it is wide whatever is dropped in it, which is what the
   row's height is built on. That height is the pocket of dead space beside
   the buttons, so .about__bio below is re-tuned whenever this number moves.

   128px, up from 104. Worth knowing what that costs: the GIF is 320px square,
   so 104 was the largest size that still had real pixels behind a 3x display
   (312 of 320). At 128 a 3x screen upscales it about 1.2x. Invisible on this
   material — a card tumbling at thumbnail size, no fine detail to lose — and
   the alternative was re-encoding at 384px, which takes the file from 1.0MB
   to about 1.4MB for a flourish. If it ever grows past ~140px, re-encode
   instead of stretching further.

   picture is inline by default, and an inline box sitting on the text
   baseline would leave a sliver of paper under the loop inside the shadow. */
.contact-loop picture { display: block; }
.contact-loop img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  height: auto;
  object-fit: cover;
}
/* A touch more room inside these two as well — scoped, so the site's other
   buttons keep their existing 13px/20px. */
.contact-now .btn { padding: 15px 22px; }
/* The one place buttons drop the uppercase treatment: these print a real
   address and a real number, and the point is that they read exactly as they
   are rather than as a label. */
.contact-now .btn { text-transform: none; letter-spacing: 0.02em; font-size: 0.875rem; }
.contact-now .btn svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}
@media (max-width: 480px) {
  /* stacked, full width — thumb targets rather than a wrapped pair */
  .contact-now { flex-direction: column; align-items: stretch; }
  .contact-now .btn { justify-content: center; }
}

/* =================== Page head =================== */
.head { padding: calc(var(--step) * 0.9) 0 var(--step); }
.head h1 { margin-bottom: 0.3em; }

/* The brand artwork behind the page title on the four destinations after
   Story. Pairs with .on-heart in the markup, which is what carries the
   measured 20% #001437 wash and the contrast-checked type tokens — the type
   here is the same off-white on the same artwork, so it inherits that work
   rather than restating it. Only the file and the anchor differ.

   The un-flipped file, unlike the home page's band: flipped exists so its
   petals meet the tear at the top of that section, and this band has no tear
   to meet.

   Anchored to the foot of the image, which is the whole trick. The padding is
   untouched, so the band is the height the head already was — roughly 350px on
   Contact, which carries a heading alone, up to about 490px on Writing, whose
   lede runs four lines — while cover renders the 2400x1350 file 810px tall.
   Anchor it anywhere above the foot and the crop lands in the artwork's empty
   navy sky and the petals never appear at all. A side effect worth knowing:
   because the crop is taken from the bottom of a gradient, a taller band shows
   a taller slice and so reads darker. Contact is the brightest of the four and
   Writing the deepest, which is the reverse of the reading order.

   One knock-on in the templates. The first section under the head used to
   carry an inline padding-top: 0, because .head's padding-bottom sat on the
   same paper surface and served as that section's top space. It is inside the
   band now, so all four pages had that override removed and take .craft's
   normal step instead — otherwise the first heading butts straight up against
   the bottom edge of the artwork. */
.head--brand {
  background-image:
    linear-gradient(rgba(0, 20, 55, 0.2), rgba(0, 20, 55, 0.2)),
    url('/images/brand-background.webp');
  background-position: center bottom;
}

/* =================== Home ===================
   The claim is sized so the lead artwork breaks into the first
   viewport. On a portfolio the eye has to reach the work before
   it finishes reading the headline — a full screen of type,
   however good, is the interface winning. */
.hero { padding: clamp(36px, 5vw, 64px) 0 0; }
.hero h1 { font-size: var(--t-mega); letter-spacing: -0.032em; max-width: 18ch; }
.hero .lede { margin-top: 22px; font-size: clamp(1.0625rem, 1.5vw, 1.3125rem); max-width: 44ch; }

/* ---- Home hero: full-bleed portrait, copy set over it ----
   The frame is pinned to the artwork's own 1600x899 so "cover" crops
   almost nothing, and the gradient underneath matches the painting's
   backdrop so the capped frame has no seam. Width must be pinned too:
   with aspect-ratio plus max-height and auto width the browser shrinks
   the WIDTH to hold the ratio and leaves a gap at the page edge. */
.home-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 1600 / 899;
  max-height: 78vh;
  overflow: hidden;
  background: linear-gradient(180deg, var(--ink-950) 0%, var(--ink-600) 100%);
}
.home-hero__art {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 32% 18%;
}
.home-hero__copy {
  position: absolute; inset: 0;
  max-width: var(--wide);
  margin: 0 auto;
  padding-inline: var(--gutter);
  display: flex; flex-direction: column;
  align-items: flex-end; justify-content: center;
  text-align: right;
  text-shadow: 0 2px 18px rgba(0, 25, 58, 0.55);
}
/* every line white: the gradient brightens under the copy column and
   muted tones drop to 3.49:1 there, white holds 5.37:1 */
.home-hero__copy .kicker,
.home-hero__copy h1,
.home-hero__copy .lede { color: var(--paper-0); }
/* name → title → action. The kicker follows the h1 rather than leading it, so
   spacing runs from the h1 down. The tagline that used to sit under the title
   has been removed; the .lede rule is kept in case a line comes back here.

   The name keeps the site's --t-mega. The title is the one thing sized up: at
   the global 13px it read as a caption under a 68px name — a 5:1 drop — and
   with the tagline gone it is the only line describing the work. At ~23px that
   ratio is about 3:1, which reads as a subtitle rather than a label. Tracking
   comes down from 0.2em to 0.14em, because mono uppercase at that size sprawls
   at the smaller-type spacing. Scoped to the hero: the global .kicker is
   unchanged everywhere else. */
.home-hero__copy h1 {
  font-size: var(--t-mega); letter-spacing: -0.03em;
  max-width: 15ch; margin-bottom: 0.16em;
}
.home-hero__copy .kicker {
  font-size: clamp(1rem, 1.6vw, 1.5rem);
  letter-spacing: 0.14em;
  margin: 0 0 26px;
}
/* .home-hero__copy is a flex column, so margins between children do not
   collapse — the kicker's 26px bottom and .hero__actions' 30px top were
   stacking to a 56px gap and pushing the button away from the title it belongs
   with. Scoped, so the shared .hero__actions spacing is unaffected. */
.home-hero__copy .hero__actions { margin-top: 8px; }
.home-hero__copy .lede { margin-top: 0; max-width: 38ch; }

/* Hero actions. The buttons sit over artwork, where the page's default
   navy ink would disappear, so they invert: solid white for the primary
   and a white outline for the secondary. */
.hero__actions {
  margin-top: 30px; display: flex; flex-wrap: wrap; gap: 12px;
  justify-content: flex-end;
}
/* larger than the site's default button — these are the page's primary
   calls to action, sitting alone over artwork with nothing to compete
   with, so they can take the extra weight */
.home-hero__copy .btn {
  color: var(--paper-0);
  border-color: rgba(255, 255, 255, 0.55);
  text-shadow: none;
  font-size: var(--t-sm);
  letter-spacing: 0.11em;
  padding: 19px 32px;
  gap: 10px;
}
.home-hero__copy .btn:hover {
  background: var(--paper-0); border-color: var(--paper-0); color: var(--ink-900);
}
.home-hero__copy .btn--solid {
  background: var(--paper-0); border-color: var(--paper-0); color: var(--ink-900);
}
.home-hero__copy .btn--solid:hover {
  background: transparent; border-color: rgba(255, 255, 255, 0.75); color: var(--paper-0);
}

@media (max-width: 900px) {
  /* the copy leaves the artwork and stacks under it, so the frame can no
     longer be pinned to the art's ratio — it would clip the copy away */
  .home-hero {
    aspect-ratio: auto;
    max-height: none;
    overflow: visible;
  }
  /* taller and tighter on the figure — a 16:9 slice of a 1.78:1 painting
     leaves half the frame as empty backdrop at phone width */
  .home-hero__art { aspect-ratio: 3 / 2; object-position: 26% 6%; }
  .home-hero__copy {
    position: static;
    align-items: flex-start;
    text-align: left;
    padding-block: clamp(28px, 6vw, 48px) clamp(36px, 7vw, 56px);
    text-shadow: none;
  }
  .hero__actions { justify-content: flex-start; }
}
/* the lead piece: one image, large, nothing on top of it */
/* no top margin: the section kicker above it sets the gap */
.lead { display: block; }
.lead__plate { display: block; background: var(--paper-1); overflow: hidden; box-shadow: var(--lift-hi); }
.lead__plate img { width: 100%; }
.lead__foot {
  margin-top: 18px; display: flex; flex-wrap: wrap; gap: 14px 20px;
  align-items: center; justify-content: space-between;
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-quiet);
}
.lead__cap { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: baseline; }
.lead__cap b { color: var(--fg); font-weight: 500; }

/* the narrative spine — three claims, each backed by work */
.claims { display: grid; gap: clamp(28px, 4vw, 48px); grid-template-columns: repeat(3, 1fr); }
.claim__n {
  font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: 0.16em;
  color: var(--accent-ink); display: block; margin-bottom: 12px;
}
.claim__h { font-size: var(--t-h3); margin-bottom: 10px; }  /* h2 for the outline, h3 in scale */
.claim p { font-size: var(--t-sm); color: var(--fg-body); }
@media (max-width: 780px) { .claims { grid-template-columns: 1fr; } }

/* =================== Work hub =================== */
.hub { display: grid; gap: clamp(32px, 5vw, 64px); }
.hub__item { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(24px, 4vw, 56px); align-items: center; }
/* Alternate which side the plate sits on, but mirror the track widths with it.
   Re-ordering alone moved the plate into the 5fr track on every other row, so
   the thumbnails came out large, small, large, small. The plate keeps 7fr on
   both sides; it is the body copy that changes column.

   Driven by an explicit class, not :nth-child(even) — the Directing page splits
   its cards across three .hub containers, so positional matching restarted the
   count in each one and the page read left, right, left, left, left. The
   templates carry a running index instead. */
.hub__item--flip { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
.hub__item--flip .hub__plate { order: 2; }
.hub__plate { display: block; background: var(--paper-1); overflow: hidden; box-shadow: var(--lift-hi); }
.hub__plate img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.hub__body h2 { margin-bottom: 12px; }
.hub__body .lede { font-size: var(--t-body); max-width: 46ch; }
.hub__cred {
  margin: 16px 0 0; display: flex; flex-wrap: wrap; gap: 6px 18px;
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-quiet);
}
.hub__more { margin-top: 20px; }
@media (max-width: 820px) {
  .hub__item { grid-template-columns: 1fr; }
  .hub__item:nth-child(even) .hub__plate { order: 0; }
}

/* Compact variant — the home page's "also in the portfolio" band.
   Those two cards preview other pages rather than showing work, so they should
   not out-measure the story section above them. At the standard .hub ratio the
   plate takes 7/12 of the row, which is 744px wide and therefore 419px tall,
   and two of those plus padding ran the band past 1000px. Flipping the ratio
   puts the plate at 5/12 — 531px wide, 299px tall — and the copy column takes
   the space instead, which it can use. Roughly a quarter off the band's height
   without shrinking the type or dropping a card.
   The plate stays on the 5fr track in both directions: .hub__item--flip moves
   it with `order`, so its tracks are declared the other way round. */
.hub--compact .hub__item { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
.hub--compact .hub__item--flip { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
.hub--compact .hub__body .lede { font-size: var(--t-sm); max-width: 52ch; }
.hub--compact .hub__more { margin-top: 16px; }
/* Re-stated inside the variant: the breakpoint above is a single class and
   would otherwise lose to these on specificity. */
@media (max-width: 820px) {
  .hub--compact .hub__item,
  .hub--compact .hub__item--flip { grid-template-columns: 1fr; }
}

/* =================== Credit block ===================
   Designed once, used everywhere. A portfolio is mostly metadata
   and a set credit block signals professional context faster
   than any amount of prose. */
.credits {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 20px 28px;
  border-top: 1px solid var(--fg-line);
  padding-top: 22px; margin: 0;
}
.credits div { min-width: 0; }
.credits dt {
  font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--fg-quiet); margin-bottom: 6px;
}
.credits dd { margin: 0; font-size: var(--t-sm); color: var(--fg); line-height: 1.45; }
/* A credit block inside a craft section is followed by more of the same
   sample — its note, then its excerpt — so it needs a bottom edge of its own.
   The case-study version closes out the hero and takes its spacing from the
   next section instead. */
.craft .credits { padding-bottom: 30px; }
/* A field whose value runs long enough that a single 150px track would wrap it
   to four or five lines and stretch every sibling in the row to match.
   Breakpoint-guarded: auto-fit can resolve to one column, and a span-2 item
   would then manufacture an implicit second track and overflow the container. */
@media (min-width: 860px) {
  .credits__span2 { grid-column: span 2; }
}

/* =================== Embedded document ===================
   A pitch PDF shown in place and scrolled with the browser's own viewer.
   Deliberately the quieter of the two: a centred, page-shaped panel rather
   than a full-width band, so the lead artwork above it stays the biggest
   thing on the page. Width is derived from the page ratio plus room for the
   viewer's own toolbar, so the fitted page sits in it without a wide gutter. */
.docframe {
  margin-top: 28px;
  height: min(76vh, 820px);
  max-width: 700px;
  background: var(--paper-1);
  box-shadow: var(--lift-hi);
}
.docframe iframe { width: 100%; height: 100%; border: 0; display: block; }
.docframe__note { margin: 14px 0 0; }
@media (max-width: 620px) {
  /* mobile PDF viewers are cramped and often refuse to render inline at all —
     a shorter frame keeps the fallback link in reach */
  .docframe { height: 60vh; max-width: none; }
}

/* =================== Case study =================== */
.case__hero { padding: var(--step) 0 0; }
.case__title { margin-bottom: 0.25em; }
.case__sub { font-family: var(--font-display); font-weight: 600; font-size: var(--t-h3); color: var(--fg-quiet); margin-bottom: 0.5em; }
.case__logline { font-size: var(--t-lede); color: var(--fg); max-width: 46ch; }

.result { margin: var(--step) 0 0; }
.plate { background: var(--paper-1); overflow: hidden; box-shadow: var(--lift-hi); }
.plate img { width: 100%; }
/* Kept for a result that should read as a document rather than artwork:
   centred and capped to the viewport instead of running the column width.
   Nothing uses it at present — the lead on every case study is artwork. */
.plate--page { background: none; box-shadow: none; display: flex; justify-content: center; }
.plate--page img { width: auto; max-width: 100%; max-height: 86vh; box-shadow: var(--lift-hi); }
.frame16 { position: relative; width: 100%; aspect-ratio: 16 / 9; background: var(--ink-950); box-shadow: var(--lift-hi); }
.frame16 iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.brief { margin-top: var(--step); }
.brief .kicker { color: var(--accent-ink); }

/* a stage section — the process description the rail points at */
.stage { padding: var(--step) 0 0; scroll-margin-top: calc(var(--chrome-h) + 28px); }
.stage__head { display: flex; align-items: baseline; gap: 16px; border-bottom: 1px solid var(--fg-line); padding-bottom: 14px; }
.stage__n { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: 0.16em; color: var(--accent-ink); }
.stage__name { font-family: var(--font-display); font-weight: 700; font-size: var(--t-h3); color: var(--fg); }
.stage__note { margin-top: 20px; font-size: var(--t-body); color: var(--fg-body); max-width: var(--measure); }
.stage__link { margin-top: 20px; }

/* Media runs. Sized by the work's real ratio, never cropped to a tile. */
.run { margin-top: 28px; display: grid; gap: clamp(14px, 1.8vw, 22px); }
.run--1 { grid-template-columns: 1fr; }
.run--2 { grid-template-columns: repeat(2, 1fr); }
.run--3 { grid-template-columns: repeat(3, 1fr); }
.run figure { margin: 0; }
.run img { width: 100%; background: var(--paper-1); cursor: zoom-in; box-shadow: var(--lift); }
.run video { width: 100%; height: auto; display: block; background: var(--ink-950); box-shadow: var(--lift); }
.run figcaption { margin-top: 8px; font-family: var(--font-mono); font-size: var(--t-mono); color: var(--fg-quiet); }
/* caption under a stage-level embed — carries the separate credit */
.stage__cap { margin: 10px 0 0; font-family: var(--font-mono); font-size: var(--t-mono); color: var(--fg-quiet); max-width: var(--measure); }
.run--4 { grid-template-columns: repeat(4, 1fr); }
/* A poster or splash page inside a run. The Heartdecker splash is 1527x2160;
   at the full column width it renders taller than two viewports, so the reader
   never sees the whole thing at once — which is the one job a poster has.
   Capped at the same 700px .docframe uses, so everything page-shaped on this
   site is the same size. Centred rather than ranged left, because a lone
   narrow figure in a full-width column reads as a mistake. */
.run--poster { justify-items: center; }
.run--poster figure { width: 100%; max-width: 700px; }
@media (max-width: 900px) { .run--3, .run--4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .run--2, .run--3, .run--4 { grid-template-columns: 1fr; } }

/* The rotating composition slots. Opacity only — the artwork never moves in
   its frame, and the box never changes size because every panel in the pool is
   1600x900. Longer than the site's 200ms state-feedback rule on purpose: this
   is a crossfade between two drawings, not a hover response. The global
   prefers-reduced-motion block below flattens it, and v2.js declines to run the
   rotation at all under that setting. */
[data-rotate] img { transition: opacity 420ms ease; }
[data-rotate] img.is-out { opacity: 0; }

/* Visual Development runs read as one column of work, so consecutive runs sit
   closer to each other than a run sits to the heading above it. The label is a
   quiet mono line naming a set, not a section heading — the page has two of
   those and no more. */
.vd__run + .vd__run { margin-top: clamp(14px, 1.8vw, 22px); }
.vd__label {
  margin: 34px 0 -14px;
  font-family: var(--font-mono); font-size: var(--t-mono); font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-ink);
}

/* next project — never dead-end the most engaged moment on the site */
.next { display: block; margin-top: var(--step); border-top: 1px solid var(--fg-line); padding: 28px 0 var(--step); }
.next__k { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--fg-quiet); }
.next h2 { margin-top: 10px; }
.next:hover h2 { color: var(--ink-600); }
.on-dark .next:hover h2, .on-brand .next:hover h2, .on-heart .next:hover h2 { color: var(--accent); }

/* =================== Craft index =================== */
/* Top padding only, so consecutive sections don't double their gap — but
   the last one then has nothing between it and the footer. */
.craft { padding: var(--step) 0 0; scroll-margin-top: calc(var(--chrome-h) + 28px); }
.craft:last-of-type { padding-bottom: var(--step); }
.gallery { columns: 3 300px; column-gap: clamp(14px, 1.8vw, 22px); margin-top: 28px; }
.gallery figure { margin: 0 0 clamp(14px, 1.8vw, 22px); break-inside: avoid; }
.gallery img { width: 100%; background: var(--paper-1); cursor: zoom-in; box-shadow: var(--lift); }
@media (max-width: 700px) { .gallery { columns: 1; } }

/* =================== Mosaic ===================
   The sketchbook block. .gallery is CSS multi-column, which can only ever give
   every piece the same width — so the two landscape works sat at a third of the
   page while the portraits towered over them, and the column that caught both
   landscapes ended ~730px short.

   An explicit 3x3 instead. The landscapes span two columns and are placed top
   left and bottom right; the five portraits take single columns. Every row
   carries at least one 1527x2160 piece, so all three rows resolve to 605px at
   the full wrap and the block has a flat bottom edge.

   Items are centred in their row, so the slack under a shorter piece (124px
   under the widest landscape) splits above and below it rather than hanging
   underneath. Nothing is cropped: every ratio is the artwork's own, as with
   .run everywhere else.

   Placement is by source order, so reordering the figures changes the layout —
   see the note in comics.ejs. */
.mosaic {
  margin-top: 28px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(14px, 1.8vw, 22px);
  align-items: center;
}
.mosaic figure { margin: 0; }
.mosaic img { width: 100%; background: var(--paper-1); cursor: zoom-in; box-shadow: var(--lift); }
.mosaic__wide { grid-column: span 2; }
/* Two columns can't resolve seven items evenly whatever the spans — an odd
   count always leaves one slot open — so the wides stay full-bleed and the
   single gap falls at the end, next to the last portrait. */
@media (max-width: 980px) {
  .mosaic { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 620px) {
  .mosaic { grid-template-columns: 1fr; }
  .mosaic__wide { grid-column: span 1; }
}

.samples { margin-top: 28px; display: grid; gap: 18px; }
.sample { border: 1px solid var(--fg-line); padding: 22px 24px; }
.sample h3 { margin-bottom: 8px; }
.sample p { font-size: var(--t-sm); color: var(--fg-body); max-width: var(--measure); }
.sample a { margin-top: 18px; }

/* =================== Group divider ===================
   Labels a run of projects on the Directing page. Kicker typography,
   because the project titles underneath have to outrank it. */
.group {
  display: flex; align-items: baseline; gap: 20px; flex-wrap: wrap;
  border-bottom: 1px solid var(--fg-line);
  padding-bottom: 14px; margin-bottom: clamp(28px, 4vw, 48px);
}
.group__label {
  font-family: var(--font-mono); font-size: var(--t-label); font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--fg);
}
.group__note {
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-quiet);
  margin-left: auto;
}
@media (max-width: 700px) { .group__note { margin-left: 0; } }

/* =================== Excerpt ===================
   A quoted passage from a script. Indented off a rule rather than
   boxed, so it reads as manuscript rather than as a UI panel.

   Two indents doing different jobs: the margin sets the block in from the
   surrounding prose so the quote is visibly a quote, and the padding holds
   the text off its own rule. The margin collapses to nothing on small
   screens, where the measure is already tight. */
.excerpt {
  margin: 28px 0 0 clamp(0px, 4vw, 52px);
  padding-left: clamp(18px, 2.5vw, 30px);
  border-left: 2px solid var(--accent);
  max-width: var(--measure);
}
.excerpt__label {
  display: block;
  font-family: var(--font-mono); font-size: var(--t-mono); font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent-ink); margin-bottom: 14px;
}
.excerpt p { font-size: var(--t-body); color: var(--fg-body); line-height: 1.65; }
.excerpt p + p { margin-top: 0.85em; }

/* =================== Ripped edge ===================
   Ported from v1. The strip paints nothing itself — the torn shape is drawn
   on ::after in whatever colour the surface class on this element resolves,
   so the incoming section tears down over the outgoing one. The element's own
   box must stay transparent or it renders as a solid unmasked bar.
   Mirror-tiled mask, so the tear never shows a repeat seam.

   ::before is the deckle: the same shape offset up a few pixels in a pale
   tone, surviving along the tear as exposed paper fibre. It earns its keep on
   blue-on-blue transitions; over white it is invisible and simply costs
   nothing. */
.rip {
  position: relative;
  height: 26px;
  margin-top: -1px;
  background: none;
}
.rip::before,
.rip::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: 100%;
  -webkit-mask-image: url('/images/rip-mask.png');
  mask-image: url('/images/rip-mask.png');
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  -webkit-mask-size: auto 100%;
  mask-size: auto 100%;
  -webkit-mask-position: center top;
  mask-position: center top;
}
.rip::before { top: -3px; background: var(--deckle, rgba(235, 242, 252, 0.62)); }
.rip::after  { top: 0;    background: var(--bg); }

/* =================== Contact / footer =================== */
.foot { padding: var(--step) 0 48px; }
.foot h2 { max-width: 18ch; }
/* no rules between rows — the mono labels already column up and separate
   them, so the gap alone carries it */
.foot__rows { margin-top: 34px; display: grid; gap: 12px; }
.foot__row { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: baseline; }
.foot__row dt { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--fg-quiet); min-width: 92px; }
.foot__row dd { margin: 0; font-size: var(--t-body); color: var(--fg); }
.foot__row a:hover { border-bottom: 1px solid var(--accent); }
.foot__end { margin-top: var(--step); font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: 0.1em; color: var(--fg-quiet); }

/* =================== Lightbox =================== */
.lb { position: fixed; inset: 0; z-index: 300; display: none; place-items: center;
  background: rgba(5, 11, 30, 0.96); padding: 56px 20px; }
.lb.open { display: grid; }
.lb img { max-width: 100%; max-height: calc(100vh - 112px); width: auto; cursor: zoom-out; }
.lb button:hover { background: rgba(235, 242, 252, 0.16); border-color: var(--paper-0); }
.lb button { position: absolute; background: none; border: 1px solid rgba(235,242,252,0.4);
  transition: background 0.15s ease, border-color 0.15s ease;
  color: var(--paper-0); width: 42px; height: 42px; cursor: pointer; font-size: 1.1rem; }
.lb__x { top: 16px; right: 18px; }
.lb__p { left: 16px; top: 50%; transform: translateY(-50%); }
.lb__n { right: 16px; top: 50%; transform: translateY(-50%); }
.lb__c { position: absolute; top: 22px; left: 20px; font-family: var(--font-mono);
  font-size: var(--t-mono); color: var(--ink-200); }

/* =========================================================
   Motion thesis: the site holds still, and cuts.
   The only sustained movement belongs to her animation tests.
   Transitions are state feedback only — under 200ms, no
   scroll-triggered reveals, no parallax on artwork.
   ========================================================= */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
