/* Irene Visuals. Tokens follow DESIGN-2.md. */

/* Client font rules are at the top of DESIGN-2.md. Apple's SF files must never be hosted or copied. */
@font-face { font-family: "Inter"; font-weight: 400; font-display: swap; src: url("/assets/fonts/inter-latin-400-normal.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: "Inter"; font-weight: 400; font-display: swap; src: url("/assets/fonts/inter-latin-ext-400-normal.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, 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; }
@font-face { font-family: "Inter"; font-weight: 700; font-display: swap; src: url("/assets/fonts/inter-latin-700-normal.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: "Inter"; font-weight: 700; font-display: swap; src: url("/assets/fonts/inter-latin-ext-700-normal.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, 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; }
@font-face { font-family: "Open Sans"; font-weight: 400; font-display: swap; src: url("/assets/fonts/open-sans-visuals.woff2") format("woff2"); }
@font-face { font-family: "Syne"; font-weight: 800; font-display: swap; src: url("/assets/fonts/syne-latin-800-normal.woff2") format("woff2"); }
/* Preview only until the commercial licence is bought: holds just the letters of "Irene". */
@font-face { font-family: "High Cruiser"; font-weight: 800; font-display: swap; src: url("/assets/fonts/high-cruiser-irene.woff2") format("woff2"); }

:root {
  --black: #000000;
  --white: #ffffff;
  --gray: #999999;
  /* Studio action colours. Public pages never use these. */
  --publish: #30d158;
  --caution: #ff9f0a;
  --danger: #ff453a;
  --font-default: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-wordmark-top: "High Cruiser", "Syne", var(--font-default);
  --font-wordmark-bottom: "Open Sans", var(--font-default);
  /* Measured with bold SF: tighter than this makes letters touch. */
  --display-tracking: -0.01em;
  --label-size: 14px;
  --label-line: 18.2px;
  --tracking: 0.84px;
  --space-15: 15px;
  --space-20: 20px;
  --space-25: 25px;
  --space-72: 72px;
  --space-144: 144px;
  --gutter: 6px;
  --ease-settle: cubic-bezier(0.16, 1, 0.3, 1);
  --motion-quick: 250ms;
  --page-inset: var(--space-25);
  /* Heading size never exceeds what fits the longest word, down to an 18px floor; longer words wrap as a last resort. --fit is that word's length in characters, set by fitClass() in src/html.ts. */
  --fit: 8;
  --char-width: 0.66; /* Widest average character of the bold fallbacks, in em. */
  --radius: 15px;
  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }
/* Elements hidden by script stay hidden, whatever display their class gives them. */
[hidden] { display: none !important; }

html { background: var(--black); -webkit-text-size-adjust: 100%; scroll-padding-top: 64px; }

body {
  margin: 0;
  background: var(--black);
  color: var(--white);
  font: 400 var(--label-size) / var(--label-line) var(--font-default);
  letter-spacing: var(--tracking);
  -webkit-font-smoothing: antialiased;
}

/* Keeps the footer at the bottom of the screen on short pages. */
body:not(.studio) { display: flex; flex-direction: column; min-height: 100svh; }
body:not(.studio) > main { flex: 1 0 auto; }

img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
h1, h2, p, ul { margin: 0; padding: 0; font: inherit; }
ul { list-style: none; }
button { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }

:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
.pair:focus-visible, .sequence-item:focus-visible { outline-offset: -6px; }

.skip { position: absolute; left: var(--space-25); top: -60px; z-index: 30; background: var(--black); padding: var(--space-15); text-transform: uppercase; }
.skip:focus { top: 0; }

.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Navigation */

.nav {
  position: sticky;
  top: 0;
  z-index: 20;
  /* Equal gaps between the visible words, never below 24px. Each item's box is exactly its word at rest. */
  display: flex;
  justify-content: space-between;
  column-gap: 24px;
  min-height: 48px;
  padding: var(--space-15) var(--space-25);
  background: var(--black);
  text-transform: uppercase;
}


.nav-link { position: relative; text-transform: uppercase; white-space: nowrap; }

/* Underlines draw in from the left, using transform so nothing reflows. */
.nav-link::after, .filter a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: 2.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--motion-quick) var(--ease-settle);
}
.nav-link:hover::after, .nav-link[aria-current="page"]::after,
.filter a:hover::after, .filter a[aria-current="page"]::after { transform: scaleX(1); }
/* Weight wave: gallery.js splits link text into letters on devices with a mouse. A hidden copy of those letters
   at the resting weight sets the link's width, so the gaps match the visible words. The letters are drawn over
   it and may grow a little into the gap as they turn bold, without moving anything. */
