/* ============ BUDDY BLECKLEY — portfolio (light) ============ */

:root {
  --bg: #ffffff;
  --ink: #111111;
  --ink-dim: #8a8a8a;
  /* THE ACCENT is Buddy's reel teal — the wash colour of the demo-reel spot and
     one of his brand colours. The variable keeps the name --gold: fifteen rules
     here and two other pages read it by that name and renaming it would be
     churn for nothing. Only the value moved.
       --gold      DECORATION and dark ground. Hairlines, rules, rings, progress
                   fills, washes, and anything on the story viewer's dark wash,
                   where it reads at 10.76:1 over #111.
       --gold-ink  WORDS on the white page. The raw teal manages only 1.75:1 on
                   white — the old gold was 2.31:1, also poor — so text takes the
                   same hue walked down in lightness to 4.63:1, which clears
                   WCAG AA for normal text. Anything carrying letters rather
                   than drawing a line uses this one.
       --gold-rgb / --gold-ink-rgb
                   the same two as bare "r,g,b" so a tint can be mixed here —
                   rgba(var(--gold-rgb), .14) — instead of a second hard-coded
                   copy of the colour that then goes stale.
     These four names and these four values are exactly what js/accent.js emits
     for this accent, so the static defaults below and anything that sets them
     at runtime are the same colours rather than two near-misses. */
  --gold: #09d9dc;
  --gold-rgb: 9,217,220;
  --gold-ink: #058284;
  --gold-ink-rgb: 5,130,132;
  --display: "Space Grotesk", system-ui, sans-serif;
  --body: "Inter", system-ui, sans-serif;
  --gap: 12px;
  --radius: 14px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
/* anything given a display value below would otherwise beat the browser's own
   [hidden] rule (the lightbox is display:flex) */
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--body);
  overflow-x: hidden;
  /* brand-color wash while hovering a tile — slow so it never jars */
  transition: background-color 1.4s ease;
}

/* hide the native cursor over the feed — the emoji cursor replaces it. Keyed
   to a class main.js adds only once the emoji is actually running, so a
   ?touch=1 test or a script that died never leaves the visitor with no
   pointer at all. #grid is included so the gutters between tiles keep the
   emoji instead of flashing the arrow. */
.emoji-cursor .tile, .emoji-cursor .tile *, .emoji-cursor .filter-btn, .emoji-cursor .client-btn,
.emoji-cursor #grid,
/* the lightbox keeps the emoji too — without this the real arrow would be drawn
   on top of it the moment a spot opened */
.emoji-cursor #lightbox, .emoji-cursor #lightbox * { cursor: none; }

::selection { background: var(--gold); color: #000; }

/* ============ SPLASH — quick logo flash ============ */

#splash {
  position: fixed; inset: 0; z-index: 300;
  background: var(--bg);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 34px;
  transition: transform .7s cubic-bezier(.7,0,.2,1);
}

#splash.is-done { transform: translateY(-101%); }

#splash-logo {
  width: min(72vw, 560px);
  animation: logoflash 1.1s cubic-bezier(.2,.9,.25,1.2) both .1s;
}

@keyframes logoflash {
  0%   { opacity: 0; transform: scale(.72); filter: blur(6px); }
  55%  { opacity: 1; filter: none; }
  75%  { transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}

#splash-dots { display: flex; gap: 9px; }

#splash-dots span {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--ink);
  animation: dotpulse 1s ease-in-out infinite;
}
#splash-dots span:nth-child(2) { animation-delay: .16s; }
#splash-dots span:nth-child(3) { animation-delay: .32s; }

@keyframes dotpulse {
  0%, 100% { opacity: .18; transform: scale(.8); }
  45%      { opacity: 1;   transform: scale(1); }
}

/* ============ HEADER ============ */

#site-header {
  position: sticky; top: 0; z-index: 50;
  display: flex; flex-direction: column;
  background: rgba(255,255,255,.86);
  -webkit-backdrop-filter: blur(14px);   /* Safari < 18 only knows the prefixed one */
  backdrop-filter: blur(14px);
  border-bottom: 1px solid rgba(17,17,17,.07);
}

.header-main {
  height: 76px;
  /* 1fr | auto | 1fr puts the category nav at the TRUE centre of the window.
     space-between only centred it when the logo and the right-hand group
     happened to be the same width, which they never are. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 0 clamp(14px, 3vw, 36px);
}
.header-main .logo { justify-self: start; }
.header-main .filters { justify-self: center; }
.header-right { justify-self: end; display: flex; align-items: center; gap: clamp(14px, 2vw, 26px); }

/* ---- CLIENTS drawer: a second row that slides down and pushes the feed ----
   0fr -> 1fr animates to whatever height the wrapped list actually needs, so
   the row grows with the client list instead of a hardcoded height. The
   header is sticky (not fixed), so it occupies real layout space and the
   mosaic below simply reflows down as it opens. */
.client-bar {
  height: 0;
  overflow: hidden;
  transition: height .38s cubic-bezier(.3,.9,.3,1);
}
/* no max-height here: it would resolve against the parent's animated height
   (0 when closed), collapsing scrollHeight and leaving nothing to animate to */
.client-bar-inner { overflow-y: auto; }

.client-list {
  /* centred so the row sits under the (centred) category tabs — left-justified
     it trailed off to one side and read as unrelated to the nav above it */
  display: flex; flex-wrap: wrap; justify-content: center; gap: 1px 3px;
  padding: 5px clamp(14px, 3vw, 36px) 7px;
  border-top: 1px solid rgba(17,17,17,.07);
}

.client-btn {
  font-family: var(--display);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: .1em;
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: none;
  color: var(--ink-dim);
  white-space: nowrap;
  transition: color .2s, border-color .2s, background .2s;
}
.client-btn:hover { color: var(--ink); }
.client-btn.is-active { color: var(--ink); border-color: rgba(17,17,17,.55); }

/* 46px, up from 38. The header-main is a fixed 76px, so there is room for it
   without the bar growing. Phone sizes are deliberately left alone: Buddy has
   had me cut that header twice and the logo is the tallest thing in it. */
.logo img { height: 46px; display: block; }

.filters { display: flex; gap: 4px; }

.filter-btn {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .16em;
  padding: 9px 16px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: none;
  color: var(--ink-dim);
  transition: color .2s, border-color .2s, background .2s;
}
.filter-btn:hover { color: var(--ink); }
.filter-btn.is-active { color: var(--ink); border-color: rgba(17,17,17,.55); }

/* ---- tile-size slider (header, beside ABOUT) ---- */
.size-ctl { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.size-row { display: flex; align-items: center; gap: 7px; }
/* − and + are real buttons now, one slider notch each. They keep a normal
   pointer like the slider beside them (see the note below). */
.size-ctl .ico {
  font-size: 13px; color: var(--ink-dim); line-height: 1; user-select: none;
  width: 14px; text-align: center;
  background: none; border: 0; padding: 0;
  font-family: inherit;
  cursor: pointer;
  transition: color .15s;
}
.size-ctl .ico:hover:not(:disabled) { color: var(--ink); }
.size-ctl .ico:disabled { opacity: .3; cursor: default; }
.size-ctl .ico:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
.size-label { font-family: var(--display); font-size: 8.5px; letter-spacing: .18em; color: var(--ink-dim); }
/* NOT cursor:none here — the emoji cursor only replaces the pointer over
   tiles and filter buttons, so hiding it over the slider left nothing at all.
   The slider keeps a real cursor. */
#size-range {
  -webkit-appearance: none; appearance: none;
  width: 96px; height: 2px; border-radius: 2px;
  background: rgba(17,17,17,.22); cursor: ew-resize;
}
#size-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  cursor: ew-resize;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--ink); border: 2px solid var(--bg);
  box-shadow: 0 0 0 1px rgba(17,17,17,.3);
}
#size-range::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%; border: 2px solid var(--bg);
  background: var(--ink); box-shadow: 0 0 0 1px rgba(17,17,17,.3);
}
@media (max-width: 860px) { .size-ctl { display: none; } }

