/* ==================================================================
   Director portfolio — no build step. Fonts are self-hosted (no calls
   out to Google), 60KB total across three woff2 files.

   Type follows arthurvalverde.com's name/nav face throughout — Archivo
   Narrow, SIL OFL — including the grid's project titles, which started
   as a Didone stand-in (Cinzel) but were moved onto the header's own
   face instead: one typeface for the whole site.
   ================================================================== */

@font-face{
  font-family:'Archivo Narrow';
  src:url('fonts/archivo-narrow-latin.woff2') format('woff2');
  font-weight:400 700; font-style:normal; font-display:swap;
  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:'Archivo Narrow';
  src:url('fonts/archivo-narrow-latin-ext.woff2') format('woff2');
  font-weight:400 700; font-style:normal; font-display:swap;
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,
    U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,
    U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'Cinzel';
  src:url('fonts/cinzel-latin.woff2') format('woff2');
  font-weight:400 900; font-style:normal; font-display:swap;
}
:root{
  --black:#000;
  --white:#fff;
  --ink:#111;
  --muted:#6d6d6d;
  --rule:#e6e6e6;

  --sans: "Archivo Narrow", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --display: "Cinzel", Georgia, "Times New Roman", serif;

  --grey: #9c9c9c;              /* Arthur's secondary type colour */
  --masthead-h: 52px;
  /* The bar's own height, and the space every panel reserves for it — one
     number, so the last row meets it exactly with nothing left over.

     It is a flat 44px rather than the old clamp(44px,9vh,60px): that clamp
     was never the height the footer actually drew (it sized itself from its
     padding, ~44px), so tying the bar to it made the bar grow to 60px on a
     tall screen. 44px is the height this footer has always looked right at.
     Sizing it off the viewport also makes the row above it shift height for
     no reason as the window changes. */
  --footer-h: 44px;
  --footer-total: calc(var(--footer-h) + env(safe-area-inset-bottom, 0px));
  /* .social's own intrinsic height as a secondary fixed footer: 18px padding
     top and bottom + the 40px icons + the 1px border-top between them, none
     of which are responsive, so this is exact rather than approximate. */
  --social-h: 77px;
  --ease: cubic-bezier(.22,.61,.36,1);
  --page-ms: 380ms;

  /* Contact page's vertical rhythm, smallest to largest — every gap on that
     page is one of these three, nothing in between. 2 and 3 are exact
     multiples of 1 (not independent clamps that merely land close at each
     end), so the ratio holds at every viewport width, not just at the two
     endpoints — that's what makes it one system instead of three near-misses.
       1: between siblings in a list (the award lines)
       2: heading to the content it introduces
       3: a full section break (bio copy → Awards heading, end of awards →
          whatever block comes next) */
  --space-1: clamp(14px,2.2vh,20px);
  --space-2: calc(var(--space-1) * 2);
  --space-3: calc(var(--space-1) * 4);
}

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

html,body{
  margin:0; padding:0;
  height:100%;
  background:var(--black);
  color:var(--ink);
  font-family:var(--sans);
  -webkit-font-smoothing:antialiased;
  -webkit-text-size-adjust:100%;
}

/* Lock the document: the panels do their own scrolling. */
body{ overflow:hidden; overscroll-behavior:none; }

/* The whole document is what goes full screen, so it has to keep filling the
   screen once it gets there. Browsers give a fullscreen element their own
   sizing and a white backdrop by default, which would letterbox the site in
   white and leave the panels measuring the wrong width — the swipes are laid
   out in pixels off that width. */
:root:fullscreen,
:root:-webkit-full-screen{
  width:100%; height:100%;
  background:var(--black);
}

img{ display:block; max-width:100%; }
button{ font:inherit; color:inherit; background:none; border:0; padding:0; cursor:pointer; }
a{ color:inherit; text-decoration:none; }

/* ------------------------------------------------------------------
   Deck — three stacked full-viewport panels, moved with transforms.
   Slot -1 / 0 / +1 is assigned in JS so the loop wraps in both directions.
   ------------------------------------------------------------------ */
.deck{ position:fixed; inset:0; overflow:hidden; }

.view{
  position:absolute; inset:0;
  width:100%; height:100%;
  transform:translate3d(100%,0,0);   /* JS overwrites immediately */
  will-change:transform;
  backface-visibility:hidden;
}
.view--animate{ transition:transform var(--page-ms) var(--ease); }