.nav-text { position: relative; display: inline-block; }
.nav-reserve { visibility: hidden; }
.nav-letters { position: absolute; top: 0; left: 0; white-space: nowrap; }
.nav-letter { display: inline-block; white-space: pre; transition: font-weight 400ms ease-out; transition-delay: calc(var(--i) * 28ms); }
.nav-link:hover .nav-letters .nav-letter, .nav-link:focus-visible .nav-letters .nav-letter { font-weight: 700; }
.nav-form { display: contents; }
/* Its left padding and border sit outside the layout, so the gap before it is measured to the word, like the
   links. The right side stays, so its border lines up with the page's right edge. */
.nav-signout { align-self: center; margin: -8px 0 -8px -13px; }
/* A button centres its text in the row and a link doesn't, so links centre too and share one baseline. */
.studio .nav a.nav-link { align-self: center; }

/* Phone menu: a bottom button opens a full-screen list of links. It uses the browser's popover feature, so it needs no script. Browsers without it keep the top links. */
.menu-button, .menu { display: none; }
@supports selector(:popover-open) {
  @media (max-width: 700px) {
    .nav-public .nav-link:not(.nav-home) { display: none; }
    .menu-button {
      display: block;
      position: fixed;
      left: 50%;
      bottom: calc(var(--space-15) + env(safe-area-inset-bottom));
      z-index: 30;
      translate: -50% 0;
      min-height: 44px;
      padding-inline: var(--space-25);
      background: var(--black);
      color: var(--white);
      border: 1px solid var(--white);
      border-radius: 22px;
      text-transform: uppercase;
    }
    .footer { --footer-bottom: calc(84px + env(safe-area-inset-bottom)); }
    .menu:popover-open {
      display: flex;
      flex-direction: column;
      position: fixed;
      inset: 0;
      width: 100%;
      height: 100dvh;
      max-width: none;
      max-height: none;
      margin: 0;
      padding: var(--space-15);
      overflow-y: auto;
      border: 0;
      background: var(--black);
      color: var(--white);
      opacity: 1;
      transition: opacity var(--motion-quick), display var(--motion-quick) allow-discrete, overlay var(--motion-quick) allow-discrete;
    }
    @starting-style { .menu:popover-open { opacity: 0; } }
  }
}
.menu-close { align-self: flex-end; min-height: 44px; padding-inline: var(--space-15); text-transform: uppercase; }
.menu-links { display: grid; margin-block: auto; }
.menu-links a {
  padding-block: var(--space-20);
  border-bottom: 1px solid var(--gray);
  font: 700 clamp(40px, 13vw, 64px) / 1 var(--font-default);
  letter-spacing: var(--display-tracking);
}
.menu-links a:first-child { border-top: 1px solid var(--gray); }
.menu-links a[aria-current="page"] { text-decoration: underline; text-decoration-thickness: 2.5px; text-underline-offset: 0.12em; }

.menu-open { overflow: hidden; }

/* Display type */

.display {
  font: 700 max(18px, min(clamp(40px, 7.5vw, 120px), calc((100vw - 2 * var(--page-inset)) / (var(--fit) * var(--char-width))))) / 0.92 var(--font-default);
  letter-spacing: var(--display-tracking);
  text-align: center;
  text-wrap: balance;
  overflow-wrap: break-word;
}

.fit-10 { --fit: 10; }
.fit-12 { --fit: 12; }
.fit-14 { --fit: 14; }
.fit-17 { --fit: 17; }
.fit-20 { --fit: 20; }
.fit-25 { --fit: 25; }
.fit-30 { --fit: 30; }

/* The home page wordmark. It sits outside the clipped hero so it can stick over the photos,
   and its height is taken back with a negative margin, so nothing below it moves. */

.stage-home {
  /* The visible letters of "Irene" fill a 97-unit measure in Syne ExtraBold, with "Visuals" centred below at the client's size, 70% of that width. Re-measure the sizes and offsets if either font changes.
     The block is 32 units tall. 121px is the nav, padding, gap and tagline, so it also fits short screens. */
  --unit: min((100vw - 2 * var(--page-inset)) / 100, (100svh - 121px) / 32);
  --logo-height: calc(var(--unit) * 31.75);
  --logo-offset: calc((100svh - 48px - var(--logo-height) - 43px) / 2);
  --logo-scale-loss: 0.728;
  display: flow-root;
}