/* ---- the header does not follow you down a phone screen ----
   Sticky means the feed scrolls UNDER the header everywhere except the very
   top, and on a phone that header is 137px of an 812px screen with the client
   row open. Buddy watched it land on his work in a Domino's gallery.

   A reveal-on-scroll-up header does not fix it: it springs back mid-page and
   covers a tile there instead — measured at scrollY 120, first tile top 29,
   header bottom 137. The only arrangement where the work is never behind it is
   not to pin it at all on a phone. It scrolls away with the page and is back
   when you reach the top, where it is in flow and pushes the grid down.

   Desktop keeps the sticky header: there is room for it there, and the nav is
   worth having permanently on a big screen. */
@media (max-width: 720px), (max-height: 560px) and (orientation: landscape) {
  #site-header { position: static; }
}

/* ---- the header's right-hand side ----
   CONTACT first with room around it, then the controls stacked together as one
   block of furniture: tile size on top, the role filter under it. Buddy wanted
   the thing a client needs — how to reach him — pulled out of the cluster of
   knobs it used to sit in. */
.header-right { display: flex; align-items: center; gap: 26px; }
.header-tools { display: flex; flex-direction: column; align-items: center; gap: 5px; }

/* ---- filter by what he did on the job ---- */
.role-ctl { position: relative; }
.role-btn {
  font-family: var(--display);
  font-size: 8.5px;
  letter-spacing: .18em;
  color: var(--ink-dim);
  background: none;
  border: 0;
  padding: 2px 7px;
  border-radius: 999px;
  cursor: pointer;
  line-height: 1.5;
  transition: color .2s, background-color .2s;
}
.role-btn:hover, .role-btn.is-open { color: var(--ink); }
/* on: the button stops being a label and starts naming what it is doing, so a
   short grid is never a mystery */
.role-btn.is-on { color: var(--ink); background: rgba(var(--gold-rgb), .18); }
.role-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.role-menu {
  position: absolute; top: calc(100% + 7px); right: 0; z-index: 60;
  min-width: 186px;
  padding: 7px;
  border-radius: 11px;
  background: #fff;
  border: 1px solid rgba(17,17,17,.13);
  box-shadow: 0 12px 30px rgba(17,17,17,.13);
  animation: dropin .18s cubic-bezier(.2,.8,.2,1) both;
}
/* group label — WHAT I DID / SHAPE */
.role-head {
  margin: 4px 2px 3px;
  padding: 0 8px;
  font-family: var(--display);
  font-size: 8.5px;
  letter-spacing: .18em;
  color: var(--ink-dim);
}
.role-head + .role-opt { margin-top: 0; }
.role-opt + .role-head { margin-top: 9px; padding-top: 8px; border-top: 1px solid rgba(17,17,17,.09); }

.role-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px;
  border-radius: 7px;
  cursor: pointer;
  font-family: var(--display);
  font-size: 10px;
  letter-spacing: .13em;
  color: var(--ink);
}
.role-opt:hover { background: rgba(17,17,17,.045); }
/* a combination with nothing in it — visibly not an option, rather than a
   click that empties the page */
.role-opt.is-dead { opacity: .35; cursor: not-allowed; }
.role-opt.is-dead:hover { background: none; }
.role-opt.is-dead input { cursor: not-allowed; }
.role-opt input { accent-color: var(--gold-ink); width: 13px; height: 13px; cursor: pointer; flex: none; }
.role-name { flex: 1 1 auto; }

.role-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  margin: 5px 2px 1px;
  padding-top: 7px;
  border-top: 1px solid rgba(17,17,17,.09);
  font-family: var(--display);
  font-size: 9px;
  letter-spacing: .1em;
  color: var(--ink-dim);
}
.role-clear {
  font: inherit;
  color: var(--gold-ink);
  background: none; border: 0; padding: 2px 4px;
  cursor: pointer;
  letter-spacing: .12em;
}
.role-clear:hover { color: var(--ink); }

/* the filter follows the slider off a phone: the header there is already down
   to two tight rows and a dropdown would push it back out. */
@media (max-width: 860px) { .role-ctl { display: none; } }

/* No scrim and no shadow on tile captions — ever; Daniel has turned both down
   twice. Stills used to flip their caption to ink here on the theory that it
   sat on the white page. It doesn't: a caption is only ever visible while its
   tile is LIVE (hover on a desktop, the centre tile while scrolling a phone),
   and a live tile also washes the whole page to that asset's brand colour, so
   the words land on the brand. White reads there, and it means every caption
   on the site — photo or footage — is the same white. */

.about-link {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .16em;
  color: var(--ink-dim);
  text-decoration: none;
  transition: color .2s;
}
.about-link:hover { color: var(--ink); }

/* ============ MOSAIC FEED ============ */

#work { padding: var(--gap); }

#grid {
  display: grid;
  /* Column count is a variable so the header slider can drive it. Row unit is
     always half a column: for N columns that is (100vw - (N+1)*gap) / 2N. */
  grid-template-columns: repeat(var(--cols, 7), 1fr);
  grid-auto-rows: calc((100vw - (var(--cols, 7) + 1) * var(--gap)) / (var(--cols, 7) * 2));
  grid-auto-flow: dense;
  gap: var(--gap);
}

/* Drilled into a single client, the survivors used to cling to the left with
   dead space on the right, because 1fr tracks always fill the container.
   In "focused" mode the tracks become a FIXED pixel width (the same width
   they'd have had) and only as many as the survivors need, so the whole
   group centres in the window and stays evenly spaced. */
#grid.is-focused {
  grid-template-columns: repeat(var(--focus-cols, 4), var(--col-px, 200px));
  /* the row unit MUST follow the widened column or tiles come out wide and
     squashed — a row is always half a column */
  grid-auto-rows: calc(var(--col-px, 200px) / 2);
  justify-content: center;
}

.tile {
  position: relative;
  grid-column: span var(--c, 2);
  grid-row: span var(--r, 2);
  border-radius: var(--radius);
  background: #ececec;
  overflow: hidden;
  z-index: 1;
  /* transform pieces: hover-scale × neighbor-push */
  transform: translate(var(--px, 0px), var(--py, 0px)) scale(var(--s, 1));
  transition: transform .5s cubic-bezier(.3,.9,.3,1), filter .4s, opacity .5s, box-shadow .5s;
  /* first-reveal state */
  opacity: 0;
}

.tile.is-in { opacity: 1; }

/* filtered out */
.tile.is-out { display: none; }

/* Tetris pops: leavers puff up then vanish; returners spring back */
.tile.pop-out {
  animation: tile-pop-out .3s cubic-bezier(.55,-.15,.7,.35) both;
  pointer-events: none;
}
@keyframes tile-pop-out {
  0%   { transform: scale(1); opacity: 1; }
  28%  { transform: scale(1.07); opacity: 1; }
  100% { transform: scale(.08); opacity: 0; }
}