/* Only the panel in slot 0 takes input. */
.view[aria-hidden="true"]{ pointer-events:none; }
.view[aria-hidden="true"] :where(a,button){ pointer-events:none; }

/* ==================================================================
   GRID
   ================================================================== */
/* Black, not white: the rows reserve --footer-h at the bottom but the footer
   itself is shorter than that, so a pale strip used to sit above it at the end
   of the scroll — and rubber-band overscroll flashed the same white. Black
   matches the masthead and footer either side of it, and it also means a
   poster that has not decoded yet fades up out of the dark rather than off a
   white page. */
.view--grid{
  background:var(--black);
  overflow-y:auto;
  overflow-x:hidden;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-y:contain;
}

.masthead{
  position:sticky; top:0; z-index:5;
  display:flex; align-items:flex-start; justify-content:space-between; gap:16px;
  padding:calc(env(safe-area-inset-top) + clamp(13px,1.9vw,24px))
          clamp(15px,1.56vw,20px)
          clamp(11px,1vw,12px);
  background:var(--black);
  color:var(--white);
}
/* Name and role read as one continuous line, split by a rule:
   NAME | ROLE */
.masthead__name{
  display:flex; flex-direction:row; align-items:baseline;
  min-width:0;
}
.masthead__who,
.masthead__role{
  /* Scales with the viewport rather than sitting on a floor: the whole line
     is ~20.5x the font-size, so on a 320px phone it has to come down to keep
     off the edges. The 13px floor is the narrowest it ever needs to go. */
  font-size:clamp(13px,4.3vw,24px);
  line-height:1.2;
  letter-spacing:.02em;
  text-transform:uppercase;
  font-weight:600;
  color:var(--white);
  white-space:nowrap;
}
/* The divider is furniture, not type — it sits back from both halves. */
.masthead__role::before{
  content:"|";
  margin:0 .45em;
  font-weight:400;
  opacity:.45;
}
.masthead__link{
  font-size:clamp(12px,1.25vw,16px);
  letter-spacing:.02em;
  text-transform:uppercase;
  font-weight:400;
  color:var(--grey);
  transition:color .2s ease;
}
.masthead__link:hover{ color:var(--white); }

/* On a phone the name is the only thing in the bar, and left-aligned it read
   as pushed against the edge rather than placed. Centred it sits as a title.
   Desktop keeps the left alignment, where the line is short against the width
   and centring would leave it floating. */
@media (max-width:767px){
  .masthead{ justify-content:center; }
  .masthead__name{ justify-content:center; text-align:center; }
}

/* Full-bleed banner rows, 33vh tall — matching the reference site. */
.grid{ display:block; padding-bottom:var(--footer-total); }

.tile{
  position:relative; display:block;
  width:100%;
  aspect-ratio:16/7;
  overflow:hidden;
  background:#0d0d0d;
}
.tile__img{
  --poster-zoom:1;           /* raise per project to crop baked-in letterbox bars */
  width:100%; height:100%;
  object-fit:cover;          /* centre-crop, like the reference */
  object-position:center;
  transform:scale(var(--poster-zoom));
  transition:transform 900ms var(--ease), opacity .5s ease;
  opacity:0;
}
.tile__img.is-loaded{ opacity:1; }
@media (hover:hover){
  .tile:hover .tile__img{ transform:scale(calc(var(--poster-zoom) * 1.06)); }
}

/* Title: Arthur's treatment — a Didone, tracked wide, vertically centred
   and inset from the left, hidden until you hover the row. */
.tile__scrim{
  position:absolute; inset:0;
  background:rgba(0,0,0,.30);
  opacity:0;
  transition:opacity .3s cubic-bezier(.33,0,.2,1);
  pointer-events:none;
}
.tile__title{
  position:absolute;
  left:50%; top:50%; transform:translate(-50%,-50%);
  margin:0;
  width:calc(100% - 2 * clamp(18px,2.73vw,35px));
  text-align:center;
  overflow-wrap:break-word;
  color:var(--white);
  font-family:var(--display);
  font-size:clamp(16px,2.6vw,30px);
  line-height:1.25;
  letter-spacing:.02em;
  text-transform:none;
  font-weight:400;
  font-style:normal;
  text-shadow:0 1px 26px rgba(0,0,0,.34);
  pointer-events:none;
  opacity:0;
  transition:opacity .3s cubic-bezier(.33,0,.2,1);
}
@media (hover:hover){
  .tile:hover .tile__title,
  .tile:focus-visible .tile__title{ opacity:1; }
  .tile:hover .tile__scrim,
  .tile:focus-visible .tile__scrim{ opacity:1; }
}
/* Touch has no hover, so app.js walks the rows on a timer instead and marks
   one at a time. Same three moves as the hover state, so a phone sees the
   film exactly the way a cursor would reveal it. */
