/* Calouro photography portfolio.
   Ported from the original design's globals.css. The Tailwind import was
   dropped because no utility classes were ever used; every rule here is
   hand-written and applies to the semantic class names in the templates. */

/* Outfit (OFL), self-hosted, is the site's display face: the wordmark, the hero
   headline, the chapter titles and the footer name. Everything else — captions,
   labels, body — stays Arial, because a display face at 9px with .09em tracking
   is mud.

   It replaced Jost on 2026-08-17, arriving under its own filename rather than by
   overwriting the file of the face before it. A different face means a different
   filename, never a re-upload: the URL carries no fingerprint, so a replaced
   file would be served stale from caches for a week.

   The full Latin cut, not a subset of the letters in "Calouro": the wordmark is
   editable in the studio, and fallback is per glyph, so a renamed site would
   render half in each face. Contents never change, so the URL carries no
   fingerprint.

   Variable on the weight axis since 2026-08-25, when the wordmark's second word
   wanted a lighter cut than the rest of the display type. The file it replaced
   was a static Regular instance of this same face, 14 kB; this one is the
   variable source cut down to wght 100-400, 22 kB. Nine kilobytes buys every
   weight from Thin to Regular out of one request, where two static files would
   have cost 28 kB in two, and both would have blocked.

   Instanced at 400 it is metrically identical to the static file it replaced:
   same x-height (.475em), cap height (.694em), ascender and descender, and the
   same advance widths to four decimals. Everything measured against the old file
   in the comments below therefore still holds. The axis stops at 400 because
   nothing here is heavier; the range costs nothing (100-400 is in fact 100 bytes
   smaller than 200-400) so it is left open down to Thin.

   The old file stays in public/ deliberately. Its URL carries no fingerprint and
   Cloudflare holds the stylesheet for four hours, so browsers on the previous
   CSS keep asking for it after this deploy. Delete it a day later, not now. */
@font-face {
  font-family: "Outfit";
  font-weight: 100 400;
  font-style: normal;
  font-display: block;
  src: url(/outfit-display-var.woff2) format("woff2");
}

/* EB Garamond (OFL), self-hosted, for the quote alone. A Garamond revival, so
   literary by descent rather than by recent styling: the objection to the ten
   editorial serifs offered earlier was that they were drawn in the last fifteen
   years to look the part, and this is a cut of types from the 1500s.

   Two files, split on unicode-range exactly as Google serves them. The quotes are
   editable in the studio, so the face has to cover whatever gets typed: a name
   like Zdzislaw with its proper diacritics would otherwise render half Garamond
   and half fallback, which is the per-glyph trap this project has hit before. The
   browser only fetches the second file if the text actually needs it, so the usual
   cost is the 23 kB one.

   font-display: swap, and deliberately no preload. The quote sits a full viewport
   below the fold, behind the hero, so the swap happens long before it is looked
   at, and the homepage's critical path stays with the photograph. Outfit is
   preloaded and set to block because it paints in the first screenful; this does
   not. */