.hero {
  position: relative;
  min-height: calc(100svh - 48px);
  overflow: hidden;
}

.hero-backdrop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Full colour and softly blurred, so the photo stays recognisable. It is darkened only as much as the white
     wordmark needs: html.ts picks a tone class from the photo's brightness. The scale hides the blur's soft edge,
     and the hero clips it, so nothing scrolls sideways. */
  filter: blur(12px) brightness(var(--hero-brightness, 0.6));
  transform: scale(1.1);
  mask-image: linear-gradient(to bottom, #000 0%, #000 25%, transparent 88%);
}
/* Measured with white type over the brightest tenth of the photo behind it: a busy photo (brightest third 133)
   needs 0.8, a bright orange one (206) about 0.8, and a near-white one (241) 0.58. Dark photos keep nearly full
   brightness, with a little held back for bright spots that an average can't see. */
.hero-tone-1 { --hero-brightness: 0.9; }
.hero-tone-2 { --hero-brightness: 0.8; }
.hero-tone-3 { --hero-brightness: 0.75; }
.hero-tone-4 { --hero-brightness: 0.58; }

.wordmark {
  position: relative;
  z-index: 10;
  /* Sized to the letters, so the arrival zoom and the small line offsets can't widen the page. */
  width: max-content;
  margin-inline: auto;
  height: var(--logo-height);
  margin-top: var(--logo-offset);
  margin-bottom: calc(-1 * (var(--logo-offset) + var(--logo-height)));
  text-align: center;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  animation: logo-arrive 1.4s var(--ease-settle) both;
}

.wordmark span { display: block; }
.wordmark-top { position: relative; left: calc(var(--unit) * -0.24); font: 800 calc(var(--unit) * 19.07) / 0.86 var(--font-wordmark-top); letter-spacing: 0; }
.wordmark-bottom { position: relative; left: calc(var(--unit) * 0.43); font: 400 calc(var(--unit) * 17.85) / 0.86 var(--font-wordmark-bottom); letter-spacing: 0; }

.hero-tagline {
  position: absolute;
  top: calc(var(--logo-offset) + var(--logo-height) + 25px);
  left: 0;
  right: 0;
  text-align: center;
  text-transform: uppercase;
}

/* The logo shrinks as the visitor scrolls, quickly then slowly, and rests just under the nav.
   Browsers without scroll timelines, and reduced motion, leave it in place to scroll away. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .wordmark { position: sticky; top: calc(48px + var(--logo-offset)); }
    .wordmark-inner {
      --rise: calc(12px - var(--logo-offset));
      transform-origin: 50% 0;
      animation: logo-settle linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 64svh;
    }
    /* The tagline fades early, so the shrinking logo never runs through it. */
    .hero-tagline {
      animation: tagline-out linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 8svh;
    }
  }
}

@keyframes tagline-out { to { opacity: 0; } }

@keyframes logo-settle {
  26% { transform: translateY(calc(var(--rise) * 0.51)) scale(calc(1 - var(--logo-scale-loss) * 0.51)); }
  52% { transform: translateY(calc(var(--rise) * 0.84)) scale(calc(1 - var(--logo-scale-loss) * 0.84)); }
  69% { transform: translateY(calc(var(--rise) * 0.95)) scale(calc(1 - var(--logo-scale-loss) * 0.95)); }
  100% { transform: translateY(var(--rise)) scale(calc(1 - var(--logo-scale-loss))); }
}

/* The wordmark settles once, on arrival. */
@keyframes logo-arrive {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Project sections: one screen tall on desktop, with a thin gap between photos. */

.stack { display: grid; gap: var(--gutter); padding: var(--gutter) 8px 0; }

.pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gutter);
  height: calc(100svh - 48px - 2 * var(--gutter));
  clip-path: inset(0 round var(--radius));
  transition: clip-path 900ms var(--ease-settle);
}

.pair-single { grid-template-columns: 1fr; }

/* The clip also bounds the fixed caption inside, so captions wipe as sections pass.
   Nothing that contains a caption may use transform or filter, which would break position: fixed. */
.pair-pane { min-width: 0; height: 100%; overflow: hidden; clip-path: inset(0); }
.pair-pane img { width: 100%; height: 100%; object-fit: cover; transition: transform 900ms var(--ease-settle); }

/* Each photo has its own caption, with text colour chosen for that photo. On desktop each clip shows only the half over its own photo. */
.pair-caption {
  position: fixed;
  top: 50svh;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-20);
  /* Clears the page margin and the frame's 2.5% inset on hover, so a caption is never cropped. */
  padding-inline: calc(8px + 2.5vw + var(--space-15));
  text-transform: uppercase;
  pointer-events: none;
}