@media (hover:none){
  /* This walk is ambient rather than a reply to a tap, so it runs slower
     than the hover state, and the fade shares the zoom's curve so the two
     halves of the reveal land as one move instead of a blink over a drift.
     At 4s a row, an 0.8s fade also leaves the outgoing and incoming rows
     genuinely crossfading rather than cutting. */
  .tile__title,
  .tile__scrim{ transition:opacity .8s var(--ease); }
  .tile__img{ transition:transform 1600ms var(--ease), opacity .5s ease; }

  .tile.is-active .tile__title,
  .tile.is-active .tile__scrim{ opacity:1; }
  .tile.is-active .tile__img{ transform:scale(calc(var(--poster-zoom) * 1.06)); }
}

/* Cinzel (serif) for grid project titles, centred both axes. */

/* ==================================================================
   REEL
   ================================================================== */
.view--reel{ background:var(--black); overflow:hidden; }

.reel-stage{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  background:var(--black);
}

/* The track carries both slides and is what actually animates. */
.reel-track{ position:absolute; inset:0; will-change:transform; }
.reel-track.is-animating{ transition:transform 420ms var(--ease); }

.reel-slide{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  backface-visibility:hidden;
}

/* Letterbox: fit the video's real shape inside the viewport, never crop. */
.reel-box{
  --ar: 1.7778;
  position:relative;
  width:  min(100vw, calc(100vh * var(--ar)));
  height: min(100vh, calc(100vw / var(--ar)));
  background:var(--black);
}
@supports (width:100dvw){
  .reel-box{
    width:  min(100dvw, calc(100dvh * var(--ar)));
    height: min(100dvh, calc(100dvw / var(--ar)));
  }
}
.reel-video{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  border:0; display:block;
}

.reel-hit{
  position:absolute; inset:0;
  width:100%; height:100%;
  cursor:default;
  -webkit-tap-highlight-color:transparent;
}

/* ---- chrome: deliberately almost invisible ---- */
.reel-chrome{ position:absolute; inset:0; pointer-events:none; }
.reel-chrome > *{ pointer-events:auto; }

.chev, .reel-close{
  position:absolute;
  display:grid; place-items:center;
  width:44px; height:44px;
  color:var(--white);
  opacity:.22;
  transition:opacity .25s ease;
}
.chev svg, .reel-close svg{
  width:24px; height:24px;
  fill:none; stroke:currentColor; stroke-width:1.5;
  stroke-linecap:round; stroke-linejoin:round;
}
.chev:hover, .reel-close:hover{ opacity:.85; }

.chev--up{   left:50%; transform:translateX(-50%); top:max(10px, env(safe-area-inset-top)); }
.chev--down{ left:50%; transform:translateX(-50%); bottom:calc(var(--footer-total) + 10px); }
.reel-close{ top:max(10px, env(safe-area-inset-top)); right:10px; }

/* Bottom right, sitting on the same line as the down chevron and clear of the
   footer. Away from the close button at the top, so the two never collide on a
   narrow screen. Same near-invisible treatment as the rest of the chrome. */
.reel-fs{
  position:absolute;
  bottom:calc(var(--footer-total) + 10px);
  right:max(10px, env(safe-area-inset-right));
  display:grid; place-items:center;
  width:44px; height:44px;
  color:var(--white);
  /* A third brighter than the rest of the chrome (.22). The chevrons and the
     close button are shortcuts for things you can already do by flicking or
     swiping; this one is the only way to reach full screen, so it is worth
     finding without hunting for it. */
  opacity:.29;
  transition:opacity .25s ease;
  -webkit-tap-highlight-color:transparent;
}
.reel-fs svg{
  grid-area:1/1;                       /* both icons stack; only one shows */
  width:22px; height:22px;
  fill:none; stroke:currentColor; stroke-width:1.5;
  stroke-linecap:round; stroke-linejoin:round;
}
.reel-fs:hover{ opacity:.85; }
.reel-fs__out{ display:none; }
.reel-fs.is-full .reel-fs__in{ display:none; }
.reel-fs.is-full .reel-fs__out{ display:block; }
/* Nothing to offer if the browser cannot do it — app.js sets this. */
.reel-fs[hidden]{ display:none; }