@font-face {
  font-family: "EB Garamond";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url(/eb-garamond-quote.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "EB Garamond";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url(/eb-garamond-quote-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root { --paper: #f2f0ea; --ink: #171713; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--paper); color: var(--ink); font-family: Arial, Helvetica, sans-serif; }
body.no-scroll { overflow: hidden; }
a { color: inherit; text-decoration: none; }
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
button { font: inherit; }
/* The rail stays with the reader the whole way down the page. Over the hero it
   is bare white type on the photograph, as designed. Past the hero it takes a
   paper bar (.banded), which is what stops its type landing on the work —
   floating it over the photographs is unreadable in either colour, since
   nothing wins against both a bright sky and a dark frame. */
/* Everything in the rail is centred, and that is a decision rather than a
   default. Tried and reverted on 2026-08-25: the nav labels' baseline sits
   3.24px above the wordmark's at these sizes, which can read as the labels
   hanging, and they were dropped onto the wordmark's baseline to close it.
   Asked for the centred version back the same day. **Do not "fix" this again
   without asking.**
   The gap is real and it is arithmetic, so knowing why saves rediscovering it: a
   centred line box puts its baseline below its own centre by (lineGap/2 +
   ascent) less half the line height, which scales with the type size. Outfit is
   1.0/-.26/0 and lands 7.40px below centre at 20px; Arial is 1854/434/67 over
   2048 and lands 4.16px below at 12px. Two sizes centred on one line therefore
   cannot share a baseline, at any size, in any layout system. What centring does
   give is optical centres that agree almost exactly: the wordmark's ink sits
   .02px off the mid-line and the labels' .14px off it. The hamburger is an icon
   with no baseline, so centring is the only thing it can do, and it was never
   part of the question. */
/* --rail-pad is 28px because that is the page's gutter: .work, .photograph,
   .about and footer all sit at 28px, and .hero-caption at 28px too. The rail
   was inherited at 48px, which put the wordmark 20px inboard of the headline
   directly under it and of every section below it. Reported as the wordmark not
   lining up with the hero headline on desktop; it agreed on phones only because
   the 640px rule already brings both to 18px. If the gutter ever moves, this
   moves with it. */
.site-header { --rail-pad: 28px; position: fixed; z-index: 10; inset: 0 0 auto; display: flex; justify-content: space-between; align-items: center; padding: 8px var(--rail-pad); color: var(--ink); font-size: 11px; letter-spacing: .09em; text-transform: uppercase; transition: color 180ms ease, background-color 180ms ease, box-shadow 180ms ease, text-shadow 180ms ease; }
.site-header.on-dark { color: #fff; text-shadow: 0 1px 8px rgba(0,0,0,.35); }
/* A hairline rather than a shadow: the page is flat, and a drop shadow would be
   the only depth cue on it. */
.site-header.banded { background: var(--paper); box-shadow: 0 1px 0 rgba(23,23,19,.18); }
/* The bar would otherwise cover the heading a jump link lands on. */
:target, .work, footer, .chapter { scroll-margin-top: 68px; }
/* The name is the identity, not a third nav item: larger, and tracked tighter
   than the wide label spacing the rest of the header uses.

   20px, not the 16px it wore as Arial. A display face with a small x-height
   loses optical size at the same pixel value: Outfit's is .475em against Arial's
   .519em, so 20px here reads like about 18px of Arial against the 12px nav
   beside it. Outfit sits within a rounding error of the Jost it replaced
   (.475 against .460), so the size carried over unchanged. */
.wordmark { font-family: "Outfit", Arial, Helvetica, sans-serif; font-size: 20px; font-weight: 400; letter-spacing: .04em; }
/* The second word of the wordmark, "Collection", editable beside the first in
   the studio. Same face, same size, same tracking: the only thing separating it
   from the name is weight, which is the whole point of it.

   200, not 300. Outfit's stem at 20px is about 1.5px at Regular, 1.24px at
   Light and 1.0px at ExtraLight, so one step down is a quarter of a pixel and
   antialiasing eats it: 300 beside 400 at this size reads as the same weight
   badly rendered rather than as a lighter word. Two steps is visible. The bar
   sits on the paper background on inner pages and white-on-photograph over the
   hero, and ExtraLight holds up on both; anything lighter starts to break up on
   the dark. The axis reaches 100 if that is ever wanted.

   The space between the words is the literal one in the markup plus .12em of
   margin. The literal space stays so the name copies and reads as two words with
   no stylesheet at all; the margin is what makes the gap deliberate. Outfit's
   word space is .223em and the wordmark's own .04em tracking falls on both sides
   of it, so the two words already sat .303em apart. .12em takes that to .423em,
   about a tenth over half again a word space: enough to read as a gap that was
   chosen rather than one that came out loose, and short of the two words looking
   unrelated.

   The transition here is the second word coming *back*, and it is deliberately
   the slower half of the pair. See the 480px rule further down for the leaving
   half and for why the two are not symmetrical; nothing on this element moves
   at rest. It waits 240ms because that is how long the links take to go: they
   fade over 220ms with no delay on close, so the word comes back into the space
   only once they are out of it, rather than the two crossing through each other.
   200ms was close enough on paper and 20ms short in practice, which is the same
   mistake the leaving half made in a different direction.
   The curve is the one the links arrive on, so the bar has one motion and not
   two. */
/* The layer is permanent, and that is the point.
   An opacity transition is run on the compositor, so the browser lifts this span
   onto its own layer for the duration and drops it back afterwards. Text on a
   promoted layer is redrawn without subpixel antialiasing, so it changes weight
   at the moment the layer appears and again when it goes, and on the homepage
   this is white type on a photograph carrying the .on-dark text-shadow, which is
   the worst case for noticing it. That is the flash, and it is why two rounds of
   retiming did nothing: promotion happens when the transition is scheduled, not
   when it starts painting, so a delay cannot move it.
   Declaring will-change keeps the span on its own layer at all times. Nothing is
   promoted or demoted mid-animation, so its rasterisation never changes and
   there is nothing to see. It costs one small permanent layer, and this element
   is one word in the corner of every page. inline-block because promotion of a
   bare inline box is unreliable; for a span of text it is the same box, laid out
   on the same baseline. */
.wordmark-alt { font-weight: 200; margin-left: .12em; display: inline-block; will-change: opacity; transition: opacity 260ms cubic-bezier(.22,.61,.36,1) 240ms; }
nav { display: flex; gap: 28px; }
.site-header nav { font-size: 12px; }
/* min-height rather than padding, so the hit area clears 44px whatever the type
   size is. These sit in the top corner, where taps are least accurate. */
.site-header a { display: flex; align-items: center; min-height: 44px; transition: opacity 180ms ease; }
.site-header a:hover, .site-header a:focus-visible { opacity: .5; }

/* --- The menu ---------------------------------------------------------------
   The links collapse behind the button and expand back into the bar, in the
   place they occupy when there is no script at all. Everything here is scoped
   to .js-drawer, a class nav.js puts on <html>: without JavaScript no button is
   shown and the links simply stay in the bar. A menu that cannot open is worse
   than no menu.

   This is a disclosure, not a dialog. There is no scrim, the page does not
   lock, and focus is not trapped, because nothing is covered: the links come
   back to their own corner of the rail and the page behind stays entirely
   usable. */
.nav-toggle { display: none; }
/* The wrapper must not exist as far as layout is concerned, in either mode, or
   the nav's flex child is one div instead of three links. display:contents
   hands them to the nav, which is the row this header has always had. */
.nav-links { display: contents; }

.js-drawer .nav-toggle { display: inline-flex; align-items: center; justify-content: flex-end; min-width: 44px; min-height: 44px; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; }
.nav-toggle-bars { display: flex; flex-direction: column; gap: 5px; width: 22px; }
/* Hairlines, to match the rules the rest of the site is ruled with. */
/* will-change for the same reason .wordmark-alt has it: these are transitioned
   on the compositor, so without it each bar is promoted to its own layer as the
   animation starts and dropped when it ends. A 1px hairline is the worst thing
   to do that to, because it is re-rasterised at the promotion and again at the
   demotion and there is only one row of pixels to disagree about. Declaring it
   up front means they are rasterised once and never again. */
.nav-toggle-bars span { display: block; width: 100%; height: 1px; background: currentColor; will-change: transform, opacity; transition: transform 260ms ease, opacity 160ms ease; }
/* 1px bars with 5px between puts the middle bar 6px below the top one, which is
   the distance the outer two travel to meet it as a cross. */
body.nav-open .nav-toggle-bars span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
body.nav-open .nav-toggle-bars span:nth-child(2) { opacity: 0; }
body.nav-open .nav-toggle-bars span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* Taken out of flow so the button stays hard against the right edge whether the
   links are shown or not. In flow the collapsed nav would still hold its gap
   and the button would float short of the corner. Its right offset is the rail
   padding plus the button and the space beside it, which is why the padding is
   a variable rather than a number repeated here. */
.js-drawer .nav-drawer {
  position: absolute;
  top: 50%;
  right: calc(var(--rail-pad) + 22px + 26px);
  transform: translateY(-50%);
  display: flex;
  gap: 28px;
  white-space: nowrap;
  visibility: hidden;
  transition: visibility 0s linear 300ms;
}
body.nav-open .nav-drawer { visibility: visible; transition: visibility 0s; }

/* They arrive from the right, from under the button, a few milliseconds apart.
   The stagger is the only thing that makes this read as expanding rather than
   as three links being switched on. Delays are set in the open state only, so
   closing takes them all at once, which is what impatience wants. */
.js-drawer .nav-drawer a { opacity: 0; transform: translateX(12px); transition: opacity 220ms ease, transform 300ms cubic-bezier(.22,.61,.36,1); }
body.nav-open .nav-drawer a { opacity: 1; transform: none; }
body.nav-open .nav-drawer a:nth-child(1) { transition-delay: 40ms; }
body.nav-open .nav-drawer a:nth-child(2) { transition-delay: 90ms; }
body.nav-open .nav-drawer a:nth-child(3) { transition-delay: 140ms; }
.hero { min-height: 100svh; position: relative; background: #1d1c18; color: #fff; }
/* <picture> is a layout box, so percentage heights inside it resolve against
   the picture rather than the section. Give it an explicit block box. */
.hero > picture { display: block; }
/* Fills the viewport, so a frame that is not the screen's shape is cropped to
   fit. object-position is set per photograph from the studio's "Hero crop"
   field, so what survives is chosen rather than always the middle.

   This was briefly height:auto under a max-height, which stopped the crop
   entirely on any screen narrower than the photograph and moved the caption
   below the picture. It measured better (a phone in portrait was losing 65% of
   the frame's width) but the user preferred the full-bleed opening, which is a
   judgement about the site rather than about the arithmetic. Reverted
   2026-08-18. */
.hero img { width: 100%; height: 100svh; display: block; object-fit: cover; }
.hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,.12), transparent 45%, rgba(0,0,0,.28)); }
.hero-caption { position: absolute; z-index: 1; left: 28px; right: 28px; bottom: 26px; display: flex; align-items: end; justify-content: space-between; }
/* The ceiling matters more than the slope here. At the old clamp(32px, 4.7vw,
   72px) the headline stopped growing at 1532px wide, so on a 16" laptop, a 1920
   monitor and a 27" display it sat at the same 72px while the photograph behind
   it kept getting bigger — the type shrinking against the image on every large
   screen. It now keeps scaling to 104px. The floor is left near where it was:
   this is two lines of display type on a phone. */
/* Display type is Outfit — the same face as the wordmark, so the identity is one
   face rather than two. Tracking is looser than the Arial it replaces at every
   size: Arial took -.055em comfortably, but a geometric sans's round forms
   collide well before that. Scoped to .hero-caption rather than bare h1 so the
   studio's headings stay in the interface face. */
/* Leading, measured rather than guessed. Outfit's tallest letter reaches .738em
   and its deepest descender ('g') drops -.209em, so two lines clear each other
   from .947em between baselines. 1.00 leaves .053em of daylight in that worst
   case — about 5px at full size — and is tighter than the 1.06 Jost needed,
   whose extenders were much longer. The headline is editable in the studio, so
   this has to hold for any pair of letters, not just today's line. */
/* The box sits on the 28px gutter; the ink is what has to line up with the
   wordmark, so the headline is pulled left by its own first letter's side
   bearing. --ink-lsb is set on the element by the server, because the headline
   is editable and the bearing is a property of the letter it starts with: .082em
   for "P", .031em had it been "A". See src/type.js for the measurements and for
   why a single constant will not do. */
.hero-caption h1 { font-family: "Outfit", Arial, Helvetica, sans-serif; letter-spacing: -.02em; line-height: 1.00; margin-left: calc(var(--ink-lsb, 0) * -1em); }
h1 { margin: 0; font-size: clamp(34px, 5.2vw, 104px); line-height: .96; letter-spacing: -.055em; font-weight: 400; }
.hero-caption p { margin: 0; font-size: 11px; line-height: 1.45; letter-spacing: .06em; text-transform: uppercase; text-align: right; }
.hero-credit-meta { color: rgba(255,255,255,.7); }
@media (max-width: 640px) { .site-header { --rail-pad: 18px; padding: 6px var(--rail-pad); } .wordmark { font-size: 18px; } nav { gap: 20px; } .js-drawer .nav-drawer { gap: 20px; right: calc(var(--rail-pad) + 22px + 18px); } .hero-caption { left: 18px; right: 18px; bottom: 20px; align-items: start; flex-direction: column; gap: 20px; } .hero-caption p { text-align: left; } }


/* 320px phones are the only width where the expanded labels reach the
   wordmark. Shrinking them a step buys 30px, which clears it, and is better
   than the name disappearing: on every iPhone in portrait, 375px and up, there
   was never a collision to solve. Recompute rather than eyeball if the labels
   or the face change: the wordmark's width comes from the font file, and the
   labels are Arial's standard advances.

   Recomputed 2026-08-25, when the wordmark gained a second word. At 18px with
   .04em, "Calouro Collection" measures 8.150em plus tracking and the .12em that
   opens up the gap between the words, 162px, against the 69px "Calouro" alone
   took. The two rules further down, at 480px and 400px, are what that 93px
   costs. All of it is measured, so redo the arithmetic if the wordmark text
   changes: it is editable, and a longer second word moves every threshold
   here. */
@media (max-width: 350px) {
  .site-header nav { font-size: 11px; letter-spacing: .08em; }
  .js-drawer .nav-drawer { gap: 15px; right: calc(var(--rail-pad) + 22px + 14px); }
}

/* Both rules here are the wordmark's second word getting out of the way of the
   links, and both come from one measurement: how wide the labels actually are.

   Measure them as they render, not as they are written in the markup. The bar
   sets text-transform: uppercase and letter-spacing: .09em, and the nav inherits
   both, so the row is "WORK ABOUT CONTACT" at .09em and not "Work About
   Contact" at none. In Arial's standard advances that is 195px at 12px with the
   20px gaps, against 141px for the same words in mixed case with no tracking.
   Getting that wrong is what put the first version of this block at 400px, which
   was inside the broken range rather than above it, and left the menu sitting on
   the wordmark on every phone in portrait. */

/* The open menu is absolutely positioned over the bar with nothing behind it, so
   the links land on the wordmark rather than beside it. They start at the
   viewport width less 253px and the full wordmark ends at 180px, so they touch
   at 433px and overlap below it: every phone in portrait, up to and including
   the 430px Pro Max. With the second word gone the name ends at 87px instead,
   which is clear by 9px even at 320px, the narrowest width the bar is built for.

   So the second word fades for as long as the menu is open and comes back with
   it, which is the same disclosure the links themselves are, run backwards.
   Opacity rather than display, so nothing reflows and the name does not jump.
   480px rather than 433: it wants to be above every phone in portrait with room
   to spare, and the first attempt at this number was too tight by being too
   clever. Nothing between 433 and 480 is harmed by fading a word it had room
   for. */
@media (max-width: 480px) {
  /* Leaving is the fast half, and asymmetry is the whole point: the second word
     has to be out of the way before the links land on top of it, and coming back
     it has to wait until they have gone. Both halves fading at the same 220ms,
     which is what this was first, is what made the menu look untidy rather than
     abrupt: two things crossing through each other in the same 90px of bar.

     100ms, and ease-out rather than ease, so most of the opacity is gone inside
     the first 40ms. That number is not taste: the first link starts arriving at
     40ms over 220ms, and 100ms is where the curves stop crossing while both are
     still readable. Plotted against each other, the most the two are ever
     simultaneously visible is .14, and there is no moment at all where both sit
     above .15. At 150ms that window is 32ms long, at the 220ms this started as,
     79ms. Below about 60ms a fade stops reading as a fade and starts reading as
     a flicker, so this is the far end of the useful range, not the middle. */
  body.nav-open .wordmark-alt { opacity: 0; transition: opacity 100ms ease-out; }

  /* And the links wait for it to go.
     Fading the word out faster was not enough on its own, because "faster" and
     "sooner" are not the same thing: both transitions still started on the same
     tap, so for the first 100ms the links were arriving on top of a word that
     was still there. That superimposition is what read as the second half of
     "Collection" flashing, and second half is literal. The word spans 94px to
     179px in the bar; the first link starts at 137px on a 390px phone, which is
     its exact midpoint, and lands on "ction". At 375px it takes "lection", at
     360px all but the C.
     So on the widths where they share that space, the stagger starts after the
     word has finished leaving rather than with it: 100ms to clear, then 20ms of
     daylight, then the links begin. Above 480px they never overlap and keep the
     original 40/90/140. The button's own bars still animate from the tap, so the
     menu answers immediately even though its links now start a fifth of a second
     in. */
  body.nav-open .nav-drawer a:nth-child(1) { transition-delay: 120ms; }
  body.nav-open .nav-drawer a:nth-child(2) { transition-delay: 170ms; }
  body.nav-open .nav-drawer a:nth-child(3) { transition-delay: 220ms; }
}

/* With no script the links never leave the bar, so the same 195px sits beside
   the wordmark permanently rather than only while a menu is open. 36px of
   padding plus 162px of wordmark plus those labels is 392px, so the bar stops
   holding all three below about 393px. The second word is dropped rather than
   the links becoming unreachable, which is the same trade the label-shrinking
   rule above makes. Script is the common case and keeps the full name at every
   width; this is the fallback's fallback, which is why it can afford to sit at
   the measurement rather than well above it. */
@media (max-width: 400px) {
  html:not(.js-drawer) .wordmark-alt { display: none; }
}
/* The gallery was the one content region on the site with no ceiling, so it
   grew with the monitor while .photograph, .about and the lightbox all stopped
   at 1500. Same measure here, so the homepage agrees with the pages it leads
   to. The hero above is deliberately exempt: it is full-bleed by design. */
.work { padding: 88px 28px 130px; max-width: 1500px; margin: 0 auto; }
/* Every rule-topped header on the page shares one measure: a 1px rule, 11px of
   air, then 10px uppercase text on a tight line box so it sits hard against the
   rule rather than floating below it. */
.section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 24px; border-top: 1px solid rgba(23,23,19,.45); padding-top: 11px; font-size: 10px; line-height: 1; letter-spacing: .09em; text-transform: uppercase; }
.section-head h2, .section-head p { font: inherit; margin: 0; }
.section-head p { text-align: right; color: rgba(23,23,19,.62); }
.project-intro { padding: 64px 0 76px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: end; }
/* The quote is the one serif on the page, which is the point: everything else
   here is Arial or Outfit, so a serif reads immediately as someone else speaking
   rather than as the site's own voice.

   Georgia held this for an hour and was too plain for it: it is a workhorse cut
   for screen legibility, not a face with much character. EB Garamond is a book
   face, which is what "literary" actually asks for.

   The numbers are measured off the file, not guessed, which is the whole reason
   for self-hosting rather than leaning on a system serif:

   - x-height .400em, against Arial's .519. It needs 1.30x the pixel size to read
     the same, which is why the ceiling is 70px where the Arial paragraph was 58.
     Held slightly under true parity so a long quote does not run away down the
     column.
   - tallest letter .706em, deepest -.290em, so lines clear at .996em. Leading is
     1.10, which keeps .104em of daylight in the worst case and gives the block the
     air a book would.
   - Tracking -.01em. Old-style serifs want to be left nearly alone; the -.05em
     Arial carried would close up the space its brackets are meant to hold. */
/* The opening quotation mark hangs, so every letter of the quote lines up and
   only the mark sits outside. -.418em is the mark's exact advance in this cut,
   measured off the file rather than rounded to something that looks about right,
   and text-indent affects only the first line, which is the one that needs it.

   The figure carries the padding it hangs into, and that padding is not optional.
   Without it the mark hangs off the *page*: `.work` has 28px of padding, so a 26px
   negative indent put the mark 1.7px from the edge of the screen. Hanging
   punctuation needs a margin to hang into, and at the left edge of this column
   there is none to borrow. Matching that padding to the indent moves the block in
   by the mark's width, so the mark lands on the column edge, where the rule above
   it starts, and the letters sit just inside.

   It sits on the figure rather than the blockquote for two reasons, both about the
   em resolving against the right thing. `.418em` is the mark's advance *in the
   quote's size*, so whatever carries the padding must be set at that size: the
   figure takes the clamp and the blockquote inherits it. And everything inside the
   figure then shares one text edge, which is what puts the attribution's em dash
   under the first letter of the quote rather than under the quotation mark. With
   the padding on the blockquote the figcaption sat outside it, on the column edge,
   lined up with the mark. */
.work-quote { margin: 0; font-size: clamp(30px, 4.4vw, 70px); padding-left: .418em; }
.work-quote blockquote { margin: 0; font-family: "EB Garamond", Georgia, "Times New Roman", serif; font-size: inherit; line-height: 1.10; letter-spacing: -.01em; text-indent: -.418em; }
/* The attribution stays a label, in the register the rest of the furniture uses,
   so the quote reads as the voice and this reads as the credit. */
.work-quote figcaption { margin-top: 22px; font-size: 10px; line-height: 1; letter-spacing: .09em; text-transform: uppercase; color: rgba(23,23,19,.62); }
/* 15px for the section name, asked for on 2026-08-19 after 13px the same day. It
   went 10px -> 11px on 2026-08-18, on the reasoning that beside a 70px quote a
   10px row read as a caption rather than as a way in; that reasoning did not stop
   being true at 11px or at 13px. The old note here warned that a large link
   starts competing with the quote: it does not, because the quote is still four
   times the size and the only thing that grew is the one line a visitor is meant
   to click.

   Tracking stays at .09em, which is the house value for uppercase furniture and
   still reads at 15px. Uppercase needs *less* tracking as it grows, not more, so
   if the name ever goes past this, tighten toward .06em rather than leaving this
   alone -- at that point it is a heading and wants setting as one.

   The count and the arrow stay at 11px. That is what keeps this from reading as
   a heading: the name is the thing that got bigger, and the furniture around it
   did not, so the row gains emphasis without gaining weight. They sit on the
   name's baseline, which the row already aligns to.

   The padding is the more useful half of this. The link was about 15px of tap
   target, on a site whose own header rule is a 44px minimum with a comment
   explaining why. 24px above and 8px below puts the row past 44px without moving
   anything visible: the underline still sits tight under the text, and the gap
   between rows comes from the padding now rather than from a flex gap. */
.project-sections { justify-self: end; display: flex; flex-direction: column; align-items: flex-end; gap: 0; font-size: 15px; text-transform: uppercase; letter-spacing: .09em; }
.project-sections a { display: flex; align-items: baseline; gap: 12px; padding: 24px 0 8px; border-bottom: 1px solid rgba(23,23,19,.25); }
.project-sections a:hover, .project-sections a:focus-visible { border-bottom-color: var(--ink); }
.section-count { font-size: 11px; color: rgba(23,23,19,.5); }
/* A down arrow, not a right one: these jump down this page to a chapter, they
   do not go anywhere else, and a right arrow would promise a page that does not
   exist. It is here because the rows read as a list until they are pointed at:
   the underline alone is furniture on a site whose headers are all rules.

   Dimmer than the count so the corner stays as quiet as it was meant to be --
   the arrow is what darkens and drops on hover, not the whole row. aria-hidden
   in the markup: the link already reads its own name. */
.section-arrow { font-size: 11px; color: rgba(23,23,19,.38); transition: color 160ms ease, transform 160ms ease; }
.project-sections a:hover .section-arrow, .project-sections a:focus-visible .section-arrow { color: var(--ink); transform: translateY(2px); }
.gallery { display: block; }
.chapter + .chapter { margin-top: 180px; }
/* Stacked, in reading order: number on the rule, a clear drop, then the title
   with its note beneath. Nothing is optically aligned to anything of a
   different size, so there is no cramming and nothing to drift at other widths. */
.chapter-head { border-top: 1px solid rgba(23,23,19,.45); padding: 11px 0 52px; }
.chapter-head > span { display: block; font-size: 10px; line-height: 1; letter-spacing: .09em; text-transform: uppercase; }
.chapter-head h3 { margin: 76px 0 0; font-family: "Outfit", Arial, Helvetica, sans-serif; font-size: clamp(22px, 2.5vw, 36px); line-height: 1.00; letter-spacing: -.015em; font-weight: 400; }
.chapter-head > p { margin: 14px 0 0; font-size: 10px; line-height: 1; letter-spacing: .09em; text-transform: uppercase; color: rgba(23,23,19,.62); }

/* The original hardcoded placement for exactly three figures per chapter, so a
   fourth photograph collapsed to 1/12 of the page width. The rhythm is now
   generalised: one full-bleed lead image, then balanced pairs, for any count. */
.chapter-images { display: grid; grid-template-columns: repeat(12, 1fr); gap: 72px 18px; }
.chapter-images figure { grid-column: 1 / -1; }
.chapter-images figure:nth-child(even) { grid-column: 1 / span 6; }
.chapter-images figure:nth-child(odd):not(:first-child) { grid-column: 7 / -1; }
/* A trailing unpaired image reads as a deliberate full-bleed close. */
.chapter-images figure:nth-child(even):last-child { grid-column: 1 / -1; }

figure { margin: 0; }
.image-button { appearance: none; border: 0; padding: 0; display: block; width: 100%; background: #ddd; cursor: zoom-in; overflow: hidden; color: inherit; text-decoration: none; }
/* No photograph in the gallery is cropped. home.js writes each one's own ratio
   as an inline style, which beats the ratios here, so these now only apply to a
   row stored without dimensions — a fallback, not the design. Pairs of
   differently-shaped photographs therefore finish at different heights, which is
   inherent to showing them whole. */
.image-button { aspect-ratio: 16 / 10; }
.chapter-images figure:not(:first-child) .image-button { aspect-ratio: 4 / 5; }
.chapter-images figure:nth-child(even):last-child .image-button { aspect-ratio: 16 / 10; }
.image-button picture { display: block; width: 100%; height: 100%; }
/* No hover treatment on the photographs themselves. The old rule scaled them
   1.5% and dropped saturation to 92%, which showed visitors a subtly altered
   version of the work. `cursor: zoom-in` above is the affordance. */
.image-button img { display: block; width: 100%; height: 100%; object-fit: cover; }
figcaption { margin-top: 9px; display: flex; justify-content: space-between; gap: 20px; font-size: 9px; letter-spacing: .08em; text-transform: uppercase; }
.empty-work { padding: 120px 0 60px; text-align: center; font-size: 13px; letter-spacing: .04em; color: rgba(23,23,19,.6); }
.eyebrow { margin: 0; border-top: 1px solid rgba(23,23,19,.45); padding-top: 11px; font-size: 10px; line-height: 1; letter-spacing: .09em; text-transform: uppercase; }
/* No min-height: the footer is a name and three short lines, so tying it to
   the viewport reserved 835px of empty dark space on a tall monitor. The h2
   carries explicit margins now — `margin: auto 0` existed only to distribute
   the slack that the min-height created. */
/* Pages other than the homepage -------------------------------------------
   There is no hero for the fixed rail to sit over, so the content starts below
   it rather than under it. */
/* Positioned, so the rail sentinel can hang off the top of the page. */
.single { position: relative; padding-top: 68px; }
/* Stands in for the hero on a page that has none: while it covers the top sliver
   of the viewport the rail stays bare, and it bands once the page has moved
   under it. Absolutely positioned inside the 68px of clearance that is already
   empty, so it occupies no layout and shifts nothing. 40px rather than the
   rail's full 60px because banding a little early is invisible, while banding
   late would show content through a transparent bar.

   12px, down from 40px: the bar should read as answering the scroll, and 40px
   put its arrival well into the gesture, which felt like it popped in at random.
   Small enough that a reader rarely comes to rest on the threshold, and the
   180ms ease on .site-header covers the switch either way. */
.rail-sentinel { position: absolute; top: 0; left: 0; width: 1px; height: 12px; pointer-events: none; }
.photograph, .about, .not-found { padding: 40px 28px 96px; max-width: 1500px; margin: 0 auto; }
.photograph figure { margin: 0; }
.photograph img { display: block; width: 100%; height: auto; background: #ddd; }

/* Rule-topped, like every other header on the site: 1px, 11px of air, then the
   content. The title takes two thirds; the facts list takes the last third and
   is the only thing allowed in that column, so nothing floats in it. */
.photograph-facts { display: grid; grid-template-columns: 2fr 1fr; gap: 30px 48px; align-items: start; margin-top: 34px; border-top: 1px solid rgba(23,23,19,.45); padding-top: 11px; }
.photograph-title h1 { margin: 0; font-family: "Outfit", Arial, Helvetica, sans-serif; font-size: clamp(28px, 3.4vw, 52px); line-height: 1.00; letter-spacing: -.02em; font-weight: 400; }
/* Sits directly under the title it describes, indented to nothing and measured
   to about 46 characters so it stays a note rather than becoming a paragraph. */
.photograph-note { margin: 20px 0 0; }
.photograph-note p { margin: 0; font-size: 13px; line-height: 1.6; max-width: 46ch; color: rgba(23,23,19,.78); }

.photograph-spec { margin: 4px 0 0; display: grid; gap: 16px; }
.photograph-spec > div { display: grid; gap: 5px; }
.photograph-spec dt { font-size: 9px; letter-spacing: .09em; text-transform: uppercase; color: rgba(23,23,19,.55); }
.photograph-spec dd { margin: 0; font-size: 12px; letter-spacing: .04em; }
.photograph-spec dd a { border-bottom: 1px solid rgba(23,23,19,.35); }
.photograph-spec dd a:hover, .photograph-spec dd a:focus-visible { border-bottom-color: var(--ink); }

.photograph-nav { display: flex; justify-content: space-between; align-items: baseline; gap: 24px; margin-top: 64px; border-top: 1px solid rgba(23,23,19,.45); padding-top: 11px; font-size: 10px; line-height: 1; letter-spacing: .09em; text-transform: uppercase; }
.photograph-nav a:hover, .photograph-nav a:focus-visible { opacity: .55; }
.photograph-nav .all { color: rgba(23,23,19,.62); }
.photograph-nav .next { text-align: right; }

.about h1, .not-found h1 { margin: 30px 0 0; font-family: "Outfit", Arial, Helvetica, sans-serif; font-size: clamp(34px, 4.4vw, 64px); line-height: 1.00; letter-spacing: -.02em; font-weight: 400; }
.about-standfirst { margin: 26px 0 0; font-size: clamp(15px, 1.5vw, 20px); line-height: 1.5; max-width: 40ch; }
.about-body { display: grid; grid-template-columns: 2fr 1fr; gap: 48px; margin-top: 54px; }
.about-prose p { margin: 0 0 18px; font-size: 14px; line-height: 1.7; max-width: 62ch; }
.about-prose p:last-child { margin-bottom: 0; }
.about-aside p:not(.eyebrow) { margin: 16px 0 0; font-size: 11px; line-height: 1.9; letter-spacing: .04em; color: rgba(23,23,19,.72); }
.about-portrait { margin: 64px 0 0; max-width: 46%; }
.about-portrait img { display: block; width: 100%; height: auto; background: #ddd; }
.about-portrait figcaption { margin-top: 9px; font-size: 9px; letter-spacing: .08em; text-transform: uppercase; color: rgba(23,23,19,.62); }

.not-found-actions { margin: 40px 0 0; font-size: 10px; letter-spacing: .09em; text-transform: uppercase; }
.not-found-actions a { border-bottom: 1px solid rgba(23,23,19,.45); }

@media (max-width: 760px) {
  .photograph, .about, .not-found { padding: 26px 18px 64px; }
  .photograph-facts, .about-body { grid-template-columns: 1fr; gap: 24px; }
  .photograph-spec { grid-auto-flow: column; grid-auto-columns: 1fr; gap: 14px; }
  .about-portrait { max-width: 100%; }
  .photograph-nav { flex-wrap: wrap; gap: 14px; }
  .photograph-nav .all { order: 3; flex-basis: 100%; }
}

footer { color: #f3f0e9; background: #171713; padding: 40px 28px 26px; display: flex; flex-direction: column; }
footer .eyebrow { border-color: rgba(255,255,255,.35); }
footer h2 { margin: 40px 0 48px; font-family: "Outfit", Arial, Helvetica, sans-serif; font-size: clamp(34px, 4.4vw, 64px); line-height: 1.00; letter-spacing: -.02em; font-weight: 400; }
footer h2 em { font-family: Georgia, serif; font-weight: 400; }
.contact-row { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 24px; font-size: 11px; line-height: 1.5; padding-bottom: 32px; }
.contact-row > a { font-size: clamp(18px, 2vw, 28px); text-decoration: underline; text-underline-offset: 5px; }
.contact-row div { display: flex; flex-direction: column; }
.contact-row p { margin: 0; }
.footer-base { border-top: 1px solid rgba(255,255,255,.35); padding-top: 11px; display: flex; justify-content: space-between; align-items: baseline; gap: 24px; text-transform: uppercase; font-size: 10px; line-height: 1; letter-spacing: .09em; }

/* Opaque, not the .96 it was: at four percent the gallery behind stayed just
   visible enough to read as movement while looking at the photograph. */
/* --lb-chrome: everything in the viewer's height that is not the photograph, so
   the photograph can be given what is actually left. 50px of padding top and
   bottom, then 52px for the close button (12px), the caption (16px) and the two
   12px gaps between the three rows, then 8px more. The 8px is deliberate slack:
   whenever the budget below binds rather than the fraction, the total lands
   exactly on the viewport height, and those row heights are computed from font
   metrics rather than measured in a browser. Overstating the chrome by 8px is
   what turns an exact fit into a fit. Restated wherever the padding changes. */
.lightbox { --lb-chrome: 160px; position: fixed; z-index: 100; inset: 0; background: #0c0c0b; display: grid; place-items: center; padding: 50px clamp(16px, 4vw, 50px); color: #fff; animation: fade-in 220ms ease both; }
.lightbox[hidden] { display: none; }
/* The frame shrink-wraps the photograph — no object-fit, so the img box is the
   picture's own box — which is what lets the close button and the caption sit
   against its edges instead of the viewport's. They were previously pinned to
   the corners, so a portrait photograph left the caption stranded a long way
   below it.

   The width cap therefore has to live on the frame, not on the img. It used to
   sit on both and they disagreed: the img was capped at 92vw while the frame
   was capped by the 50px padding at 100vw - 100px. Those cross over at 1250px,
   so on anything narrower than that (and wider than the 640px rule below) the
   picture was the wider of the two and simply overflowed the frame to the
   right. The photograph sat off-centre, and the close button and the caption,
   which align to the frame, stopped lining up with its edges. An iPad Pro 11"
   in portrait is 834px, right in the middle of that band.

   Now the frame carries the cap and the img is 100% of whatever the frame
   resolved to, so the two cannot disagree at any width. The horizontal padding
   is on the same 4vw rail as the arrows, which makes the frame's available
   width exactly the 92vw the design asks for below 1250px instead of fighting
   it. min() keeps every width that was already right: 92vw still binds on
   mobile and on the desktop, 1500px still binds beyond 1630px. */
.lightbox-frame { display: grid; gap: 12px; width: fit-content; max-width: min(100%, 92vw, 1500px); }
/* 76vh is the design's share of a tall screen; the calc is what stops that share
   exceeding the screen on a short one. The viewer does not scroll, so anything
   over the viewport is simply cut off, and 76vh plus 152px of chrome is taller
   than the viewport at every height below 633px. min() takes whichever binds, so
   there is no breakpoint to fall through: the fraction wins when there is room
   and the budget wins when there is not.

   dvh, not vh, and a phone in landscape is the case that needs it. On iOS vh is
   the large viewport, the height the page would have if the browser furniture
   retracted, which is taller than what is actually on screen with the toolbar
   up. That gap is slack in portrait and is most of the problem in landscape. The
   plain vh line before it is the fallback for anything that does not know the
   unit and will ignore the second declaration. */
.lightbox img { display: block; width: auto; height: auto; max-width: 100%; max-height: 76vh; max-height: min(76vh, calc(100dvh - var(--lb-chrome))); }
.lightbox .close { justify-self: end; padding: 0; background: none; border: 0; color: #fff; cursor: pointer; text-transform: uppercase; letter-spacing: .08em; font-size: 10px; }
/* The arrows were bare white type on the ground, which vanished the moment a
   photograph ran wide enough to sit behind them. They are now discs: a dark
   translucent ground and a hairline border keep them readable over a bright
   frame as well as a dark one, and the glyph is a drawn chevron rather than a
   10px text arrow, which rendered differently in every face. */
.lightbox-nav { position: absolute; z-index: 2; top: 50%; transform: translateY(-50%); display: grid; place-items: center; width: 52px; height: 52px; padding: 0; border-radius: 50%; border: 1px solid rgba(255,255,255,.4); background: rgba(12,12,11,.55); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); box-shadow: 0 2px 20px rgba(0,0,0,.5); color: #fff; cursor: pointer; transition: background-color 160ms ease, border-color 160ms ease; }
.lightbox-nav svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
.lightbox-nav:hover, .lightbox-nav:focus-visible { background: rgba(12,12,11,.92); border-color: #fff; }
.lightbox-nav:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.lightbox-nav.previous { left: clamp(18px, 4vw, 60px); }
.lightbox-nav.next { right: clamp(18px, 4vw, 60px); }
.lightbox p { margin: 0; display: flex; justify-content: space-between; gap: 20px; text-transform: uppercase; letter-spacing: .08em; font-size: 9px; }
/* The credit is not a link — a photograph's title and place should read as a
   caption. The link sits beside it and says what it is. */
.lightbox-credit { display: flex; align-items: baseline; gap: 14px; min-width: 0; }
#lightbox-caption { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The credit row is uppercase at 9px with .08em tracking, which is the
   site's label idiom but made the link read as a shout in brackets. The
   link opts out: sentence case, no tracking (that spacing is for caps),
   and 11px, because lowercase at 9px would sit visibly under the
   caption's cap height rather than beside it. */
.lightbox p a { flex: none; color: rgba(255,255,255,.72); border-bottom: 1px solid rgba(255,255,255,.35); padding-bottom: 2px; text-transform: none; letter-spacing: normal; font-size: 11px; transition: color 160ms ease, border-color 160ms ease; }
.lightbox p a:hover, .lightbox p a:focus-visible { color: #fff; border-bottom-color: #fff; }
.lightbox img { animation: image-in 260ms ease both; }
@keyframes fade-in { from { opacity: 0; } }
@keyframes image-in { from { opacity: 0; transform: translateX(5px); } }
@media (prefers-reduced-motion: reduce) { .section-arrow { transition: color 160ms ease; } .project-sections a:hover .section-arrow, .project-sections a:focus-visible .section-arrow { transform: none; } html { scroll-behavior: auto; } .lightbox, .lightbox img { animation: none; } .account-dropdown { transition: none; transform: none; } .js-drawer .nav-drawer a, .nav-toggle-bars span, .wordmark-alt { transition: none; } body.nav-open .nav-drawer a:nth-child(n) { transition-delay: 0s; } body.nav-open .wordmark-alt { transition: none; } }
@media (max-width: 640px) {
  .work { padding: 64px 18px 84px; }
  .project-intro { padding: 48px 0 56px; grid-template-columns: 1fr; gap: 32px; }
  .project-sections { justify-self: start; align-items: flex-start; }
  .chapter + .chapter { margin-top: 110px; }
  .chapter-head { padding-bottom: 34px; }
  .chapter-head h3 { margin-top: 44px; }
  .chapter-images { display: block; }
  .chapter-images figure { margin-bottom: 54px; }
  .chapter-images .image-button { aspect-ratio: 4 / 5; }
  figcaption { align-items: start; }
  figcaption span:last-child { text-align: right; }
  footer { padding: 28px 18px 20px; }
  footer h2 { margin: 30px 0 34px; }
  .contact-row { grid-template-columns: 1fr 1fr; gap: 44px 15px; }
  .contact-row > a { grid-column: 1 / -1; }
  .lightbox { --lb-chrome: 132px; padding: 36px 16px; }
  .lightbox-nav { top: auto; bottom: 48px; transform: none; width: 46px; height: 46px; }
  .lightbox-nav.previous { left: 8px; }
  .lightbox-nav.next { right: 8px; }
  .lightbox-nav svg { width: 19px; height: 19px; }
  /* The arrows sit at the foot of the viewport on a phone, so the frame needs
     to end above them rather than run underneath. */
  .lightbox img { max-height: 66vh; max-height: min(66vh, calc(100dvh - var(--lb-chrome))); }
  .lightbox-nav { bottom: 20px; }
}

/* A phone in landscape, which is the one shape the viewer had never been sized
   for. It is not caught by the 640px rule above: an iPhone 17 Pro on its side is
   874px wide, so it takes the desktop viewer, and the desktop viewer's 50px of
   padding top and bottom is a quarter of a 402px landscape viewport. The cap
   above already stops the photograph overflowing on its own; this is what stops
   it being squeezed into a letterbox to do it. Dropping the padding to 16px
   hands 68px back to the picture.

   520px because it has to sit above every phone in landscape (430px at the
   largest) and below an iPad in either orientation (768px at the smallest), so
   it catches the case it is for and nothing else. */
@media (max-height: 520px) {
  .lightbox { --lb-chrome: 92px; padding-block: 16px; }
}
}

/* Private content studio */
.studio-shell { min-height: 100vh; background: #e8e5de; padding: 0 38px 100px; }
.studio-header { height: 68px; display: grid; grid-template-columns: 1fr auto 1fr; grid-template-areas: "brand nav account"; align-items: center; border-bottom: 1px solid rgba(23,23,19,.3); font-size: 10px; letter-spacing: .09em; text-transform: uppercase; }
.studio-wordmark { grid-area: brand; font-weight: 600; }
.studio-account { grid-area: account; justify-self: end; }
.studio-header form { display: inline; }

/* Tabs sit in the header bar itself and rest on its bottom rule, so the studio
   opens with one horizontal line instead of a stray row of links under it. */
.studio-nav { grid-area: nav; align-self: stretch; display: flex; align-items: stretch; gap: 30px; }
.studio-nav a { display: flex; align-items: center; margin-bottom: -1px; border-bottom: 2px solid transparent; color: rgba(23,23,19,.5); transition: color .15s ease; }
.studio-nav a:hover { color: var(--ink); }
.studio-nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ink); }
.studio-header button.linklike { border: 0; background: none; padding: 0; cursor: pointer; font: inherit; text-transform: uppercase; letter-spacing: .09em; }

/* Account menu. The trigger is a real link to /studio/account, so the menu is a
   shortcut rather than the only way in — pointers get it on hover, keyboards on
   focus, and touch simply follows the link. */
.account-menu { position: relative; }
.account-trigger { display: block; max-width: 200px; padding: 8px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-bottom: 1px solid transparent; }
.account-menu:hover .account-trigger, .account-menu:focus-within .account-trigger { border-bottom-color: rgba(23,23,19,.45); }
.account-dropdown { position: absolute; top: 100%; right: 0; z-index: 30; min-width: 184px; padding: 8px 0; background: #f5f3ed; border: 1px solid rgba(23,23,19,.25); opacity: 0; visibility: hidden; transform: translateY(-4px); transition: opacity .14s ease, transform .14s ease, visibility .14s; }
.account-menu:hover .account-dropdown, .account-menu:focus-within .account-dropdown { opacity: 1; visibility: visible; transform: none; }
.account-dropdown form { display: block; }
.account-dropdown a, .account-dropdown button.linklike { display: block; width: 100%; padding: 10px 16px; text-align: left; font: inherit; font-size: 10px; letter-spacing: .09em; text-transform: uppercase; }
.account-dropdown a:hover, .account-dropdown button.linklike:hover { background: rgba(23,23,19,.07); }
.studio-intro { display: grid; grid-template-columns: 1fr 2fr 1fr; align-items: end; gap: 30px; padding: 100px 0 80px; }
.studio-intro > p:first-child { align-self: start; text-transform: uppercase; letter-spacing: .09em; font-size: 9px; }
.studio-intro h1 { font-size: clamp(58px, 9vw, 132px); line-height: .82; font-weight: 400; letter-spacing: -.065em; }
.studio-intro > p:last-child { margin: 0; font-size: 13px; line-height: 1.5; }
.upload-panel, .library { background: #f5f3ed; border: 1px solid rgba(23,23,19,.25); padding: 30px 30px 34px; }

/* Panel heading: a quiet label closed by a hairline, so it reads as a header
   for what follows instead of a line of text sitting on top of the fields.
   The rule does the separating, which is what lets the gaps be generous
   without the heading drifting away from its own panel. */
.upload-panel h2, .library-head h2 { margin: 0; font-size: 11px; letter-spacing: .1em; font-weight: 500; text-transform: uppercase; color: rgba(23,23,19,.62); }
.upload-panel h2 { padding-bottom: 18px; border-bottom: 1px solid rgba(23,23,19,.18); }
.account-note { margin: 22px 0 0; max-width: 64ch; font-size: 12px; line-height: 1.55; color: rgba(23,23,19,.7); }
.upload-panel form, .record-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 28px 20px; margin-top: 32px; }
.upload-panel label, .record-fields label { display: flex; flex-direction: column; gap: 8px; }
.upload-panel label > span, .record-fields label > span { font-size: 9px; letter-spacing: .08em; text-transform: uppercase; }
.upload-panel input, .upload-panel select, .upload-panel textarea, .record-fields input, .record-fields select, .record-fields textarea { width: 100%; border: 0; border-bottom: 1px solid rgba(23,23,19,.35); border-radius: 0; background: transparent; color: inherit; padding: 9px 0; font: inherit; font-size: 14px; outline: none; }
.upload-panel input:focus, .upload-panel select:focus, .upload-panel textarea:focus, .record-fields input:focus, .record-fields select:focus, .record-fields textarea:focus { border-bottom-color: var(--ink); }
.upload-panel textarea, .record-fields textarea { resize: vertical; }
.upload-panel .span-two, .record-fields .span-two { grid-column: 1 / -1; }
.file-field input { border: 1px dashed rgba(23,23,19,.4); padding: 20px; }
.publish-field { flex-direction: row !important; align-items: center; align-self: center; gap: 9px !important; }
.publish-field input { width: 15px !important; margin: 0; }
/* Buttons had no hover, focus or press state of any kind: the only sign a click
   had registered was the page changing, which on a slow save is a second or more
   of nothing. Colour only, no lift or shadow, because the page is flat and a
   depth cue here would be the only one on it.

   :hover is guarded with :not(:disabled) so a button already working does not
   light up under the cursor, and the outlined variants restate their own hover
   because they are less specific than the base rule and would otherwise take the
   dark one. */
.studio-button { justify-self: end; align-self: end; border: 1px solid var(--ink); background: var(--ink); color: #fff; padding: 12px 18px; cursor: pointer; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease; }
.studio-button:hover:not(:disabled) { background: #33322b; border-color: #33322b; }
.studio-button:active:not(:disabled) { background: var(--ink); border-color: var(--ink); }
/* The custom background swallows the browser's default ring, so the focus state
   is drawn outside the button where it stays visible on either variant. */
.studio-button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.studio-button:disabled { opacity: .45; cursor: wait; }
.studio-button.danger { background: none; color: #8a2018; border-color: #8a2018; }
/* Destructive: fills rather than lightens, so the difference from Save is not a
   shade of the same thing. */
.studio-button.danger:hover:not(:disabled) { background: #8a2018; color: #fff; }
.studio-button.danger:active:not(:disabled) { background: #6d1912; border-color: #6d1912; }
.studio-button.danger:focus-visible { outline-color: #8a2018; }
.record-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; gap: 12px; }
/* Upload progress. The bar is deliberately a hairline: it is a status line in
   a text-first back office, not a component. Its indeterminate state (the value
   attribute removed) is what shows while the server encodes. */
.upload-progress { grid-column: 1 / -1; display: grid; gap: 10px; }
.upload-progress progress { appearance: none; -webkit-appearance: none; display: block; width: 100%; height: 3px; border: 0; background: rgba(23,23,19,.15); color: var(--ink); }
.upload-progress progress::-webkit-progress-bar { background: rgba(23,23,19,.15); }
.upload-progress progress::-webkit-progress-value { background: var(--ink); transition: width 120ms linear; }
.upload-progress progress::-moz-progress-bar { background: var(--ink); }
.upload-progress p { margin: 0; font-size: 10px; line-height: 1.5; letter-spacing: .08em; text-transform: uppercase; color: rgba(23,23,19,.62); }

/* One save covers the whole library, so the button belongs to the library head
   rather than to each record. The foot repeats it so a long list need not be
   scrolled back to the top. */
.library-save { margin: 0; }
.library-foot { display: flex; justify-content: flex-end; padding-top: 26px; }

.studio-status { min-height: 48px; padding: 16px 2px; font-size: 11px; }
.studio-status.error { color: #8a2018; }
.library { margin-top: 28px; }
.library-head { display: flex; justify-content: space-between; align-items: baseline; gap: 24px; padding-bottom: 18px; border-bottom: 1px solid rgba(23,23,19,.25); }
.library-head span { font-size: 10px; letter-spacing: .09em; text-transform: uppercase; color: rgba(23,23,19,.62); }
.empty-library { padding: 60px 0; text-align: center; color: rgba(23,23,19,.65); }
.photo-record { display: grid; grid-template-columns: minmax(220px, .8fr) 2fr; gap: 30px; padding: 30px 0; border-bottom: 1px solid rgba(23,23,19,.25); }
.photo-record > img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; background: #d8d4cb; }
.record-fields { margin-top: 0; grid-template-columns: repeat(3, 1fr); }
.draft-flag { display: inline-block; margin-left: 10px; padding: 3px 7px; border: 1px solid rgba(23,23,19,.4); font-size: 8px; letter-spacing: .08em; }
.hero-flag { align-self: center; margin-right: auto; font-size: 9px; letter-spacing: .08em; text-transform: uppercase; color: rgba(23,23,19,.6); }
.studio-button.subtle { background: none; color: var(--ink); }
.studio-button.subtle:hover:not(:disabled) { background: rgba(23,23,19,.07); border-color: var(--ink); color: var(--ink); }
.studio-button.subtle:active:not(:disabled) { background: rgba(23,23,19,.14); }
/* Site text page. One field per row — these are sentences, not short values,
   so they get the full width rather than being packed into columns. */
.copy-panel + .copy-panel { margin-top: 28px; }
.copy-fields { display: grid; gap: 28px; margin-top: 32px; }
.copy-fields label { display: flex; flex-direction: column; gap: 8px; }
.copy-fields label > span { font-size: 9px; letter-spacing: .08em; text-transform: uppercase; }
.copy-fields input, .copy-fields textarea { width: 100%; border: 0; border-bottom: 1px solid rgba(23,23,19,.35); border-radius: 0; background: transparent; color: inherit; padding: 9px 0; font: inherit; font-size: 14px; outline: none; }
.copy-fields input:focus, .copy-fields textarea:focus { border-bottom-color: var(--ink); }
.copy-fields textarea { resize: vertical; line-height: 1.5; }
.field-optional { font-style: normal; color: rgba(23,23,19,.45); }
.field-help { font-size: 11px; line-height: 1.45; color: rgba(23,23,19,.6); }
.copy-actions { display: flex; justify-content: flex-end; }

/* Sections page. Three columns so Name / Short label / Order fill a row exactly
   and nothing is left sitting beside an empty cell. */
.upload-panel .section-form { grid-template-columns: repeat(3, 1fr); margin-top: 32px; }
.category-record { padding: 30px 0; border-bottom: 1px solid rgba(23,23,19,.25); }
.category-record:last-child { border-bottom: 0; padding-bottom: 4px; }
.category-record .record-fields { margin-top: 0; grid-template-columns: repeat(3, 1fr); }
.category-meta { align-self: center; margin-right: auto; font-size: 9px; letter-spacing: .08em; text-transform: uppercase; color: rgba(23,23,19,.6); }

.account-panel { margin-top: 0; }
.account-back { margin: 0; font-size: 10px; letter-spacing: .09em; text-transform: uppercase; }
.account-back a { border-bottom: 1px solid rgba(23,23,19,.4); padding-bottom: 3px; }
.account-signout { margin-top: 28px; display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 26px 30px; background: #f5f3ed; border: 1px solid rgba(23,23,19,.25); }
.account-signout p { margin: 0; font-size: 12px; line-height: 1.5; color: rgba(23,23,19,.75); }
.account-signout .studio-button { flex: none; }
.account-panel form { margin-top: 32px; }
.hero-panel { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-top: 28px; padding: 26px 30px; background: #f5f3ed; border: 1px solid rgba(23,23,19,.25); }
.hero-panel h2 { margin: 0 0 12px; font-size: 11px; letter-spacing: .1em; font-weight: 500; text-transform: uppercase; color: rgba(23,23,19,.62); }
.hero-panel p { margin: 0; font-size: 12px; line-height: 1.5; color: rgba(23,23,19,.75); }
.hero-panel img { width: 132px; aspect-ratio: 16 / 10; object-fit: cover; background: #d8d4cb; flex: none; }
@media (max-width: 760px) { .hero-panel { flex-direction: column-reverse; align-items: stretch; padding: 18px; } .hero-panel img { width: 100%; } }

/* Studio sign in */
.signin-shell { min-height: 100svh; display: grid; place-items: center; background: #e8e5de; padding: 24px; }
.signin-card { width: min(400px, 100%); background: #f5f3ed; border: 1px solid rgba(23,23,19,.25); padding: 38px; }
.signin-card h1 { font-size: 34px; letter-spacing: -.04em; margin-bottom: 6px; }
.signin-card > p { margin: 0 0 32px; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: rgba(23,23,19,.6); }
.signin-card label { display: flex; flex-direction: column; gap: 8px; margin-bottom: 22px; }
.signin-card label span { font-size: 9px; letter-spacing: .08em; text-transform: uppercase; }
.signin-card input { width: 100%; border: 0; border-bottom: 1px solid rgba(23,23,19,.35); background: transparent; color: inherit; padding: 9px 0; font: inherit; font-size: 14px; outline: none; }
.signin-card input:focus { border-bottom-color: var(--ink); }
.signin-card .studio-button { width: 100%; justify-self: stretch; }
.signin-error { margin: 0 0 20px; color: #8a2018; font-size: 12px; }

@media (max-width: 760px) {
  .studio-shell { padding: 0 18px 70px; }
  /* The tabs drop to their own row rather than being hidden, so both areas of
     the studio stay reachable on a phone. */
  .studio-header { height: auto; grid-template-columns: 1fr auto; grid-template-areas: "brand account" "nav nav"; padding-top: 16px; }
  .studio-nav { gap: 24px; }
  .studio-nav a { padding: 14px 0; }
  .account-trigger { max-width: 46vw; }
  .studio-intro { display: block; padding: 72px 0 52px; }
  .studio-intro > p:last-child { margin-top: 35px; }
  .upload-panel, .library { padding: 22px 20px 26px; }
  .upload-panel form, .record-fields, .upload-panel .section-form { grid-template-columns: 1fr; }
  .upload-panel .span-two, .record-fields .span-two { grid-column: auto; }
  .photo-record { grid-template-columns: 1fr; }
}