.pair-title, .pair-descriptor { min-width: 0; max-width: calc(50vw - 8px - 2.5vw - 2 * var(--space-15)); overflow-wrap: break-word; }
.pair-descriptor { text-align: right; }

.ink-dark { color: var(--black); }
/* Set by gallery.js on a caption's title or count, so each suits the photo behind it. */
.ink-light { color: var(--white); }

.stack-more {
  justify-self: center;
  margin-top: var(--space-72);
  text-transform: uppercase;
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}

/* Project hover: the frame draws in and the photo grows. Mouse only. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .pair:hover { clip-path: inset(2.5% round 80px); }
  .pair:hover img { transform: scale(1.1); }
}

/* Scroll reveals. gallery.js adds .reveal only to work below the fold, so without the script everything stays visible.
   Only photos and captions animate, never a section, so fixed captions keep working. */
.reveal:not(.is-revealed) img { opacity: 0; transform: translateY(24px) scale(1.04); transition: none; }
.reveal:not(.is-revealed) .pair-caption { opacity: 0; transition: none; }
.reveal img { transition: opacity 700ms ease-out, transform 900ms var(--ease-settle); }
.reveal .pair-caption { transition: opacity 600ms ease-out 200ms; }

@media print {
  .reveal:not(.is-revealed), .reveal:not(.is-revealed) * { opacity: 1 !important; transform: none !important; }
  .pair-caption { position: absolute; }
}

/* Work */

.page-head { display: grid; gap: var(--space-25); padding: var(--space-72) var(--page-inset) var(--space-25); }
.filter { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-15) var(--space-25); text-transform: uppercase; }
.filter a { position: relative; }

.empty { padding: var(--space-144) var(--page-inset); text-align: center; text-transform: uppercase; color: var(--gray); }

/* Project page: the title, details in two columns, then one-screen photographs. */

.project-title { padding: var(--space-72) var(--page-inset) var(--space-25); }
.project-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-25);
  padding: 0 var(--page-inset) var(--space-72);
}
.project-details, .project-about { width: min(100%, calc(50% - var(--space-15))); min-width: 0; overflow-wrap: break-word; }
.project-details { display: grid; gap: 4px; text-transform: uppercase; align-content: start; }
.project-about { white-space: pre-line; }
.draft-note { flex-basis: 100%; color: var(--gray); }
.draft-note a, .text-link { color: var(--white); text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 2.5px; }

.sequence { display: grid; gap: var(--gutter); padding-inline: 8px; }
.sequence-row { display: grid; gap: var(--gutter); height: calc(100svh - 48px - 2 * var(--gutter)); }
.row-duo { grid-template-columns: 1fr 1fr; }
.sequence-item { display: block; min-width: 0; height: 100%; overflow: hidden; clip-path: inset(0 round var(--radius)); cursor: zoom-in; }
.sequence-item img { width: 100%; height: 100%; object-fit: cover; }
/* A video sits exactly where a photo would, on its poster until it plays. */
.sequence-video { display: block; width: 100%; height: 100%; object-fit: cover; }
/* A lone portrait isn't cropped: it's centred at full height, with its caption on the black beside it. */
.row-lone .sequence-item { display: flex; justify-content: center; clip-path: inset(0); }
.row-lone .sequence-item img { width: auto; max-width: 100%; object-fit: contain; border-radius: var(--radius); }
.row-lone .sequence-video { width: auto; max-width: 100%; object-fit: contain; border-radius: var(--radius); }
.row-lone .pair-title, .row-lone .pair-descriptor { max-width: 22vw; }

.next-project { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-20); justify-items: center; text-align: center; padding: var(--space-144) var(--page-inset) var(--space-72); }
.next-label { text-transform: uppercase; font-weight: 700; }
.next-project .display { justify-self: stretch; }
.next-project:hover .display { text-decoration: underline; text-decoration-thickness: 3px; text-underline-offset: 0.1em; }

/* About, contact and not found: a centred display line, then label text. */

.statement { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-72); justify-items: center; padding: var(--space-72) var(--page-inset) var(--space-144); }
.statement .display { justify-self: stretch; }
.statement-text { width: 100%; max-width: 1040px; columns: 2; column-gap: var(--space-72); white-space: pre-line; }
.statement-text p + p { margin-top: var(--label-line); }
.contact-big:hover { text-decoration: underline; text-decoration-thickness: 3px; text-underline-offset: 0.1em; }
.contact-details { display: grid; gap: var(--space-15); justify-items: center; text-align: center; }
/* A long handle wraps rather than scrolling the page sideways. It only breaks when it can't fit. */
.contact-details a { max-width: 100%; overflow-wrap: anywhere; }
.empty-inline { color: var(--gray); }