.tile.pop-in { animation: tile-pop-in .4s cubic-bezier(.2,.85,.3,1.18) both; }
@keyframes tile-pop-in {
  0%   { transform: scale(.15); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

/* living stills: frames swap with the same pop energy as the filters */
.tile img.poster.frame-out { animation: frame-pop-out .26s cubic-bezier(.55,-.15,.7,.35) both; }
.tile img.poster.frame-in  { animation: frame-pop-in .34s cubic-bezier(.2,.85,.3,1.16) both; }
@keyframes frame-pop-out {
  0%   { transform: scale(1); opacity: 1; }
  30%  { transform: scale(1.05); }
  100% { transform: scale(.2); opacity: 0; }
}
@keyframes frame-pop-in {
  0%   { transform: scale(.2); opacity: 0; }
  100% { transform: scale(var(--parallax, 1)); opacity: 1; }
}

.tile.is-hot {
  z-index: 30;
  box-shadow: 0 24px 60px rgba(0,0,0,.3);
  filter: none !important;
}

/* demo-reel hero: big and centered but never wider than the grid,
   with smaller tiles flanking it and peeking below */
.tile.is-hero {
  grid-column: var(--hero-start, 2) / span var(--hero-span, 5);
  grid-row: span var(--hero-rows, 6);
}

.tile.is-pushed { filter: brightness(.92) saturate(.9); }

.tile-inner { position: absolute; inset: 0; overflow: hidden; border-radius: var(--radius); }

.tile video, .tile img.poster {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: var(--radius);
  transform: scale(var(--parallax, 1));
}

/* ---- still galleries ----------------------------------------------------
   Photos arrive in every shape (2:3, 3:2, 4:5, square). The tile is square
   and each frame is CONTAINED inside it rather than cropped, so nothing gets
   chopped and the tile never changes size as the slideshow cycles. The tile
   is transparent so the page — and the brand-colour hover wash — shows behind
   the frame instead of a grey box.
   No parallax scale here: with object-fit:contain the frame already touches
   the tile edge on its long side, so any scale-up would clip it. */
.tile[data-type="still"] { background: transparent; }
/* A `contain`-fitted image still fills its element box — the painted photo is
   smaller and centred inside it — so border-radius rounds an invisible
   transparent rectangle and the JPEG keeps square corners. Sizing the img to
   its CONTENT (auto + max) and centring it with flex makes the element and
   the photo the same rectangle, so the radius lands on the picture itself. */
.tile[data-type="still"] img.poster {
  position: absolute;
  top: 0; right: 0; left: 0;
  /* ...but the photo's box stops --cap short of the tile's FOOT, so the
     caption below it has clear air instead of printing over the picture (see
     "caption strip" below). margin:auto then re-centres the photo in what is
     left, which is why this stays a shrink-to-fit box and not a flex child. */
  bottom: var(--cap, 30px);
  margin: auto;                 /* centres a shrink-to-fit box on both axes */
  width: auto; height: auto;
  max-width: 100%;
  /* a percentage max-height resolves against the whole TILE, not against the
     top/bottom gap, so `bottom` alone would move nothing — the strip has to
     come out of the cap here as well */
  max-height: calc(100% - var(--cap, 30px));
  object-fit: contain;
  transform: none;
  /* the frame-swap keyframe ends on scale(var(--parallax)) — pin it so a
     photo never pops in a few % too big and then snaps back */
  --parallax: 1;
  border-radius: var(--radius);
  transition: filter .5s;
}
/* Stills carry NO shadow at all — Buddy's call. The tile is a transparent
   square the photo only part-fills, so a box-shadow haloes the empty square;
   moving it onto the picture (drop-shadow on the img) still read as a blocky
   rectangle, because so many of these frames are product shots on white and
   the shadow just outlines that white rectangle against a white page. Videos
   keep their shadow: they're opaque and fill the tile, so it hugs the footage.
   The hover lift on a still comes from the scale alone. */
.tile[data-type="still"].is-hot, .tile[data-type="still"].is-focus { box-shadow: none; }
.tile[data-type="still"].is-hot img.poster,
.tile[data-type="still"].is-focus img.poster { filter: none; }

/* ---- caption strip on a still ----
   .tile-label is anchored to the TILE, not to the photo — which is fine over a
   video (the tile IS the footage) but printed the words straight over any
   photo tall enough to use the tile's full height. The strip above reserves
   room for them. main.js measures each label and writes --cap per tile, so a
   brand that wraps to two lines on a phone gets two lines' worth and a short
   one gives the photo its pixels back; the 30px fallback covers the frame
   before JS has measured. Sitting 6px off the foot rather than 10 keeps the
   strip tight — on a transparent tile there is no visible bottom edge to
   respect, only the picture above it. */
.tile[data-type="still"] .tile-label { bottom: calc(var(--radius) * 0.28); }
/* Drilled into a single client the captions are hidden outright (see
   #grid.is-focused further down), so nothing needs reserving there and the
   photos go back to the tile's full height. !important because main.js writes
   --cap as an inline style. */
#grid.is-focused .tile[data-type="still"] { --cap: 0px !important; }

/* Column count now comes from JS (a width-based default, overridden by the
   header size slider and remembered per browser). */

.tile img.poster { z-index: 1; }
.tile video { z-index: 2; opacity: 0; transition: opacity .5s; }
.tile video.is-playing { opacity: 1; }

.tile-label {
  position: absolute; z-index: 3;
  /* Inset tracks the corner radius. At a fixed 12px the caption sat right on
     the edge once the radius grew with the tile size — the corner curved away
     underneath it. Tying the two keeps the same visual margin at every setting. */
  left: calc(var(--radius) * 0.85 + 4px);
  right: calc(var(--radius) * 0.85 + 4px);
  bottom: calc(var(--radius) * 0.5 + 4px);
  display: flex; flex-direction: column;
  pointer-events: none;
  color: #fff;
}

.tile-label .brand {
  font-family: var(--display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: .04em;
  transform: translateY(8px);
  opacity: 0;
  transition: transform .4s cubic-bezier(.2,.8,.2,1), opacity .4s;
}

.tile-label .title {
  font-size: 12.5px;
  letter-spacing: .12em;
  color: rgba(255,255,255,.85);
  transform: translateY(8px);
  opacity: 0;
  transition: transform .45s cubic-bezier(.2,.8,.2,1) .05s, opacity .45s .05s;
}

.tile.is-hot .brand, .tile.is-hot .title { transform: none; opacity: 1; }

/* keyboard: a focused tile shows its ring and its caption (main.js also gives
   it the hover treatment) */
.tile:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
.tile:focus-visible .brand, .tile:focus-visible .title { transform: none; opacity: 1; }

/* Inside a client's own sub-category you already know whose work this is, so
   the caption doesn't repeat it — the work just plays. It returns on the
   main feed and on a category view. */
#grid.is-focused .tile-label { display: none; }

/* ============ ABOUT PAGE ============ */

.about-page { min-height: 100vh; }

#about {
  padding: clamp(70px, 14vh, 160px) clamp(20px, 8vw, 140px);
  max-width: 980px;
}

.about-logo { width: min(70vw, 420px); max-width: 100%; margin-bottom: 46px; }

#about p {
  font-size: clamp(19px, 2.3vw, 28px);
  line-height: 1.45;
  font-weight: 500;
  max-width: 24em;
}

#about em { color: var(--gold-ink); font-style: normal; }

/* wraps, so extra contact lines (a phone number, an agent) stack instead of
   pushing the row off the side of the column */
.about-contact { margin-top: 44px; display: flex; flex-wrap: wrap; gap: 14px 26px; }

/* ---- ONE label, three columns ----
   EMAIL / INSTAGRAM in column one, WHAT BUDDY'S UP TO in column two and
   INSTAGRAM in column three are the same piece of typography, so they are the
   same RULE — not three rules that look alike until somebody edits one of them.
   Space Grotesk 500 (the weight the header's own nav already uses), 13px, .2em,
   upright, with the accent hairline under the words. No other sizes, no other
   weights, nothing new loaded. */
.about-contact a,
.about-contact .contact-key,
.about-live h2 a {
  font-family: var(--display);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .2em;
  line-height: 1.25;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 2px solid var(--gold);
  padding-bottom: 4px;
  transition: color .2s, border-color .2s;
}
/* on hover the words take the ink teal, because at that point the accent is
   carrying letters rather than drawing a line */
.about-contact a:hover,
.about-contact .contact-key:hover,
.about-live h2 a:hover { color: var(--gold-ink); border-bottom-color: var(--gold-ink); }
.about-contact a:focus-visible,
.about-contact .contact-key:focus-visible,
.about-live h2 a:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* EMAIL and CALL / TEXT are buttons, not links — they open a panel instead of
   firing a mailto. They join the rule above rather than imitating it, so they
   only need the chrome a <button> brings with it stripped back off. */
.about-contact .contact-key {
  margin: 0;
  padding: 0 0 4px;
  border-width: 0 0 2px;
  border-style: solid;
  background: none;
  cursor: pointer;
  text-transform: none;
}
/* open: the hairline becomes the words, so the row shows which panel is out */
.about-contact .contact-key.is-open { color: var(--gold-ink); border-bottom-color: var(--gold-ink); }

/* ---- the panel itself ----
   Sits under the row and pushes the page down rather than floating over it:
   this column is a document, not an app, and nothing here should cover the
   words above it. */
.contact-drop {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* 10px, not 14: at 375px the number plus COPY/CALL/TEXT needs every pixel to
     stay on one line, and a wrapped TEXT button on its own row looks like a
     mistake rather than a choice */
  gap: 8px 10px;
  /* hugs its contents rather than stretching the column: in the wide layout a
     full-width bar would be mostly empty, and in the narrow one it wraps */
  width: max-content;
  max-width: 100%;
  margin-top: 16px;
  padding: 13px 14px;
  border-radius: 12px;
  background: rgba(var(--gold-rgb), .10);
  border: 1px solid rgba(var(--gold-rgb), .38);
  animation: dropin .22s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes dropin { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }

/* the address/number. Monospace so a digit is never mistaken for another, and
   selectable, because some people will always highlight it by hand. */
/* Space Grotesk, like the EMAIL / CALL / TEXT label right above it and the
   COPY / CALL / TEXT buttons beside it — so the panel is one piece of
   typography rather than a system monospace dropped into the page. It WAS
   monospace, to keep digits unmistakable; Buddy looked at it and the mismatch
   cost more than the precision was worth. A touch of letter-spacing keeps a
   phone number readable in a proportional face. */
.contact-val {
  font-family: var(--display);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: .045em;
  color: var(--ink);
  user-select: all;
  margin-right: 2px;
}

.contact-copy, .contact-act {
  font-family: var(--display);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .14em;
  line-height: 1;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid rgba(17,17,17,.22);
  background: #fff;
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: background-color .18s, color .18s, border-color .18s;
}
.contact-copy:hover, .contact-act:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.contact-copy:focus-visible, .contact-act:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
/* copied: the accent, for the couple of seconds it takes to register */
.contact-copy.is-done { background: var(--gold); border-color: var(--gold); color: #111; }

/* On a narrow column the number plus COPY/CALL/TEXT cannot hold one line, and
   letting it wrap strands TEXT on a row by itself. Below this width the value
   takes its own line on purpose and the buttons share the next — two tidy rows
   instead of an accident. */
@media (max-width: 460px) {
  .contact-val { flex: 0 0 100%; margin-right: 0; }
}

/* ---- the written index of the work ----
   Server-rendered, one per page, so /food and /beverage/casamigos are not the
   same document with a different <title>. Quiet by design: it sits under the
   grid for anyone who scrolls that far, and it is what a crawler reads. */
#site-foot, .seo-index {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--gap);
  font-family: var(--body);
}
.seo-index { padding-top: 54px; }
.seo-index h2 {
  font-family: var(--display);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink);
}
.seo-index p { margin-top: 8px; font-size: 13px; line-height: 1.6; color: var(--ink-dim); max-width: 70ch; }
.seo-index a { color: var(--ink-dim); text-decoration: none; border-bottom: 1px solid rgba(17,17,17,.14); }
.seo-index a:hover { color: var(--gold-ink); border-bottom-color: var(--gold-ink); }
.seo-more { line-height: 2 !important; }
.seo-shots { font-variant-numeric: tabular-nums; }
.seo-list { margin-top: 10px; padding: 0; list-style: none; columns: 2; column-gap: 40px; }
.seo-list li { break-inside: avoid; font-size: 12.5px; line-height: 1.75; color: var(--ink-dim); }
.seo-role { opacity: .65; }
@media (max-width: 720px) { .seo-list { columns: 1; } }

/* ---- the two other things people ask him for ----
   Rentals and the robot, under the contact row. These are cards rather than
   another pair of text links because the whole point is that a visitor who came
   for a DP may not know the other two exist — a picture of each site says that
   faster than a URL does. The pictures are the real sites: the rentals hero as
   it is actually rendered, and LA Cinema Robotics' own share card. */
.about-more {
  margin-top: 36px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.more-card {
  /* a ROW, not a banner. As a full-width picture with the text underneath,
     these two read louder than the bio they sit beneath — and they are a
     footnote, not the point of the page. Small thumbnail, text beside it. */
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 11px 9px 9px;
  text-decoration: none;
  color: inherit;
  border: 1px solid rgba(17,17,17,.13);
  border-radius: 11px;
  background: #fff;
  transition: border-color .25s, transform .35s cubic-bezier(.2,.8,.2,1), box-shadow .25s;
}
.more-card:hover {
  border-color: var(--gold);
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(17,17,17,.07);
}
.more-card:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* kept at the shot's own 880x334 so neither picture is cropped — the rentals
   headline and the robot wordmark both survive at this size */
.more-shot {
  /* 160, up from 104. At 104 the shot was 39px tall and both previews read as
     black rectangles; at 160 it is 61px and you can see a nav bar and a headline
     and tell it is a website. Still a fraction of the 469px full-bleed banner
     these started as. */
  flex: 0 0 160px;
  aspect-ratio: 880 / 334;
  border-radius: 6px;
  background: rgba(17,17,17,.07);   /* neutral: the rentals shot is light now, the robot one dark */
  overflow: hidden;
}
.more-shot img {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.more-card:hover .more-shot img { transform: scale(1.04); }

.more-copy { display: block; min-width: 0; }

.more-title {
  display: block;
  font-family: var(--display);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .14em;
  color: var(--ink);
}
.more-blurb {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--ink-dim);
}
.more-go {
  display: block;
  margin-top: 5px;
  white-space: nowrap;   /* a URL broken across two lines reads as a mistake */
  font-family: var(--display);
  font-size: 9.5px;
  letter-spacing: .1em;
  color: var(--gold-ink);
  transition: color .2s;
}

/* The bio column is 469px on a desktop but only 315px on a phone, and a 160px
   thumbnail there left the blurb 121px and four lines of it. Scaled to the
   column so the picture still reads as a website without eating the words. */
@media (max-width: 520px) {
  .more-shot { flex: 0 0 128px; }
  .more-go { font-size: 9px; letter-spacing: .08em; }
}

/* the <h2> itself is only a landmark — the label above is the visible thing */
.about-live h2 { font-size: inherit; font-weight: 400; }

/* ---- WHAT BUDDY'S UP TO — live Instagram stories (About page) ----
   Not a contact sheet: ONE 9:16 story at a time, playing the day's stories back
   to back the way Instagram would, with the segmented bars above it and the
   time and position underneath. Clicking it opens the full viewer further down
   this file. Same ground rules as everything else here: white ground, gold
   accent, var(--radius) corners, no scrim and no text-shadow anywhere. Nothing
   is ever written over the picture — over-the-photo text is the one thing this
   page never does.

   Unlike the feed, this block stays put once the feature is configured: an
   empty tray is normal for most of the day and gets its own quiet card. */

#stories {
  margin-top: clamp(56px, 9vh, 104px);
  padding-top: clamp(30px, 5vh, 56px);
  border-top: 1px solid rgba(17,17,17,.09);
  /* the player's width, derived from the height it is allowed: a 9:16 frame
     tall enough to read but never taller than most of the window */
  --story-w: min(100%, 320px, calc(62vh * 9 / 16));
}

/* ---- the two live columns open on the same line ----
   --sub-slot is the height the line of small type under a label takes up. Only
   the stories column has one, so the feed reserves the same space and leaves it
   empty; without that its grid starts 24px above its neighbour's picture and the
   two columns stop reading as a pair. Retune the sub's type and this one number
   moves with it. */
.about-live > section { --head-gap: 26px; --sub-slot: 32px; }

/* scoped under #stories on purpose: #about p is the big bio type and would
   otherwise win on specificity and blow these two lines up */
#stories .stories-sub {
  margin-top: 12px;
  min-height: 20px;          /* 12 + 20 = --sub-slot */
  max-width: none;
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink-dim);
}