/* Entry hint — chevrons pulse once, then settle back to near-invisible. */
.reel-chrome.is-hinting .chev{ animation:hint 2.4s var(--ease) 1 both; }
.chev--down{ animation-delay:.12s; }
@keyframes hint{
  0%   { opacity:0; }
  18%  { opacity:.9; }
  55%  { opacity:.9; }
  100% { opacity:.22; }
}
.chev--up.is-nudge{   animation:nudgeUp .5s ease; }
.chev--down.is-nudge{ animation:nudgeDown .5s ease; }
@keyframes nudgeUp{   50%{ transform:translate(-50%,-5px); opacity:.8; } }
@keyframes nudgeDown{ 50%{ transform:translate(-50%, 5px); opacity:.8; } }

/* Edge click zones — pointer devices only, so they never eat a tap.
   They are 70px wide and full height, which covers the corners where the real
   buttons sit, and being last in the markup they used to paint over them: a
   click on the close button landed on the right edge and went to Contact, and
   one on the full-screen button landed on the left edge and went back to the
   grid. The buttons are named controls, so they win. */
.edge{ position:absolute; top:0; bottom:0; width:70px; cursor:pointer; z-index:1; }
.chev, .reel-close, .reel-fs{ z-index:2; }
.edge--left{ left:0; }
.edge--right{ right:0; }
@media (hover:none), (max-width:767px){
  .edge, .chev, .reel-close{ display:none; }
  .reel-chrome.is-hinting .chev{ display:grid; }  /* hint still shows on touch */
}

/* ---- the two states a black rectangle cannot tell apart on its own ----
   Buffering and a dead file both leave the slide black. The spinner says
   "still coming", the note says "not coming" — without them a slow link and
   a broken URL look identical, which is the state the reel used to sit in. */
.reel-spinner{
  position:absolute; left:50%; top:50%;
  width:28px; height:28px; margin:-14px 0 0 -14px;
  border:1.5px solid rgba(255,255,255,.22);
  border-top-color:rgba(255,255,255,.85);
  border-radius:50%;
  opacity:0;
  transition:opacity .3s ease;
  pointer-events:none;
}
/* Only after a beat — a flick that buffers for 200ms should not flash a
   spinner at you on every single move. */
.reel-slide.is-waiting .reel-spinner{
  opacity:1;
  transition-delay:.45s;
  animation:spin 900ms linear infinite;
}
@keyframes spin{ to{ transform:rotate(360deg); } }

.reel-note{
  position:absolute; left:50%; bottom:calc(var(--footer-total) + 54px);
  transform:translateX(-50%);
  max-width:min(90vw,420px);
  padding:10px 16px;
  text-align:center;
  color:var(--white);
  background:rgba(0,0,0,.62);
  font-size:clamp(12px,1.5vw,14px);
  letter-spacing:.04em;
  line-height:1.5;
  border-radius:2px;
}
.reel-note[hidden]{ display:none; }

@media (prefers-reduced-motion:reduce){
  .reel-slide.is-waiting .reel-spinner{ animation:none; }
}

/* ==================================================================
   CONTACT
   ================================================================== */
/* .view--contact carries the panel's slide transform, which makes it the
   containing block for any position:fixed descendant (here, .social) — so
   the scrolling has to happen on .contact instead. A fixed element scrolls
   right along with its containing block's own overflow if it's rendered
   inside that same overflow, no matter that "fixed" would normally exempt
   it — position:fixed under a transformed ancestor is really position:
   absolute relative to that ancestor, and absolute descendants scroll with
   whatever ancestor's overflow contains them. Keeping the scroll on a
   child of .view--contact, with .social a *sibling* of that child rather
   than nested inside it, is what lets .social stay outside the scroll
   while still using .view--contact as its fixed/absolute anchor. */