.footer { display: flex; justify-content: space-between; gap: var(--space-25); margin-top: var(--space-72); padding: var(--space-25) var(--page-inset) var(--footer-bottom, var(--space-25)); text-transform: uppercase; color: var(--gray); }
.footer a { color: var(--white); opacity: 0.6; transition: opacity var(--motion-quick); }
.footer a:hover, .footer a:focus-visible { opacity: 1; }

/* Slideshow viewer */

.lightbox {
  width: 100vw; height: 100dvh; max-width: none; max-height: none;
  margin: 0; padding: 0; border: 0;
  background: var(--black); color: var(--white);
  overflow: hidden;
  opacity: 0;
  transition: opacity var(--motion-quick), display var(--motion-quick) allow-discrete, overlay var(--motion-quick) allow-discrete;
}
.lightbox[open] { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; opacity: 1; }
@starting-style { .lightbox[open] { opacity: 0; } }

.viewer-top { display: grid; grid-template-columns: minmax(max-content, 1fr) minmax(0, 3fr) minmax(max-content, 1fr); align-items: center; gap: var(--space-20); min-height: 48px; padding: var(--space-15) var(--space-25); text-transform: uppercase; }
.viewer-title { text-align: center; overflow-wrap: break-word; }
.viewer-count { justify-self: end; }
.viewer-bottom { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: var(--space-20); padding: var(--space-15) var(--space-25); text-transform: uppercase; }
.viewer-top button, .viewer-bottom button { min-height: 44px; text-transform: uppercase; }
.viewer-top button { justify-self: start; }
.viewer-top button:hover, [data-prev]:not(:disabled):hover, [data-next]:not(:disabled):hover { text-decoration: underline; text-underline-offset: 4px; }
[data-prev] { justify-self: start; }
[data-next] { justify-self: end; }
.viewer-bottom button:disabled { opacity: 0.35; cursor: default; }
.viewer-close { padding-inline: var(--space-25); border: 1px solid var(--white); border-radius: 22px; transition: opacity var(--motion-quick); }
.viewer-close:hover { opacity: 0.7; }
.lightbox.is-single [data-show-all], .lightbox.is-single [data-prev], .lightbox.is-single [data-next] { visibility: hidden; }

.viewer-stage, .viewer-strip { grid-row: 2; grid-column: 1; }
.viewer-stage { position: relative; min-height: 0; overflow: hidden; cursor: pointer; transition: opacity var(--motion-quick); }
.viewer-stage.is-dimmed { opacity: 0.35; }
.lightbox.is-single .viewer-stage { cursor: default; }
.viewer-photo { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: contain; padding-inline: var(--space-25); }
.viewer-photo.is-entering { z-index: 1; animation: viewer-enter 760ms cubic-bezier(0.7, 0, 0.2, 1) both; }
.viewer-photo.is-leaving { pointer-events: none; animation: viewer-leave 900ms ease-out both; }

@keyframes viewer-enter { from { transform: translateX(calc(var(--from) * 100%)); } }
@keyframes viewer-leave { to { opacity: 0; transform: scale(0.6); } }

.viewer-strip { z-index: 2; align-self: end; display: flex; gap: var(--gutter); padding: var(--space-15) var(--space-25); overflow-x: auto; overscroll-behavior: contain; scrollbar-width: thin; cursor: grab; }
.viewer-strip[hidden] { display: none; }
.viewer-thumb { flex: none; height: 96px; aspect-ratio: 4 / 5; }
.viewer-thumb img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; opacity: 0.7; transition: opacity var(--motion-quick); }
.viewer-thumb:hover img, .viewer-thumb:focus-visible img, .viewer-thumb[aria-current] img { opacity: 1; }
.viewer-thumb[aria-current] { outline: 2.5px solid var(--white); outline-offset: -2.5px; }

/* Studio */

.studio-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-25); padding: var(--space-25) var(--page-inset) var(--space-144); }
/* Each section sits in its own panel. */
.panel { min-width: 0; padding: var(--space-25); border: 1px solid var(--gray); border-radius: var(--radius); }
.studio-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-15); }
.section-label, .studio-title { font-weight: 700; text-transform: uppercase; margin-bottom: var(--space-25); }
.studio-title { margin-bottom: 0; }