#stories-row { margin-top: var(--head-gap); }

/* the whole player is one button: bars, picture, caption row. A real <button>,
   so it is reachable by keyboard and announced as something you can press. */
.story-now {
  display: block;
  width: var(--story-w);
  max-width: 100%;
  margin: 0; padding: 0;
  -webkit-tap-highlight-color: transparent;
}

/* segmented progress, one bar per story. Gold fill on a faint ink track here;
   the viewer flips the track to white over its dark wash. */
.story-bars {
  display: flex;
  gap: 4px;
  margin-bottom: 9px;
}
.story-bar {
  flex: 1 1 0;
  min-width: 0;
  height: 3px;
  border-radius: 999px;
  background: rgba(17,17,17,.14);
  overflow: hidden;
}
/* on the white page the fill has to be the ink teal — the raw accent is 1.75:1
   there and a 3px bar simply disappears. The viewer's copy of this bar sits on a
   dark wash and flips back to the bright accent. */
.story-bar > i {
  display: block;
  width: 0; height: 100%;
  border-radius: inherit;
  background: var(--gold-ink);
}

.story-stage {
  position: relative;
  display: block;
  aspect-ratio: 9 / 16;
  border-radius: var(--radius);
  overflow: hidden;
  background: rgba(17,17,17,.06);
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.story-now:hover .story-stage { transform: scale(1.012); }

/* contain, never cover: a story is shown whole. A 9:16 story fills this box
   exactly; anything squarer letterboxes onto the faint grey rather than losing
   its edges. */
.story-media {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
  object-fit: contain;
  background: none;
}

/* a live story gets a gold hairline the feed tiles don't have — the only
   signal that this block is today and the grid next to it is the archive */
.story-stage::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  border: 2px solid var(--gold);
  pointer-events: none;
}