.view--contact{
  background:var(--white);
  overflow:hidden;
}
.contact{
  height:100%;
  overflow-y:auto; overflow-x:hidden;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-y:contain;
  display:flex; flex-direction:column;
  padding:0 clamp(22px,6vw,40px);
}
.contact__back{
  position:absolute; top:max(14px, env(safe-area-inset-top)); right:clamp(16px,5vw,28px);
  width:34px; height:34px; display:grid; place-items:center;
  color:var(--ink); opacity:.30; transition:opacity .2s ease;
}
.contact__back svg{ width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.4; stroke-linecap:round; }
.contact__back:hover{ opacity:.8; }

.contact__inner{
  flex:1;
  /* Wide enough that every bio line lands as a single statement — the
     longest is 583px. Drop this and lines start orphaning their last words. */
  max-width:600px;
  width:100%;
  margin:0 auto;
  /* Sits close under the X rather than leaving it stranded above a big
     empty gap — the whole page reads as one aligned block, not two. */
  padding-top:calc(env(safe-area-inset-top) + 52px);
  /* .social is now a fixed secondary footer stacked above the site footer
     (see below) rather than the last thing in flow, so this has to reserve
     room for both — otherwise the last award line ends up permanently
     stuck underneath them with no way to scroll it into view. */
  padding-bottom:calc(var(--footer-total) + var(--social-h) + 24px);
}
.contact__name{
  margin:0 0 clamp(26px,5vh,40px);
  font-size:clamp(19px,1.97vw,25.2px);
  line-height:1.3;
  letter-spacing:.02em;
  text-transform:uppercase;
  font-weight:400;
  color:#111;
}
/* Same colour and weight as the name above it — only the size differs. */
.contact__role{
  display:block;
  margin-top:2px;
  font-size:clamp(12px,1.17vw,15px);
  letter-spacing:.10em;
  font-weight:400;
  color:#111;
}
/* Shared text scale across the contact page — Arthur's 15px / 2.2 rhythm. */
.contact__inner :where(p, a, span, li){
  font-size:15px;
  letter-spacing:.03em;
  line-height:2.2;
}