.hint { color: var(--gray); margin-bottom: var(--space-20); }
.crumb { text-transform: uppercase; }
.status-hint { color: var(--gray); }
.notice { min-height: 18px; margin-top: var(--space-15); }
.notice:empty { display: none; }

.form-stack { display: grid; gap: var(--space-25); justify-items: start; }
.form-stack .field { width: 100%; }
.form-narrow { max-width: 640px; }

.field { display: grid; gap: 8px; color: var(--gray); }
.field input, .field select, .field textarea {
  width: 100%;
  padding: 8px 0;
  color: var(--white);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--gray);
  border-radius: 0;
  font: 400 var(--label-size) / var(--label-line) var(--font-default);
  letter-spacing: var(--tracking);
}
.field select {
  appearance: none;
  padding-right: var(--space-25);
  background: url("/assets/chevron.svg") no-repeat right center;
}
/* Phones zoom the page when a focused field is under 16px. */
@media (pointer: coarse) {
  .field input, .field select, .field textarea { font-size: 16px; }
  /* Larger tap areas that do not change the layout. */
  .nav-signout { margin-block: -13px; }
  .nav-link, .filter a, .text-link {
    display: inline-block;
    padding-block: 11px;
    margin-block: -11px;
  }
  .nav-link::after, .filter a::after { bottom: 8px; }
  .filter { row-gap: var(--space-25); }
}
.field input:hover, .field select:hover, .field textarea:hover, .field input:focus, .field select:focus, .field textarea:focus { border-bottom-color: var(--white); outline: none; }
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible { border-bottom-width: 2px; margin-bottom: -1px; }
.field select option { background: var(--black); }
.field textarea { resize: vertical; }

.check { display: flex; gap: var(--space-15); align-items: center; cursor: pointer; }
.check input { width: 18px; height: 18px; margin: 0; accent-color: var(--white); }

.text-link, .row-title { transition: opacity var(--motion-quick); }
.text-link:hover, .row-title:hover { opacity: 0.7; }

/* Studio actions. A plain .btn is an ordinary action; the other classes add the colours in DESIGN-2.md.
   Fills change instantly, so reduced motion needs nothing extra. */
.btn { --tone: var(--white); display: inline-block; padding: 7px 12px; color: var(--tone); background: transparent; border: 1px solid var(--gray); border-radius: 8px; text-align: center; text-transform: uppercase; cursor: pointer; }
.btn-main { color: var(--black); background: var(--white); border-color: var(--white); }
.btn-publish { --tone: var(--publish); border-color: var(--tone); }
.btn-caution { --tone: var(--caution); border-color: var(--tone); }
.btn-danger { --tone: var(--danger); border-color: var(--tone); }
.btn[aria-pressed="true"] { border-color: var(--white); font-weight: 700; }
.btn:disabled { opacity: 0.35; cursor: default; }
/* Larger tap areas, and links centre their text the way buttons do. */
@media (pointer: coarse) {
  .btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; }
}
.btn:not(.btn-main):not(:disabled):focus-visible { color: var(--black); background: var(--tone); border-color: var(--tone); }
.btn-main:focus-visible { color: var(--white); background: var(--black); }
@media (hover: hover) {
  .btn:not(.btn-main):not(:disabled):hover { color: var(--black); background: var(--tone); border-color: var(--tone); }
  .btn-main:hover, .dropzone:hover .btn-main { color: var(--white); background: var(--black); }
}

/* Sign in sits centred in the first screen. */
.studio-main-login { min-height: calc(100svh - 48px); align-content: center; justify-items: center; gap: var(--space-72); padding-block: var(--space-25); }
.login { width: min(100%, 360px); display: grid; gap: var(--space-25); justify-items: center; text-align: center; }
.login .form-stack { width: 100%; justify-items: center; }
.login .field { text-align: left; }

.meter { display: grid; gap: var(--space-15); }
.meter-text { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px var(--space-25); text-transform: uppercase; }
/* The track is an outline, so an empty bar never reads as a full one. */
progress { appearance: none; display: block; width: 100%; height: 6px; border: 1px solid var(--gray); background: transparent; color: var(--white); }
progress::-webkit-progress-bar { background: transparent; }
progress::-webkit-progress-value { background: var(--white); }
progress::-moz-progress-bar { background: var(--white); }

.new-project { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) auto; gap: var(--space-25); align-items: end; }