/* same white pill as the feed's badge: legible on any photo without darkening it */
.story-badge {
  position: absolute; top: 8px; right: 8px;
  pointer-events: none;   /* decoration: never eat a tap meant for the frame */
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255,255,255,.92);
  color: var(--ink);
  font-size: 10px;
  line-height: 1;
}

/* the caption row lives UNDER the picture, exactly like .ig-cap next door */
.story-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 9px;
}

.story-time, .story-count {
  display: block;
  height: 1.35em;
  line-height: 1.35;
  overflow: hidden;
  white-space: nowrap;
  font-family: var(--display);
  font-size: 11px;
  letter-spacing: .08em;
  color: var(--ink-dim);
}
.story-count { letter-spacing: .12em; }
.story-now:hover .story-time { color: var(--ink); }

/* ---- the two halves of the picture: back on the left, on on the right ----
   Invisible by design: a story tray has never drawn its hit areas, and a
   chevron over the picture would be the first thing written on a photograph
   anywhere in this design. The cursor says which half you are on, and a hint
   fades up on hover for anyone who has not met a story before. */
.story-tap {
  position: absolute; top: 0; bottom: 0;
  width: 50%;
  margin: 0; padding: 0;
  border: 0;
  background: none;
  -webkit-tap-highlight-color: transparent;
  z-index: 2;
}
.story-tap-l { left: 0;  cursor: w-resize; }
.story-tap-r { right: 0; cursor: e-resize; }
.story-tap:focus-visible { outline: 3px solid var(--ink); outline-offset: -6px; border-radius: var(--radius); }

/* the only mark either half leaves: a soft pull of light in from that edge, so
   it reads as "there is more this way" rather than as a button */
.story-tap::after {
  content: "";
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
}
.story-tap-l::after { background: linear-gradient(to right, rgba(0,0,0,.30), transparent 62%); }
.story-tap-r::after { background: linear-gradient(to left,  rgba(0,0,0,.30), transparent 62%); }
.story-tap:hover::after { opacity: 1; }

/* sound. The column autoplays muted, so this is the way in to his audio — it
   sits over the picture only while a video is on screen. */
.story-sound {
  position: absolute; left: 8px; bottom: 8px;
  z-index: 3;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,.55);
  background: rgba(17,17,17,.42);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: #fff;
  font-family: var(--display);
  font-size: 9.5px;
  letter-spacing: .14em;
  line-height: 1;
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease, opacity .3s ease;
}
.story-sound:hover { background: #fff; color: #111; border-color: #fff; }
.story-sound:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.story-sound.is-on { border-color: var(--gold); color: var(--gold); background: rgba(var(--gold-rgb), .18); }
.story-sound.is-on:hover { background: var(--gold); color: #111; }

/* the story's own permalink, in the caption row where the lightbox's link used
   to be — the one thing the viewer offered that tapping through does not */
.story-open {
  flex: 0 0 auto;
  font-family: var(--display);
  font-size: 10px;
  letter-spacing: .12em;
  color: var(--ink-dim);
  text-decoration: none;
  opacity: 0;
  transition: opacity .25s ease, color .2s ease;
}
.story-now:hover .story-open, .story-open:focus-visible { opacity: 1; }
.story-open:hover { color: var(--gold-ink); }
/* a phone never hovers, so on touch it just stays visible rather than being
   a link nobody can reach */
@media (hover: none) {
  .story-open { opacity: 1; }
}

/* nothing live: one card, same shape as the rest so the row keeps its rhythm.
   Warm, not an error — a faint gold wash and his own words. */
/* also scoped under #stories, for the same reason as .stories-sub above */
#stories .story-empty {
  flex: 0 0 clamp(186px, 62vw, 244px);
  display: flex;
  align-items: center;
  align-self: flex-start;
  max-width: none;
  min-height: clamp(84px, 22vw, 106px);
  border-radius: var(--radius);
  background: rgba(var(--gold-rgb), .10);
  border: 1px solid rgba(var(--gold-ink-rgb), .35);
  padding: 15px 18px;
  font-size: 13.5px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--ink);
}

/* ---- INSTAGRAM strip (About page) ----
   A quiet block at the bottom of the page: the latest posts, four across on a
   laptop and two on a phone. js/instagram.js unhides #ig only once real posts
   have arrived, so an unconnected account leaves the page exactly as it was.
   Captions follow the same rule as the feed's tile captions: ink on white,
   no scrim and no text-shadow. */

#ig {
  margin-top: clamp(56px, 9vh, 104px);
  padding-top: clamp(30px, 5vh, 56px);
  border-top: 1px solid rgba(17,17,17,.09);
}

#ig-grid {
  /* the label, plus the empty slot where the stories column has its sub-line */
  margin-top: calc(var(--head-gap) + var(--sub-slot));
  display: grid;
  /* auto-fill does the responsive work on its own — 4 columns in the About
     column's 720-ish px, 2 on a phone, no breakpoint needed */
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--gap);
}