.bio{ margin:0 0 var(--space-3); }
.bio p{ margin:0; color:#999; }
/* A heading sits close to its own copy — half the gap of an actual section
   break, which is exactly what --space-1 is (--space-2 is defined as 2x
   it), so this reuses the token rather than inventing a "half of space-2". */
.bio p:first-child{ margin-bottom:var(--space-1); }

/* CONTACT and REPRESENTATION are the same component — one rule set, so the
   two blocks can never drift apart. */
.block{
  border-top:1px solid var(--rule);
  padding-top:clamp(14px,2.4vh,20px);
}
/* renderContact() sets innerHTML to '' when there's nothing to show (no
   email/whatsapp rows, no representation) — collapse the block entirely
   rather than leaving a stray empty rule line. */
.block:empty{ display:none; }
#details{ margin-bottom:clamp(18px,3vh,26px); }
.block h2{
  margin:0;
  font-size:15px; letter-spacing:.03em; line-height:2.2;
  text-transform:uppercase; font-weight:400; color:#111;
}
/* Two columns so the values line up however long the label is. */
.block ul{
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:max-content 1fr; column-gap:1.6em;
}
.block li{ display:contents; }
.block .label{
  color:#111;
  font-size:11px;
  font-weight:400;
  letter-spacing:.08em;
  line-height:2.2;
  text-transform:uppercase;
}

/* "James Teh" and "Awards & Recognition" are the bio's two headings — same
   treatment as a label (weight, colour, caps, tracking-in-em so it scales
   with it) but sized above the copy underneath rather than matching
   EMAIL/USA. Position-based because the array's shape is fixed: name, then
   the body paragraph, then the awards heading, then one line per award. */
.bio p:first-child,
.bio p:nth-child(3){
  color:#111;
  font-size:16px;
  font-weight:400;
  letter-spacing:.08em;
  line-height:1.2;
  text-transform:uppercase;
}
/* Heading → the content it introduces: same (tight) gap every time, same
   reasoning as the name above — a heading belongs with its own copy. */
.bio p:nth-child(3){ margin-bottom:var(--space-1); }
/* The rest of the bio sentence, under the "James Teh" heading — smaller,
   sentence case, and the same light grey as the awards below it. Bottom
   margin is the section break to the next heading — twice the heading→copy
   gap above, since this is a different, looser relationship (end of one
   section, not "belongs with the copy" any more). */
.bio p:nth-child(2){
  font-size:13px;
  line-height:2;
  text-transform:none;
  margin-bottom:var(--space-2);
}
/* Awards read as a dense list, not prose — the shared 2.2 rhythm is too
   loose for one-liners this short. Gap goes between lines only: the last
   award's own trailing space comes from .bio's bottom margin instead, so
   the two don't stack into a double gap before whatever follows. */
.bio p:nth-child(n+4){ line-height:1.5; }
.bio p:nth-child(n+4):not(:last-child){ margin-bottom:var(--space-1); }
.block a{
  color:#999;
  border-bottom:1px solid transparent;
  transition:color .2s ease, border-color .2s ease;
}
.block a:hover{ color:#111; border-bottom-color:#111; }
.block span:not(.label){ color:#999; }

/* On a narrow phone the two columns stop fitting, so the region sits
   above its agency instead of beside it. */
@media (max-width:520px){
  .block ul{ display:block; }
  .block li{ display:block; margin-bottom:1em; }
  .block li:last-child{ margin-bottom:0; }
  .block .label{ display:block; line-height:1.6; }
}

/* A second fixed footer, stacked directly above the site footer — the
   contact page's own copy scrolls up underneath and disappears behind it,
   same as it already disappears behind the site footer below that. Colours,
   gap, padding and the border-top are untouched from the in-flow version;
   only the positioning changed. */
.social{
  position:fixed; left:0; right:0; bottom:var(--footer-total); z-index:4;
  height:var(--social-h); box-sizing:border-box;
  background:var(--white);
  display:flex; align-items:center; justify-content:center;
  gap:clamp(26px,7vw,38px);
  padding:18px 0;
  border-top:1px solid var(--rule);
}
.social a{ display:grid; place-items:center; width:40px; height:40px; opacity:.55; transition:opacity .2s ease; }
.social a:hover{ opacity:1; }
.social svg{ width:21px; height:21px; fill:currentColor; }

/* ==================================================================
   SITE FOOTER — same treatment as the masthead (black, Archivo Narrow,
   uppercase, tracked), but fixed to the bottom and present on all three
   pages rather than sticky inside one scroll container. Labels app.js sets
   per page so the arrow that actually fires (see the keydown handler) is
   always the one named here.
   ================================================================== */
.site-footer{
  position:fixed; left:0; right:0; bottom:0; z-index:5;
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  /* Height, not padding, so it is exactly what the panels reserve for it.
     The safe-area inset sits inside that height as bottom padding, which
     keeps the labels clear of the iPhone home indicator. */
  height:var(--footer-total);
  padding:0 clamp(15px,1.56vw,20px) env(safe-area-inset-bottom, 0px);
  background:var(--black);
}
.site-footer__link{
  display:flex; align-items:center; gap:.5em;
  font-size:clamp(12px,1.6vw,15px);
  letter-spacing:.08em;
  text-transform:uppercase;
  font-weight:600;
  color:var(--white);
  opacity:.75;
  transition:opacity .2s ease;
}
.site-footer__link:hover{ opacity:1; }
/* DOM order alone gives each link its correct reading order (arrow-then-
   label on the left, label-then-arrow on the right) — no flex reversal
   needed, and one here previously flipped the right link's arrow onto the
   wrong side. */
.site-footer__arrow{
  font-weight:800;
  font-size:1.3em;
  line-height:1;
}

/* ==================================================================
   Accessibility
   ================================================================== */
/* Present to assistive tech and the document outline, invisible on screen. */
.sr-only{
  position:absolute !important;
  width:1px; height:1px;
  margin:-1px; padding:0; border:0;
  clip-path:inset(50%);
  overflow:hidden; white-space:nowrap;
}
:where(a,button):focus-visible{ outline:2px solid #4c8dff; outline-offset:3px; border-radius:2px; }
/* The reel panel is a focus holder, not a control — it carries no ring.
   The chevrons, the close button and the sound chip keep theirs. */
.view--reel:focus,
.view--reel:focus-visible{ outline:none; }
.reel-hit:focus-visible{ outline:2px solid #4c8dff; outline-offset:-6px; }

@media (prefers-reduced-motion:reduce){
  .view--animate{ transition-duration:1ms; }
  .tile__img{ transition:opacity .2s ease; }
  @media (hover:hover){ .tile:hover .tile__img{ transform:none; } }
  @media (hover:none){ .tile.is-active .tile__img{ transform:none; } }
  .reel-frame{ transition-duration:1ms; }
  .reel-chrome.is-hinting .chev{ animation:none; opacity:.5; }
}