.project-row:first-child { padding-top: 0; }
.project-row:last-child { padding-bottom: 0; border-bottom: 0; }
.project-row { display: grid; grid-template-columns: 72px minmax(0, 2fr) minmax(0, 1fr) 80px auto auto; gap: var(--space-25); align-items: center; padding-block: var(--space-15); border-bottom: 1px solid var(--white); }
.status-published { color: var(--publish); font-weight: 700; text-transform: uppercase; }
.status-draft { color: var(--caution); font-weight: 700; text-transform: uppercase; }
.row-thumb { display: block; width: 72px; height: 90px; }
.row-thumb { overflow: hidden; border-radius: var(--radius); }
.row-thumb-empty { border: 1px dashed var(--gray); }
.row-thumb img { width: 100%; height: 100%; object-fit: cover; }
.row-title { font-weight: 700; text-transform: uppercase; overflow-wrap: break-word; }
.project-row .btn { justify-self: start; }
.row-title:hover { text-decoration: underline; text-decoration-thickness: 2.5px; text-underline-offset: 4px; }
.row-moves { display: flex; flex-wrap: wrap; gap: 8px; }

.edit-head { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-20); }
.edit-status { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-15); text-transform: uppercase; }

.dropzone {
  display: grid;
  gap: var(--space-15);
  justify-items: center;
  padding: var(--space-72) var(--space-25);
  border: 1px dashed var(--gray);
  text-align: center;
  color: var(--gray);
  cursor: pointer;
}
.dropzone:hover, .dropzone:focus-within, .dropzone.is-over { border-color: var(--white); border-style: solid; color: var(--white); }
.dropzone:has(input:focus-visible) { outline: 2px solid var(--white); outline-offset: 3px; }
.dropzone-title { justify-self: stretch; color: var(--white); }

.upload-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px var(--space-25); margin-top: var(--space-20); }
.upload-status:empty { display: none; }
.upload-list { display: grid; gap: var(--space-15); margin-top: var(--space-15); }
.upload-list:empty { display: none; }
.upload-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 4ch minmax(0, 1.4fr) auto;
  grid-template-areas: "name bar percent state retry";
  gap: 8px var(--space-20);
  align-items: center;
}
.upload-name { grid-area: name; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload-item progress { grid-area: bar; }
.upload-percent { grid-area: percent; text-align: right; font-variant-numeric: tabular-nums; }
.upload-state { grid-area: state; overflow-wrap: break-word; }
.upload-item .btn { grid-area: retry; justify-self: start; }
/* State is also in words, with a tick or a cross, so colour is never the only signal. */
.upload-item.is-done .upload-state { color: var(--publish); }
.upload-item.is-failed .upload-state { color: var(--danger); }

/* The home image preview is the home page's first screen at 1440 x 852, scaled to fit, so the blur and fade
   match exactly. studio.js sets the scale. */
.hero-preview { position: relative; max-width: 560px; aspect-ratio: 1440 / 852; overflow: hidden; margin-bottom: var(--space-20); border-radius: var(--radius); background: var(--black); border: 1px solid var(--gray); }
.hero-preview-stage { position: absolute; top: 0; left: 0; width: 1440px; height: 852px; overflow: hidden; transform-origin: 0 0; transform: scale(var(--preview-scale, 0.4)); }
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-15); margin-top: var(--space-20); }
.hero-upload:has(input:focus-visible) { outline: 2px solid var(--white); outline-offset: 3px; }

.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-25) var(--space-20); }
.photo-tile { display: grid; gap: var(--space-15); align-content: start; cursor: grab; }
.photo-tile.is-dragging { opacity: 0.4; }
.photo-tile img { width: 100%; aspect-ratio: 4 / 5; height: auto; object-fit: cover; border-radius: var(--radius); }
/* The studio's hidden reader for a chosen video, in the page only while its poster is made. */
.video-probe { position: fixed; left: 0; top: 0; width: 2px; height: 2px; opacity: 0.01; pointer-events: none; z-index: -1; }
.tile-kind { text-transform: uppercase; color: var(--gray); }
.tile-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.category-row:first-child { padding-top: 0; }
.category-rows { max-width: 640px; margin-bottom: var(--space-25); }
.category-row { display: flex; gap: var(--space-25); align-items: end; padding-block: var(--space-15); border-bottom: 1px solid var(--white); }
.category-form { display: flex; flex: 1; gap: var(--space-25); align-items: end; }
.category-form .field { flex: 1; }
.category-new { max-width: 640px; }