.ig-tile { display: block; text-decoration: none; color: inherit; }

.ig-frame {
  position: relative;
  display: block;
  aspect-ratio: 1;
  border-radius: var(--radius);
  overflow: hidden;
  /* a faint grey while the image is still coming down the wire */
  background: rgba(17,17,17,.06);
}

.ig-frame img {
  width: 100%; height: 100%;
  display: block;
  object-fit: cover;
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.ig-tile:hover .ig-frame img { transform: scale(1.04); }

/* video / carousel marker. White pill, ink glyph: legible over any photo
   without darkening it. */
.ig-badge {
  position: absolute; top: 8px; right: 8px;
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255,255,255,.92);
  color: var(--ink);
  font-size: 10px;
  line-height: 1;
}

/* the caption sits UNDER the photo and its row is always there (hidden or
   not), so revealing it on hover never nudges the grid */
.ig-cap {
  display: block;
  margin-top: 8px;
  height: 1.35em;
  line-height: 1.35;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 11.5px;
  color: var(--ink-dim);
  opacity: 0;
  transition: opacity .35s;
}
.ig-tile:hover .ig-cap, .ig-tile:focus-visible .ig-cap { opacity: 1; }
.ig-tile:focus-visible { outline: none; }
.ig-tile:focus-visible .ig-frame { outline: 3px solid var(--ink); outline-offset: 3px; }

/* a phone has no hover, so the captions simply stay on */
@media (hover: none) { .ig-cap { opacity: .8; } }

/* ============ LIGHTBOX ============ */

#lb-count {
  position: absolute; top: 18px; left: 50%; transform: translateX(-50%);
  font-family: var(--display); font-size: 11px; letter-spacing: .18em;
  color: rgba(255,255,255,.75); z-index: 5;
}

#lightbox {
  position: fixed; inset: 0; z-index: 100;
  /* --lb-wash is set per-asset from its brand/photo colour so the overlay
     tints to the work instead of a generic grey, and stays see-through
     enough that the feed reads behind it */
  background: var(--lb-wash, rgba(8,8,8,.88));
  -webkit-backdrop-filter: blur(16px) saturate(1.1);
  backdrop-filter: blur(16px) saturate(1.1);
  transition: background-color .45s ease;
  display: flex; align-items: center; justify-content: center;
  animation: lbfade .3s ease both;
  /* sideways swipes are ours (main.js steps frames/spots); pinch still zooms */
  touch-action: pan-y pinch-zoom;
}

@keyframes lbfade { from { opacity: 0; } }

#lightbox figure {
  position: relative;
  /* sized for the 82vh the video is capped at below, so a height-bound video
     fills its box edge to edge instead of letterboxing inside its own black.
     dvh (where supported) is the height under iOS Safari's toolbars. */
  width: min(92vw, calc(82vh * var(--lb-ar, 1.7777)));
  width: min(92vw, calc(82dvh * var(--lb-ar, 1.7777)));
  max-height: 88vh;
  max-height: 88dvh;
  display: flex; flex-direction: column; align-items: center;
}
/* photos shrink-wrap; only video keeps the aspect-sized box */
#lightbox.is-photo figure { width: auto; max-width: 92vw; }

/* ---- hover zones over the photo: previous / next FRAME ---- */
.lb-zone {
  position: absolute; top: 0; bottom: 0; width: 34%;
  display: flex; align-items: center;
  border: 0; background: none; padding: 0 22px;
  opacity: 0; transition: opacity .18s ease;
  z-index: 4;
}
.lb-zone-l { left: 0; justify-content: flex-start; cursor: w-resize; }
.lb-zone-r { right: 0; justify-content: flex-end; cursor: e-resize; }
.lb-zone span {
  font-size: 30px; line-height: 1;
  filter: drop-shadow(0 3px 10px rgba(0,0,0,.5));
  transform: translateY(0) scale(.9);
  transition: transform .18s ease;
}
.lb-zone:hover { opacity: 1; }
.lb-zone:hover span { transform: scale(1); }
/* keyboard focus has to show on a zone that is otherwise transparent */
.lb-zone:focus-visible { opacity: 1; outline: 2px solid #fff; outline-offset: -6px; }
/* no hover on touch — keep the zones faintly visible so the frames are
   discoverable at all */
@media (hover: none) {
  .lb-zone { opacity: .85; }
  .lb-zone span { font-size: 24px; }
}
#lightbox.is-single .lb-zone { display: none; }   /* not a gallery — no frames */

#lb-video {
  width: 100%;
  max-height: 82vh;
  max-height: 82dvh;
  border-radius: var(--radius);
  background: #000;                 /* a video really is a fixed rectangle */
  box-shadow: 0 30px 90px rgba(0,0,0,.35);
}

/* A gallery's frames are all different shapes, so forcing the image to the
   figure's width and painting the leftover black gave every photo two black
   edges. The photo sizes itself instead — clean bleed, no bars. */
#lb-img {
  width: auto; height: auto;
  max-width: min(92vw, 1700px);
  max-height: 82vh;
  max-height: 82dvh;
  object-fit: contain;
  border-radius: var(--radius);
  background: none;
  box-shadow: 0 30px 90px rgba(0,0,0,.35);
}

#lightbox figcaption {
  margin-top: 14px;
  display: flex; gap: 14px; align-items: baseline;
  align-self: flex-start;
  color: #fff;
  /* above the frame zones, which span the figure's full height */
  position: relative; z-index: 5;
}

#lb-prev, #lb-next { opacity: .55; transition: opacity .2s, transform .2s; }
#lb-prev:hover, #lb-next:hover { opacity: 1; transform: scale(1.08); }

#lb-brand { font-family: var(--display); font-weight: 700; font-size: 20px; letter-spacing: .04em; }
#lb-title { font-size: 13px; letter-spacing: .14em; color: rgba(255,255,255,.6); }

/* the three round buttons at the screen edges — by id, because the frame
   zones are buttons too and must not inherit this chrome */
#lb-close, #lb-prev, #lb-next {
  position: absolute; z-index: 2;
  width: 52px; height: 52px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.3);
  background: rgba(10,10,11,.5);
  color: #fff;
  font-size: 18px;
  transition: background .2s, color .2s, transform .2s;
}
#lb-close:hover, #lb-prev:hover, #lb-next:hover { background: #fff; color: #000; transform: scale(1.08); }
#lb-close:focus-visible, #lb-prev:focus-visible, #lb-next:focus-visible {
  background: #fff; color: #000; outline: 2px solid #fff; outline-offset: 2px;
}

#lb-close { top: 22px; right: 24px; z-index: 6; }   /* above a wide frame's zone in the corner */
#lb-prev  { left: 24px; top: 50%; margin-top: -26px; }
#lb-next  { right: 24px; top: 50%; margin-top: -26px; }

/* Light washes (pastel brands) flip the type to ink — main.js decides per
   asset from the luminance of the wash as actually blended over the page. */
#lightbox.is-light figcaption, #lightbox.is-light #lb-count { color: #111; }
#lightbox.is-light #lb-title { color: rgba(17,17,17,.7); }
#lightbox.is-light #lb-close, #lightbox.is-light #lb-prev, #lightbox.is-light #lb-next {
  background: rgba(255,255,255,.75); color: #111; border-color: rgba(0,0,0,.25);
}
#lightbox.is-light #lb-close:hover, #lightbox.is-light #lb-prev:hover, #lightbox.is-light #lb-next:hover {
  background: #111; color: #fff;
}
#lightbox.is-light #lb-close:focus-visible, #lightbox.is-light #lb-prev:focus-visible,
#lightbox.is-light #lb-next:focus-visible { outline-color: #111; }

/* ============ CUSTOM CURSOR — bare emoji, no chrome ============ */

/* The outer box ONLY carries the position, and only through transform, which
   the compositor can move without touching layout. It used to be driven with
   left/top — a layout and a repaint on every single frame, which is what a
   friend of Buddy's saw trailing his real pointer on a PC.

   Position and the scale/rotate flourish cannot share one transform, because
   the flourish is animated and the position must never be: the cursor would
   smear toward the pointer. So the states move to the inner span. */
#cursor {
  position: fixed; z-index: 200;
  left: 0; top: 0;
  margin: -19px 0 0 -19px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s;
  will-change: transform;
}

#cursor.is-on, #cursor.is-btn { opacity: 1; }
#cursor.is-on  #cursor-emoji { transform: scale(1) rotate(0deg); }
#cursor.is-btn #cursor-emoji { transform: scale(.7); }

#cursor-emoji {
  font-size: 34px;
  line-height: 1;
  display: block;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,.35));
  /* the flourish lives here now — see #cursor above */
  transform: scale(.4) rotate(-8deg);
  transition: transform .3s cubic-bezier(.2,.8,.2,1);
}

/* ============ LOADING / EMPTY STATE ============ */

.grid-note {
  grid-column: 1 / -1;
  padding: 80px 20px;
  text-align: center;
  color: var(--ink-dim);
  font-family: var(--display);
  letter-spacing: .2em;
  font-size: 13px;
}

/* screen-reader only: the live status line and the page heading */
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ============ SMALLER SCREENS ============ */

/* (below 1100px the reel's full-row span comes from main.js, via --hero-span,
   so its row count keeps following the column count) */

@media (max-width: 720px) {
  .filters { order: 3; width: 100%; justify-content: center; flex-wrap: wrap; }
  .header-main { display: flex; flex-wrap: wrap; height: auto; padding: 10px 14px;
                 justify-content: space-between; }
  .header-main .logo img { height: 28px; }
  /* 33 clients would run off a phone screen — JS caps the drawer at 46vh and
     the inner list scrolls inside it. The inner needs its own viewport-unit
     bound (a % would resolve against the bar's animated 0 height) or it just
     grows to its content and the bar clips it, unreachable. */
  .client-bar-inner { max-height: 46vh; overflow-y: auto; overscroll-behavior: contain; }
  .client-list { padding: 5px 14px 7px; }
  /* the --cm/--rm spans below are drawn for a 2-column grid */
  #grid { --cols: 2; transform-origin: 50% 50%; will-change: transform; }
  .tile { grid-column: span var(--cm, 2) !important; grid-row: span var(--rm, 3) !important; }
  #lb-prev, #lb-next { display: none; }   /* a swipe steps instead (main.js) */
}

/* the "live" tile while scrolling on touch devices */
.tile.is-focus {
  --s: 1.03;
  z-index: 5;
  box-shadow: 0 14px 40px rgba(0,0,0,.22);
}
.tile.is-focus .brand, .tile.is-focus .title { transform: none; opacity: 1; }

/* ============ REDUCED MOTION ============
   An added path, not a change to the default. With the OS flag set the pops,
   pushes, washes and parallax collapse to near-instant — the animations still
   END, so main.js's animationend bookkeeping completes — and the tiles are
   simply there. main.js stops the hover growth, page skew, looping previews
   and frame swaps under the same flag. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  body, #splash, .tile, .tile video, .tile-label .brand, .tile-label .title,
  .client-bar, #lightbox, #cursor, .ig-frame img, .ig-cap, .story-stage { transition-duration: .01ms !important; }
  #splash-logo, #splash-dots span, .tile.pop-out, .tile.pop-in,
  .tile img.poster.frame-out, .tile img.poster.frame-in, #lightbox, #story-viewer {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
  .tile { opacity: 1; --parallax: 1; }
}

/* ---------------- client row on small screens ----------------
   On a phone the client list used to wrap into a block capped at 46vh — with
   20-odd clients under FOOD that swallowed nearly half the screen and pushed
   the actual work below the fold. One side-scrolling row instead: the drawer
   is now a single chip high whatever the client count, and you swipe along it.
   Up to 900px so a phone in landscape (~844px, very little height to spare)
   gets it too. */
@media (max-width: 900px) {
  /* the vertical cap is what made it a wall; a single row doesn't need one */
  .client-bar-inner { max-height: none; overflow-y: visible; }

  .client-list {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    overflow-y: hidden;
    /* keep a swipe inside the row: without this, running off the end hands the
       gesture to the browser and triggers back-navigation mid-browse */
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    scrollbar-width: none;             /* the fade is the affordance, not a bar */
    /* fades the trailing edge so it reads as "keep swiping". Every category has
       16+ clients, so on a phone this always has somewhere to go. */
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 26px), transparent 100%);
            mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 26px), transparent 100%);
  }
  .client-list::-webkit-scrollbar { display: none; }

  .client-btn {
    scroll-snap-align: start;
    flex: 0 0 auto;                    /* never let flex squeeze the labels */
  }
}

/* A still tile is transparent, so its caption sits on the page — and the page
   is washed with that asset's brand colour while the tile is live. Most of
   those brands are strong enough for white, but a third of them are pastels
   where white all but disappears. main.js tags those tiles `on-light` using
   the same luminance test the lightbox already uses, and the caption flips to
   ink. No shadow, no scrim — Buddy has turned both down twice.
   Video tiles are never tagged: their captions sit on footage, not the wash. */
/* WHITE EVERYWHERE — Buddy's call, 2026-09. Stills briefly flipped to ink on
   pale brand washes for contrast; he looked at it and the dark text on a photo
   bothered him more than the soft contrast does ("there isn't really anything
   that's too hard to read"). The flip is gone; captions match the video tiles.
   Do not reintroduce it, and do not "solve" the contrast with a shadow or a
   scrim — both have been rejected twice. */

/* ---------------- "there's more sideways" indicator ----------------
   The fading edge alone wasn't enough of a cue — Buddy couldn't tell the
   client row scrolled without already knowing. A thin track under the row
   shows both THAT it scrolls and WHERE you are in it, which arrows can't do.
   Hidden unless the row actually overflows (main.js adds .can-scroll), and
   desktop never sees it because the list wraps there instead. */
.client-scroll { display: none; }

@media (max-width: 900px) {
  .client-bar-inner.can-scroll .client-scroll {
    display: block;
    height: 2px;
    margin: 0 auto 7px;
    width: 46px;                      /* a hint, not a full-width scrollbar */
    border-radius: 2px;
    background: rgba(17,17,17,.13);
    overflow: hidden;
  }
  .client-scroll i {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: var(--gold-ink, #058284);
    /* width + offset are set from main.js as the row scrolls */
    width: var(--sc-w, 40%);
    transform: translateX(var(--sc-x, 0px));
  }
}

/* ---------------- matted videos ----------------
   A spot whose shape is neither 16:9 nor 9:16 can't fill a tile without losing
   a chunk of frame — Buddy's 4:5 spots were dropping 22% off the sides, taking
   the on-screen copy with it. Those tiles are matted instead: the video sits
   whole and centred with clean space around it, exactly how a photo gallery
   already behaves. Same shrink-to-fit centring as the stills, so border-radius
   lands on the picture and not on an invisible rectangle. */
.tile.is-framed { background: transparent; }
.tile.is-framed video,
.tile.is-framed img.poster {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  margin: auto;                  /* centres whichever axis has slack */
  /* --ar is the spot's real ratio, set in main.js. A <video> has no intrinsic
     size until its metadata loads (these are preload="none"), so auto sizing
     alone collapses it to the browser's default 300x150 and the matte renders
     at 2:1 until hover. Declaring the ratio fixes the box from the first paint. */
  aspect-ratio: var(--ar, 16 / 9);
  /* height-led, not width-led. width:100% forces the box to the tile's width,
     so a wide spot in a tall-ish tile keeps a full-tile BOX and only the pixels
     letterbox inside it — which means the rounded corners get drawn on empty
     space instead of on the picture, the same trap the stills hit. Driving from
     height and letting width follow the ratio (clamped by max-width) makes the
     element box equal the painted frame, so the radius lands on the video. */
  max-width: 100%; max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius);
  background: none;
}
/* the lift rides the picture, not the empty matte around it */
.tile.is-framed.is-hot, .tile.is-framed.is-focus { box-shadow: none; }