/* Rows stack before the title column gets too narrow for a long word. */
@media (max-width: 1100px) {
  .project-row { grid-template-columns: 72px minmax(0, 1fr); gap: 8px var(--space-15); }
  .row-thumb { grid-row: span 5; }
}

@media (max-width: 700px) {
  :root { --space-72: 48px; --space-144: 96px; --page-inset: 10px; --label-size: 12px; --label-line: 15.6px; --tracking: 0.72px; --display-tracking: 0; }
  .nav { display: flex; justify-content: space-between; gap: var(--space-15); padding-inline: var(--space-15); }
  .studio-main { padding-inline: var(--page-inset); }
  .stage-home { --logo-scale-loss: 0.56; }
  .panel { padding: var(--space-15); }
  .dropzone { padding: var(--space-25) var(--space-15); }
  .btn { padding-inline: 10px; }
  .nav-signout { padding-inline: 8px; margin-inline-start: -9px; }
  .category-row { flex-wrap: wrap; gap: 8px; }
  .category-row .category-form { display: contents; }
  .category-row .field { flex: 1 1 100%; }
  .category-new { flex-wrap: wrap; gap: 8px; }
  .category-new .field { flex: 1 1 100%; }
  /* Phones stack the photos, so a clip would slice a caption wherever a page stopped with a seam at mid-screen.
     Here each caption sticks at mid-screen inside its own photo and leaves with it, always whole. */
  .stack, .sequence { padding-inline: 10px; }
  .pair, .pair-single { grid-template-columns: minmax(0, 1fr); height: auto; }
  .pair-pane { position: relative; height: auto; aspect-ratio: 4 / 5; overflow: clip; clip-path: none; }
  .pair-pane img { position: absolute; inset: 0; }
  .pair-caption { position: sticky; top: 50svh; left: auto; right: auto; transform: none; align-self: start; flex-wrap: wrap; row-gap: 4px; padding: var(--space-15) var(--space-20); }
  .pair-title { max-width: 100%; }
  .pair-descriptor { max-width: 100%; margin-left: auto; }
  .viewer-top, .viewer-bottom, .viewer-strip { padding-inline: var(--space-15); }
  /* The title takes its own row, so a long one can't squeeze the photo or break a word. */
  .viewer-top { grid-template-columns: 1fr auto; row-gap: 4px; }
  .viewer-title { grid-row: 2; grid-column: 1 / -1; text-align: left; }
  .viewer-photo { padding-inline: 0; }
  .viewer-thumb { height: 72px; }
  .sequence-row, .row-duo { grid-template-columns: minmax(0, 1fr); height: auto; }
  /* One column the width of the item, so a photo has its full width before its file arrives and nothing shifts. */
  .sequence-item, .row-lone .sequence-item { display: grid; grid-template-columns: minmax(0, 1fr); height: auto; overflow: clip; }
  .sequence-item > * { grid-area: 1 / 1; }
  .sequence-item img, .row-lone .sequence-item img, .sequence-video, .row-lone .sequence-video { width: 100%; height: auto; }
  .row-lone .sequence-item { clip-path: inset(0 round var(--radius)); }
  .row-lone .pair-title, .row-lone .pair-descriptor { max-width: 100%; }
  .ink-phone-dark { color: var(--black); }
  .project-details, .project-about { width: 100%; }
  .statement-text { columns: 1; }
  .new-project { grid-template-columns: 1fr; justify-items: start; }
  .new-project .field { width: 100%; }
  .upload-item {
    grid-template-columns: minmax(0, 1fr) 4ch;
    grid-template-areas: "name name" "bar percent" "state state" "retry retry";
    gap: 6px var(--space-15);
  }
}

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

/* The only place tracking tightens: four labels must fit a 320px phone on one line. */
@media (max-width: 350px) {
  .nav { gap: 8px; letter-spacing: 0.2px; }
}

/* On phones a home caption rises with its photo as the photo leaves, which would carry it under the pinned logo.
   Each caption line fades out just before it reaches the logo instead, so the two never meet, however long the
   title. The logo rests at 60px and is about 52px tall when pinned on a phone, so lines are gone by 140px. */
@supports (animation-timeline: view()) {
  @media (max-width: 700px) and (prefers-reduced-motion: no-preference) {
    .stage-home .pair-caption > * {
      animation: caption-leave linear both;
      animation-timeline: view(block 180px 0px);
      animation-range: exit 0px exit 40px;
    }
  }
}
@keyframes caption-leave { to { opacity: 0; } }

/* A soft cross-fade between pages. Browsers without it navigate as normal. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 300ms; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
    html { scroll-behavior: auto; }
}