/* Which edge the video touches, decided per tile in main.js — see the note
   there on why CSS can't infer it (aspect-ratio is dropped under a max-*
   clamp). Either way the element box ends up equal to the painted frame, so
   the rounded corners sit on the picture and the spacing sits around it. */
.tile.is-framed.fit-w video, .tile.is-framed.fit-w img.poster { width: 100%; height: auto; }
.tile.is-framed.fit-h video, .tile.is-framed.fit-h img.poster { height: 100%; width: auto; }

/* RESET under the tile-size slider. Deliberately quiet — it only appears once
   the slider has been moved off the default, so it is a way back rather than a
   permanent control competing with the nav. */
.size-reset {
  font-family: var(--display);
  font-size: 7.5px;
  letter-spacing: .16em;
  color: var(--ink-dim);
  background: none;
  border: 0;
  padding: 1px 6px 0;
  margin-top: -1px;
  cursor: pointer;
  opacity: .75;
  transition: opacity .2s, color .2s;
}
.size-reset:hover { opacity: 1; color: var(--ink); }
.size-reset:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ---- About: the written page, today, and the archive ----
   Three steps, and each one is a decision rather than a survival strategy.

   Below 900px everything is stacked in reading order: who he is, what he is
   doing today, then the body of work.

   900–1279px is two columns — the bio on the left, and .about-live on the right
   holding the stories above the feed. There isn't room to give the story player
   a column of its own without squeezing the feed to one tile across.

   1280px and up is the three-column page Buddy asked for: BIO | STORIES | FEED,
   side by side, all three top-aligned, each separated by the same hairline. The
   .about-live wrapper steps aside with display:contents so its two sections
   become columns in their own right, and the hairline that ran above a block on
   a phone runs beside it here.

   The bio is ONE wrapper (.about-bio), not a handful of loose children, so
   adding phone numbers or any other contact lines just makes that column longer
   — the other two don't move and nothing has to be re-counted. */
@media (min-width: 900px) {
  #about {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    column-gap: clamp(44px, 6vw, 110px);
    align-items: start;
    max-width: 1440px;
  }
  .about-bio  { grid-area: 1 / 1; min-width: 0; }

  .about-live {
    grid-area: 1 / 2;
    min-width: 0;
    align-self: start;
    padding-left: clamp(30px, 3vw, 54px);
    border-left: 1px solid rgba(17,17,17,.09);
  }

  /* whatever is first in that column starts flush with the top of the rule —
     no horizontal divider on top of a vertical one */
  .about-live > #stories {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }
  /* ...and when stories aren't configured, the feed takes that job. [hidden]
     still matches as a sibling, which is exactly what makes this work. */
  .about-live > #stories[hidden] + #ig {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }
  /* with both showing, the gap between them is a touch tighter than the old
     full-page break — they are two halves of one column, not two pages */
  .about-live > #stories:not([hidden]) + #ig {
    margin-top: clamp(34px, 4.6vh, 56px);
    padding-top: clamp(26px, 3.6vh, 42px);
  }

  /* the right column is narrower than the old full width, so hold the tiles at
     two across rather than letting auto-fill drop them to a single column */
  #ig-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- three across ----
   Each selector below deliberately mirrors one from the 900px block, including
   its [hidden] / :not([hidden]) parts. Those attribute selectors out-specify a
   plain #ig, so a tidier-looking rule here would silently lose to the two-column
   one above and the feed would keep a rule over its head that it no longer needs. */
@media (min-width: 1280px) {
  #about {
    grid-template-columns: minmax(0, 1.08fr) minmax(280px, .6fr) minmax(0, 1.3fr);
    column-gap: clamp(30px, 2.8vw, 64px);
    /* three columns need the side margins back: the generous 8vw gutter of the
       one- and two-column page would cost a whole story's width here */
    padding-inline: clamp(28px, 3.4vw, 110px);
    max-width: 1680px;
  }
  /* 28px bio type belongs in a 700px column, not a 500px one */
  #about p { font-size: clamp(18px, 1.55vw, 25px); }

  /* the wrapper gets out of the way; its padding and rule go to the sections */
  .about-live { display: contents; }

  .about-live > #stories,
  .about-live > #stories[hidden] + #ig,
  .about-live > #stories:not([hidden]) + #ig {
    grid-row: 1;
    align-self: start;
    min-width: 0;
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
    padding-left: clamp(26px, 2.6vw, 50px);
    border-left: 1px solid rgba(17,17,17,.09);
  }
  .about-live > #stories { grid-column: 2; }
  .about-live > #stories[hidden] + #ig,
  .about-live > #stories:not([hidden]) + #ig { grid-column: 3; }

  /* a live block that isn't configured doesn't leave a hole: the page falls
     back to two columns rather than to a gap where it would have been */
  #about:has(#stories[hidden]) {
    grid-template-columns: minmax(0, 1.08fr) minmax(0, 1.3fr);
  }
  #about:has(#stories[hidden]) .about-live > #stories[hidden] + #ig { grid-column: 2; }
  #about:has(#ig[hidden]) {
    grid-template-columns: minmax(0, 1.08fr) minmax(280px, .6fr);
  }
}

/* the archive column is finally wide enough for three across without the
   thumbnails going postage-stamp */
@media (min-width: 1500px) {
  #ig-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---- and on a 27" monitor ----
   Past this the three columns stop growing and the block centres instead. Left
   alignment is the page's habit, but 800px of empty paper down one side is not
   a composition — it is a column that ran out of screen. */
@media (min-width: 1900px) {
  #about {
    max-width: 1840px;
    margin-inline: auto;
  }
}

/* ---- a shorter header on phones ----
   Portrait: trim the padding and the logo so the two rows cost less.
   LANDSCAPE is the real problem — a phone on its side has ~390px of height,
   and the desktop header (76px) plus an open client row ate close to a third
   of the screen before any work showed. Keyed on HEIGHT, not width, because a
   landscape phone is ~844px wide and never matched the 720px phone rules. */
@media (max-width: 720px) {
  .header-main { padding: 6px 14px; }
  .header-main .logo img { height: 24px; }
  /* 9px, not 13: "OVERVIEW" is five characters longer than the "ALL" it
       replaced, and at 13px the four pills needed 351px against the 343px a
       375px phone actually offers — so THINGS dropped to a second row and the
       header grew from 83px to 117px. Trimming the sides buys 32px. */
    .filter-btn { padding: 7px 9px; font-size: 11.5px; }
}

@media (max-height: 560px) and (orientation: landscape) {
  .header-main {
    height: 46px;               /* from 76px */
    padding: 0 clamp(12px, 2.4vw, 28px);
    gap: 10px;
  }
  .header-main .logo img { height: 22px; }
  .filter-btn { padding: 6px 9px; font-size: 11px; letter-spacing: .13em; }
  .about-link { font-size: 11px; }
  /* the client row shrinks with it, and its cap has to come down too or the
     drawer alone can still take half the screen */
  .client-btn { padding: 5px 11px; font-size: 10.5px; }
  .client-list { padding: 3px 12px 5px; }
  .client-bar-inner { max-height: 30vh; }
  .size-ctl { display: none; }   /* no room, and the slider is already hidden under 860px */
}
