/* ==========================================================================
   DESIGN TOKENS
   Change these six values and the whole site's palette/type updates.
   ========================================================================== */
:root {
  --ink:        #0d0d0c;   /* primary background */
  --ink-raised: #161613;   /* slightly lifted panels, modal bg */
  --cream:      #f1ece1;   /* primary text on dark */
  --cream-dim:  #a8a297;   /* secondary text on dark */
  --burnt:      #c1622b;   /* unused — kept defined in case the orange accent comes back; every former usage now points at --gold */
  --gold:       #f6ba02;   /* sampled from the yellow logo mark — now the sitewide accent (was --burnt) */
  --line:       rgba(241, 236, 225, 0.14);

  --font-display: 'Bricolage Grotesque', sans-serif;
  --font-nav:     'Plus Jakarta Sans', sans-serif;
  --font-body:    'Plus Jakarta Sans', sans-serif;
  /* Nav-only voice, matching /work's header (css/work.css --font-mono) —
     system stack, zero network cost, used for nav links/callout so the
     two pages' headers read as one shared component. */
  --font-mono:    ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;

  --nav-h: 84px;
  --gutter: clamp(24px, 5vw, 64px);
  --card-radius: 18px; /* shared by deck cards + case study cards */
}

* { box-sizing: border-box; }

/* No global scroll-behavior:smooth — it was fighting initControlledScroll()
   (js/main.js), which needs every hero-pin scrollTo() to land instantly,
   not animate. Work/About/Contact anchor clicks still get a smooth scroll,
   just via a small JS handler (see main.js "SMOOTH ANCHOR NAV") instead of
   this blanket CSS switch, so the two scroll behaviors can differ by
   context instead of fighting over one shared setting.
   overscroll-behavior-y:contain on both html and body — stops the
   browser's native pull-to-refresh/rubber-band bounce from engaging
   during the touch easter-egg reveal and the touch scroll-speed cap
   (main.js), both of which drive scroll manually and need to be the only
   thing that happens when a finger drags at the top of the page. */
html {
  background: var(--ink);
  overscroll-behavior-y: contain;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--cream);
  font-family: var(--font-body);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: contain;
}

a { color: inherit; text-decoration: none; }

/* ==========================================================================
   HERO LOAD GATE
   See the inline blocks tagged "HERO LOAD GATE" in index.html <head> and
   the dispatch at the bottom of js/main.js. Two separate race conditions
   were both painting broken-looking content before the scene was ready:

   1. The panorama (assets/hero/desert-panorama.webp, ~1.2MB) is a CSS
      background-image on .desert-pan, discovered by the browser only
      after style.css has downloaded and parsed — while the cloud
      billboard (a plain <img> in the markup) paints almost immediately.
   2. Every screen's WIDTH and the cloud's position are set by JS
      (sizeScene()/the cloud's gsap.set() in main.js) — synchronous
      top-level code that
      can't run until gsap.min.js + ScrollTrigger.min.js + main.js have
      all downloaded (they're plain <script> tags at the bottom of
      <body>, no defer). Until then .desert-screen elements have no
      width, and any absolutely-positioned scene text still renders at
      its unstyled default position instead of its real one.

   Net effect without this gate: the cloud, and fragments of hero/scene
   text at the wrong position, floating over a bare ink-black frame for
   up to a second or so before the real scene snapped into place.

   Hiding the entire #hero-pin (not just the individual offenders) is
   the fix — opacity:0 doesn't stop layout/measurement from happening
   underneath (sizeScene() still reads real clientWidth/clientHeight),
   it just stops any of it from being painted until it's actually
   correct. html.hero-ready is added by the inline head script only once
   BOTH the panorama image has loaded AND main.js's initial synchronous
   setup has run (it dispatches a 'hero-scene-sized' event — see the
   bottom of main.js), or a safety timeout fires so a slow/broken CDN
   load doesn't hide the hero forever. */
html:not(.hero-ready) #hero-pin {
  opacity: 0;
}
#hero-pin {
  transition: opacity 220ms ease-out;
}

/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* ==========================================================================
   NAV — one small, always-on circle-menu, at every breakpoint, for the
   whole scroll. No more dock-to-bottom/dock-to-top choreography and no
   more "inline links on desktop, bubble on mobile" split: the corner
   trigger IS the nav now, everywhere, from the first frame — see
   js/main.js, which no longer hides/docks .site-nav at all. work.html
   uses this exact same component (duplicated into css/work.css, same
   reasoning as the sheet-fetch helpers in js/work.js) so the two pages
   read as one site.
   ========================================================================== */
.site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--nav-h);
  z-index: 100;
  pointer-events: none; /* only its actual children opt back in below */
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color .3s ease, border-color .3s ease;
}
.site-nav.is-scrolled {
  background: var(--ink);
  border-bottom-color: var(--line);
}

/* Logo + "Open to work"/"Get in touch" — fixed top-right, independent of
   the trigger/panel below (both are its own fixed elements too), so
   nothing here needs a transform or a dock-state variant anymore. */
.nav-right {
  position: fixed;
  top: 18px;
  right: var(--gutter);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 16px;
  pointer-events: auto;
}

/* .nav-contact-trigger is a real <button> (Contact + Get in touch both
   open the same dropdown — see main.js "CONTACT DROPDOWN"), styled to
   read identically to the <a> it replaced. Reset first, since a <button>
   carries browser chrome .nav-link/.nav-callout-link never had to fight. */
.nav-contact-trigger {
  appearance: none;
  background: none;
  border: none;
  border-bottom: 1px solid transparent;
  padding: 0;
  padding-bottom: 4px;
  margin: 0;
  font: inherit;
  cursor: pointer;
}
.nav-callout-link.nav-contact-trigger { border-bottom-color: var(--gold); padding-bottom: 0; }

/* ==========================================================================
   CONTACT DROPDOWN — anchored under the nav's right edge on desktop
   (matches .nav-menu-panel's tablet/mobile anchoring below), a small
   floating panel rather than a full overlay: this is a quick "leave a
   note or grab a slot" moment, not a destination.
   ========================================================================== */
.contact-dropdown {
  position: fixed;
  top: calc(var(--nav-h) + 10px);
  right: var(--gutter);
  z-index: 105;
  width: min(340px, calc(100vw - 2 * var(--gutter)));
  background: var(--ink-raised);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.6);
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  max-height: calc(100dvh - var(--nav-h) - 24px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.contact-dropdown.is-open { opacity: 1; transform: translateY(0); pointer-events: auto; }
.contact-dropdown-inner { position: relative; padding: 20px; display: flex; flex-direction: column; gap: 14px; }

/* An explicit way out — clicking the trigger again already closed this,
   but nothing said so. */
.contact-close {
  position: absolute;
  top: 10px; right: 10px;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border: none; background: transparent;
  color: var(--cream-dim);
  font-size: 20px; line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease;
}
.contact-close:hover { color: var(--gold); }

/* Highlights whichever trigger opened the dropdown while it's open, so
   "click it again to close" reads as an available action, not a guess. */
.nav-contact-trigger.is-active { color: var(--gold); }
.nav-callout-link.nav-contact-trigger.is-active { border-bottom-color: var(--gold); }

.contact-book {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 14px 16px;
  border-radius: 12px;
  background: var(--gold);
  color: var(--ink);
  position: relative;
  transition: transform 0.2s ease, filter 0.2s ease;
}
.contact-book:hover { filter: brightness(1.08); transform: translateY(-1px); }
.contact-book-label { font-family: var(--font-display); font-weight: 700; font-size: 15px; }
.contact-book-sub { font-size: 11.5px; opacity: 0.75; line-height: 1.4; }
.contact-book-arrow { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); font-size: 16px; }
/* No Calendly URL in the sheet yet (CONTACT_CONFIG.calendlyUrl blank) —
   see applyContactConfig() in main.js — hidden rather than a dead link. */
.contact-book.is-unset { display: none; }

.contact-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--cream-dim);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.contact-divider::before, .contact-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

.contact-form { display: flex; flex-direction: column; gap: 8px; }
.contact-input {
  width: 100%;
  background: rgba(241, 236, 225, 0.06);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 10px 12px;
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 13px;
}
.contact-input::placeholder { color: var(--cream-dim); }
.contact-input:focus { outline: none; border-color: var(--gold); }

.contact-send {
  align-self: flex-start;
  background: transparent;
  border: 1px solid var(--gold);
  color: var(--gold);
  border-radius: 9px;
  padding: 8px 18px;
  font-family: var(--font-nav);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}
.contact-send:hover { background: var(--gold); color: var(--ink); }
.contact-send:disabled { opacity: 0.5; cursor: default; }

.contact-status { margin: 0; font-size: 11.5px; color: var(--cream-dim); }
.contact-status.is-success { color: var(--gold); }
.contact-status.is-error { color: #e0836a; }

.contact-email-fallback {
  font-size: 11.5px;
  color: var(--cream-dim);
  text-align: center;
  border-top: 1px solid var(--line);
  padding-top: 12px;
  transition: color 0.2s ease;
}
.contact-email-fallback:hover { color: var(--gold); }

@media (max-width: 700px) {
  .contact-dropdown { right: var(--gutter); left: var(--gutter); width: auto; }
}

/* The circle trigger — always visible, every breakpoint. Sits inline in
   .nav-right's own flex row (both are fixed now, so neither needs its
   own independent position:fixed the way the old mobile-only version did). */
.nav-menu-trigger {
  display: flex;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: rgba(13, 13, 12, 0.4);
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
  cursor: pointer;
  flex: 0 0 auto;
  transition: background 0.3s ease;
}
.nav-menu-trigger:hover { background: rgba(13, 13, 12, 0.7); }
.nav-menu-trigger-bar {
  display: block;
  width: 16px;
  height: 1px;
  background: var(--cream);
  transition: transform 0.3s ease, opacity 0.3s ease, background 0.3s ease;
}
/* Open state: bars become an X */
.site-nav.is-menu-open .nav-menu-trigger-bar:first-child { transform: translateY(3px) rotate(45deg); }
.site-nav.is-menu-open .nav-menu-trigger-bar:last-child { transform: translateY(-3px) rotate(-45deg); }

.nav-callout {
  /* The eyebrow font (--font-nav), not the header/mono voice — was
     font-mono. */
  font-family: var(--font-nav);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1.6;
  color: var(--cream); /* solid white always — no dark/dim state, anywhere on the page */
  white-space: nowrap; /* keeps each line itself from auto-wrapping; the <br> in the markup is what forces "Get in touch" onto its own line */
  text-align: right;
  margin: 0;
}
.nav-callout-link {
  /* The header/mono voice (matches "A closer look" / nav-link), distinct
     from .nav-callout's eyebrow font above it — two fonts, deliberately. */
  display: inline-block;
  font-family: var(--font-mono);
  color: var(--cream);
  border-bottom: 1px solid var(--gold);
  text-transform: none;
  letter-spacing: 0.02em;
}
.nav-callout-link:hover { color: var(--gold); }

.nav-mark { display: block; }
.nav-logo-img { height: 30px; width: auto; display: block; }

/* The dropdown panel — one treatment at every breakpoint now, anchored to
   the always-fixed trigger's own box (18px top + 40px tall + 12px gap =
   70px), never to .site-nav itself. */
.nav-menu-panel {
  position: fixed;
  top: 70px;
  right: var(--gutter);
  z-index: 101;
  width: max-content;
  max-width: calc(100vw - 2 * var(--gutter));
  padding: 12px 16px;
  background: var(--ink-raised);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.6);
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.site-nav.is-menu-open .nav-menu-panel {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* One row: Work/About grouped left, Contact centered — see
   .nav-links--compact below for index.html, which has no "Get lost" and
   so doesn't use the 3-column version (that phantom empty right column
   was leaving obvious dead space in the panel). */
.nav-links {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 18px;
}
/* index.html only: no third (Get lost) item, so a plain 2-column row —
   no equal-and-empty 1fr column reserving space nothing sits in. */
.nav-links--compact {
  grid-template-columns: auto auto;
  justify-content: center;
}
.nav-links-left { display: flex; align-items: center; gap: 18px; justify-self: start; }

/* Plain text, no native/underline decoration — just a gray line sitting
   under it, not hugging the letters like a default link underline. The
   clean eyebrow font (--font-nav), not the header/mono voice. Turns gold
   (text AND the line under it) on hover/click. Fixed cream regardless of
   --nav-text-color's current hero-fade value — the panel's own solid
   dark fill means the link never actually sits over the changing hero
   art, so it must stay legible against ITS background, not the page's. */
.nav-link {
  font-family: var(--font-nav);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--cream); /* solid white, matching the rest of the header */
  padding: 8px 2px;
  border-bottom: 1px solid rgba(168, 162, 151, 0.45); /* true gray, not a faint white */
  transition: color 0.2s ease, border-color 0.2s ease;
}
.nav-link:hover, .nav-link.is-active { color: var(--gold); border-color: var(--gold); }

/* Contact — centered in the row, same plain-text/gray-line treatment as
   Work/About (no pill), just with the plus-circle icon (matching "A
   closer look", .tile__more-plus in css/work.css) after the word. The
   icon stays gold always and rotates into an "x" while the dropdown is
   open. */
.nav-contact-btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  justify-self: center;
}
.nav-contact-plus {
  display: grid;
  place-items: center;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--gold);
  color: var(--ink);
  font-size: 13px;
  line-height: 1;
  font-weight: 700;
  transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.nav-contact-btn.is-active .nav-contact-plus { transform: rotate(45deg); }

@media (max-width: 600px) {
  :root { --nav-h: 64px; }
  .nav-logo-img { height: 24px; }
  .nav-right { top: 14px; gap: 12px; }
  .nav-menu-panel { top: 62px; width: min(78vw, 240px); }
  .nav-links { grid-template-columns: 1fr; justify-items: center; gap: 10px; }
  .nav-links-left { flex-direction: column; align-items: center; justify-self: center; }
  .nav-contact-btn, .nav-link--cta { justify-self: center; }
}

/* ==========================================================================
   SHOWCASE — single-flow model, no sticky trick, no fade overlay.

   .hero-pin    -> pinned by ScrollTrigger (js/main.js) for exactly
                   PAN_SCROLL_VH of scroll while the horizontal pan runs.
                   The instant that scroll distance is consumed, the pin
                   releases and .hero-pin becomes ordinary scrolling
                   content again — so .deck-content, right after it in
                   the DOM, is already sitting at the bottom edge of the
                   viewport and scrolls up "stuck to" the hero with no
                   fade, no gap, no glitch. Plain browser document flow.
   .desert-parallax -> shifted by mouse position ("eyes"), landing-page
                   only (see js/main.js landingActive). Overscanned
                   vertically (top/bottom negative offset below) so a
                   Y-axis push can never expose the edge underneath —
                   the previous inset:0 version had no vertical room to
                   absorb that push, which is what exposed background at
                   the top/bottom edges during the landing-page look-around.
     .desert-pan -> flex row of: hidden left margin, 3 screens (sign /
                   logos / soda machine, each exactly 1 viewport-width),
                   hidden right margin. Overall width AND each child's
                   width are set directly in PIXELS by js/main.js at
                   runtime (see sizeScene()) — not by CSS vw values.
                   ONE continuous gradient spans the full scene width
                   here (not per-segment colors), so the hidden margins
                   read as an extension of the same photo once revealed
                   by the parallax push, instead of a separate block.
       .desert-margin-left -> hidden zone; holds the easter egg, centered
                   so it's never visible at rest. Only a cursor push
                   toward the left edge reveals it (see js/main.js
                   maxParallax — capped to the margin's own width now, so
                   a full push reveals it exactly, without overshooting
                   into the void past it).
       .screen-sign / .screen-logos / .screen-soda -> the 3 screens.
                   Client logos live inside .screen-logos and move WITH
                   the single .desert-pan transform (no separate parallax
                   layer), so they can only ever be visible during
                   screen 2 and are gone by the time screen 3 arrives.
       .desert-margin-right -> hidden zone, parallax safety only.
   .desert-text -> the name block, fades out early in the pan.
   .scroll-hint -> lives on the landing view (bottom center) now, not
                   below the gallery — a "there's more below" cue for the
                   very first screen.
   ========================================================================== */
.showcase {
  position: relative;
}

.hero-pin {
  position: relative;
  /* lvh = the LARGE viewport height: the height with mobile browser
     chrome retracted, and crucially a value that does NOT change as the
     URL bar shows and hides. Plain vh is the same number on most mobile
     browsers but dvh-style units move mid-scroll, which is what left the
     scene short of the screen and exposed the ink background as a black
     bar along the bottom while scrolling. vh first as the fallback for
     anything without lvh support. */
  height: 100vh;
  height: 100lvh;
  overflow: hidden;
  background: var(--ink);
}

/* Full-bleed viewport window — NOT a letterboxed/centered "frame" (that
   approach was tried and reverted: it added black bars on narrow/tall
   screens, which broke the requirement that Panel 1 present as one
   single deterministic frame everywhere for a future video overlay to
   line up against). The panorama's HEIGHT always fills this element's
   full height exactly (see frameHeight() in main.js, now just
   window.innerHeight, no min() capping) — a narrow viewport shows a
   cropped-narrower SLICE of the panel's width, never a shrunk/letterboxed
   whole panel. Vertical overscan (±32px) is for the mouse-parallax
   look-around's safety margin only, same as originally. */
.desert-parallax {
  position: absolute;
  left: 0; right: 0;
  top: -32px;
  bottom: -32px;
}

.desert-pan {
  position: absolute;
  top: 0; left: 0;
  display: flex;
  align-items: stretch;
  height: 100%;
  /* Overall width + every child's width set in JS (pixels, not vw) — see
     sizeScene() in main.js. Children in DOM order: hidden left margin,
     3 screens (sign / logos / soda machine) at exactly 1 viewport-width
     each, hidden right margin.

     ONE continuous gradient across the full scene — margin-left/right and
     all 3 screens share this single background instead of each painting
     their own solid color. That's what makes the hidden margins read as
     "more of the same photo" once the parallax reveals them, instead of a
     visually separate block. Stops approximate where each segment starts
     given SCENE_MULTIPLIER (0.18 left + 3 screens + 0.10 right = 3.28):
     margin-left 0-5.5%, screen-sign 5.5-36%, screen-logos 36-66.5%,
     screen-soda 66.5-97%, margin-right 97-100%.

     Real panorama art (assets/hero/desert-panorama.webp) now sits in front
     of that same gradient as a second background layer. It only covers
     the 3 screens (sign/billboards/machine), not the hidden margins (the
     left one gets its own separate art — see .desert-margin-left below),
     so the gradient still shows through at the far right margin exactly
     as before — the photo layer's x-offset is set in JS (sizeScene() in
     main.js) so its left edge always lines up with where screen-sign
     begins, in lockstep with the pixel widths computed there.
     background-size "auto 100%" is a height-fit: the image is never
     stretched, only cropped left/right if a viewport's aspect ratio
     doesn't match the art's.
     ?v= cache-busts the IMAGE itself, same idea as the CSS/JS ?v= in
     index.html but separate — swapping the file at the same filename
     (as happened going from the first version of this art to this one)
     doesn't change the URL those tags bump, so a browser that already
     cached the old bytes can keep serving them indefinitely with no
     signal anything changed. Bump this value too whenever the art
     itself is replaced in place. */
  background-image:
    url('../assets/hero/desert-panorama.webp?v=20260913screen3'),
    linear-gradient(90deg,
      #241812 0%,
      #2c1c14 5.5%,
      #d97a34 20%,
      #c1622b 28%,
      #8a4a3c 36%,
      #5b3f52 51%,
      #38375a 66.5%,
      #1c2238 85%,
      #1c2238 100%
    );
  background-repeat: no-repeat, no-repeat;
  background-size: auto 100%, 100% 100%;
  background-position: 0 center, 0 0; /* image x-offset set in JS */
}

.desert-margin {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
}

/* The real hidden easter-egg art — a separate, narrower image
   (desert-panorama-side.png, 497x1537, same height as the main panorama)
   made specifically to fill this margin: at "auto 100%" (the same
   height-fit technique as the main photo layer, see .desert-pan above),
   its own natural aspect ratio renders it almost exactly
   leftMarginPx()-wide, i.e. it fills the margin's real pixel width with
   no gap and no overflow. Right-aligned so its edge (a road, matching
   the main panorama's own road line) meets screen-sign's left edge
   seamlessly once revealed. Never inside the viewport at rest — only a
   cursor push toward the left edge (or the touch drag-down reveal)
   translates the scene far enough right to bring it into view (see
   js/main.js maxParallax). */
.desert-margin-left {
  background-image: url('../assets/hero/desert-panorama-side.webp?v=20260903r');
  background-repeat: no-repeat;
  background-size: auto 100%;
  background-position: right center;
}

/* Each screen is exactly 1 viewport-width (set in JS) — purely structural,
   no background of its own (the continuous gradient lives on .desert-pan,
   see above). */
.desert-screen {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  overflow: hidden;
}

.screen-logos {
  display: flex;
  align-items: center;
  justify-content: center;
}

.screen-soda {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ==========================================================================
   PANORAMA OVERLAYS — not built yet, just supported. A future billboard/
   sign graphic gets added as a child of a .desert-screen (sign/logos/soda)
   with this class, positioned via inline left/top/width percentages set
   from the OVERLAYS config array in main.js. Percentages resolve against
   the screen's own box, which main.js now sizes to exactly one panel's
   real rendered dimensions — so an overlay placed this way stays locked
   to its spot in the art and pans/scales with it at any screen size,
   with no per-overlay math needed beyond the initial percentages.
   ========================================================================== */
.panorama-overlay {
  position: absolute;
  z-index: 2;
}

/* ==========================================================================
   SUPERBLOOM FLOWERS — a .panorama-overlay variant rendered as an <img>
   (see renderOverlays() in main.js: an OVERLAYS entry with imgSrc gets an
   <img> instead of an empty div). Starts invisible/small and "pops up and
   settles" into place once main.js decides the flower has reached the
   viewport's horizontal center during the pan — see panTimeForCenterCross
   and flowerBloomState in main.js. Purely a CSS keyframe once triggered
   (the .is-bloomed class), so it costs nothing per scroll frame after
   that one moment, and staying in the DOM with the class attached IS the
   latch (never reverses on scroll-up, never replays).

   --rot/--flip are per-flower custom properties (set inline by
   renderOverlays from the OVERLAYS config) so the same handful of source
   images can be reused all over the scene without reading as copies —
   the keyframe's own scale numbers stay literal (a custom property can't
   be interpolated by name inside a keyframe step), rot/flip just ride
   along unchanged on every step. transform-origin sits low in the frame
   so growth reads as "pushing up out of the ground," not scaling from
   the middle of empty air above the art. */
.flower-overlay {
  height: auto;
  opacity: 0;
  transform: rotate(var(--rot, 0deg)) scaleX(var(--flip, 1)) scale(0.35);
  transform-origin: 50% 92%;
  pointer-events: none;
  will-change: transform, opacity;
}

.flower-overlay.is-bloomed {
  animation: flowerBloom 700ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

@keyframes flowerBloom {
  0%   { opacity: 0; transform: rotate(var(--rot, 0deg)) scaleX(var(--flip, 1)) scale(0.35); }
  55%  { opacity: 1; transform: rotate(var(--rot, 0deg)) scaleX(var(--flip, 1)) scale(1.08); }
  100% { opacity: 1; transform: rotate(var(--rot, 0deg)) scaleX(var(--flip, 1)) scale(1); }
}

/* The "yay" sparkle — timed to land right as the bloom overshoots and
   starts settling (see animation-delay below). No image asset: gradients
   are cheap and never need to be swapped/reused-disguised the way the
   flower art does. Fires once, forwards-fills to fully transparent, then
   never runs again — same latch as the bloom itself.

   Sized in vw, NOT a percentage of the flower's own box — a percentage
   made the sparkle scale down WITH the flower, so on the small
   background flowers (some as narrow as ~1.5% of the screen) it shrank
   to a few px and was invisible. A fixed-ish viewport size means every
   flower gets an equally readable twinkle regardless of how small the
   flower itself is. Two layers: ::before is a tight bright core (the
   "flash"), ::after is a wider soft gold glow behind it — together they
   read as a distinct sparkle instead of one soft blur. */
.flower-overlay.is-bloomed::before,
.flower-overlay.is-bloomed::after {
  content: '';
  position: absolute;
  top: 8%;
  left: 50%;
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
}

.flower-overlay.is-bloomed::after {
  width: 3.2vw;
  height: 3.2vw;
  background: radial-gradient(circle, rgba(255, 214, 120, 0.95) 0%, rgba(255, 214, 120, 0.4) 45%, rgba(255, 214, 120, 0) 72%);
  transform: translate(-50%, -50%) scale(0.15);
  animation: flowerSparkleGlow 650ms ease-out forwards;
  animation-delay: 300ms;
}

.flower-overlay.is-bloomed::before {
  width: 1vw;
  height: 1vw;
  background: radial-gradient(circle, #fff 0%, rgba(255, 255, 255, 0.9) 35%, rgba(255, 255, 255, 0) 75%);
  box-shadow: 0 0 1.2vw 0.2vw rgba(255, 244, 214, 0.8);
  transform: translate(-50%, -50%) scale(0.2);
  animation: flowerSparkleCore 500ms ease-out forwards;
  animation-delay: 320ms;
  z-index: 1;
}

@keyframes flowerSparkleGlow {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.15); }
  40%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.6); }
}

@keyframes flowerSparkleCore {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.2); }
  25%  { opacity: 1; transform: translate(-50%, -50%) scale(1.3); }
  60%  { opacity: 1; transform: translate(-50%, -50%) scale(0.9); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
}

/* ==========================================================================
   SUN FLARE SWEEP — a full-viewport cinematic flood of warm light, like
   the sun suddenly catching the lens through a car window as you pass.
   Replaced an earlier version that was a small cursor-driven glare
   pinned to one spot in the art — wrong brief entirely: this one covers
   the WHOLE frame, ignores the cursor completely, and its every visual
   property is DRIVEN directly by panTl's own scroll-scrubbed progress
   (see SUN FLARE SWEEP in main.js) rather than a one-shot latch —
   scrolling down plays it forward, up plays it backward, stopping
   freezes it exactly, matching a plain GSAP-on-a-scrubbed-timeline
   animation (the same mechanism .dusk-overlay already uses here) with
   no bespoke scroll-speed math of its own.

   Lives as a direct child of .hero-pin (a true pin-level sibling, same
   trick as .deck-content — see its own comment in index.html for why:
   .desert-pan/.desert-parallax both carry live transforms, and a
   transform creates a new stacking context that traps z-index inside
   it, so anything meant to sit above the WHOLE panned scene has to live
   outside that subtree, not deeper in the DOM hoping z-index reaches
   far enough). inset:0 covers exactly the pin's own viewport-sized box.

   FIVE layers on one element, colors sampled directly from the actual
   sun/sky pixels in desert-panorama.webp (not guessed) so this reads as
   THIS sun's light, not generic white glow:
     rgb(255,236,114) — the sun's own core color (sampled at its center)
     rgb(255,244,154) — its halo, one ring out
     rgb(250,176,83)  — the golden-orange right at the horizon rim
     rgb(233,153,79)  — the deeper amber further from the disc
     rgb(224,198,222) — the dusk sky's cool lavender-pink, well away from
                        the sun — used only as a faint fringe (see layer
                        4), the way a real lens flare shows a whisper of
                        a complementary color at a ghost's edge. That
                        touch of cool-against-warm is most of what reads
                        as "dimension" rather than a flat colored blob.
   Layer 1 is a small, bright, TIGHT hot core — separate from the softer
   layer-2 halo around it — because a single soft blob of one size reads
   flat; a real flare has a distinct bright center inside a bigger soft
   glow. Layer 3 is the beam streak. Layer 4 is the cool fringe. Layer 5
   is the broad bottom wash (overall brightness lift / "overexposure").
   mix-blend-mode:screen so all of this brightens what's under it instead
   of sitting on top like a flat tint — closer to how a real light flood
   behaves. Position/scale/opacity/rotate are driven by real GSAP tweens
   on panTl itself (see SUN FLARE SWEEP TIMELINE in main.js), not a CSS
   custom property — the rise/hold/fall keyframes there also carry a few
   degrees of rotation, settling to 0 right at peak brightness, for the
   "angles move a little as it fades in/out" twist. */
.sun-flare-sweep {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 4;
  mix-blend-mode: screen;
  opacity: 0;
  background:
    radial-gradient(ellipse 22% 18% at 68% 38%, rgba(255, 236, 114, 0.95) 0%, rgba(255, 236, 114, 0) 72%),
    radial-gradient(ellipse 70% 55% at 68% 38%, rgba(255, 244, 154, 0.85) 0%, rgba(250, 176, 83, 0.35) 40%, rgba(233, 153, 79, 0) 68%),
    linear-gradient(115deg, rgba(233, 153, 79, 0) 30%, rgba(250, 176, 83, 0.55) 47%, rgba(255, 236, 114, 0.78) 52%, rgba(250, 176, 83, 0.55) 57%, rgba(233, 153, 79, 0) 74%),
    linear-gradient(115deg, rgba(224, 198, 222, 0) 26%, rgba(224, 198, 222, 0.18) 40%, rgba(224, 198, 222, 0) 50%, rgba(224, 198, 222, 0.14) 60%, rgba(224, 198, 222, 0) 76%),
    linear-gradient(to bottom, rgba(233, 153, 79, 0.28), rgba(233, 153, 79, 0.28));
}

/* ==========================================================================
   SLOW-DOWN SUNSET — source x=4400..5200, panel 2 only.

   Stars only: no color, haze, cloud, or image overlay. The layer sits above
   the preserved sun glare and is controlled by the scrubbed hero timeline—
   no CSS loops or timers. */

.slow-down-sparkle-layer {
  position: absolute;
  inset: 0;
  z-index: 5;
  opacity: 0;
  pointer-events: none;
}

.slow-down-sparkle {
  position: absolute;
  left: 0;
  top: 0;
  translate: -50% -50%;
  width: clamp(17px, 1.65vw, 31px);
  aspect-ratio: 1;
  opacity: var(--sparkle-opacity, 0.92);
  transform-origin: center;
  background: radial-gradient(circle,
    rgba(255, 255, 255, 1) 0 9%,
    rgba(255, 237, 174, 0.94) 17%,
    rgba(255, 166, 185, 0.46) 34%,
    rgba(191, 166, 255, 0.13) 54%,
    transparent 72%);
  mix-blend-mode: screen;
  filter: drop-shadow(0 0 6px rgba(255, 228, 164, 0.94));
  animation: slow-down-twinkle var(--twinkle-duration, 2.8s) ease-in-out var(--twinkle-delay, 0s) infinite alternate;
  will-change: transform, opacity;
}

.slow-down-sparkle::before,
.slow-down-sparkle::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  background: linear-gradient(to bottom, transparent, rgba(255, 250, 219, 0.96) 42%, #fff 50%, rgba(255, 250, 219, 0.96) 58%, transparent);
  transform: translate(-50%, -50%);
  border-radius: 50%;
}

.slow-down-sparkle::before {
  width: 2px;
  height: 185%;
  animation: slow-down-ray-v var(--twinkle-duration, 2.8s) ease-in-out var(--twinkle-delay, 0s) infinite alternate;
}
.slow-down-sparkle::after {
  width: 155%;
  height: 2px;
  animation: slow-down-ray-h var(--twinkle-duration, 2.8s) ease-in-out var(--twinkle-delay, 0s) infinite alternate;
}

@keyframes slow-down-twinkle {
  0%, 12% { opacity: .3; }
  50%, 100% { opacity: var(--sparkle-opacity, .96); }
}

@keyframes slow-down-ray-v {
  from { transform: translate(-50%, -50%) scaleY(.42); }
  to { transform: translate(-50%, -50%) scaleY(1); }
}

@keyframes slow-down-ray-h {
  from { transform: translate(-50%, -50%) scaleX(.46); }
  to { transform: translate(-50%, -50%) scaleX(1); }
}

.slow-down-sparkle--1 { --sparkle-opacity: 0.88; --twinkle-duration: 1.45s; --twinkle-delay: -.8s; }
.slow-down-sparkle--2 { width: clamp(20px, 1.9vw, 36px); --sparkle-opacity: 1; --twinkle-duration: 1.8s; --twinkle-delay: -1.1s; }
.slow-down-sparkle--3 { --sparkle-opacity: 0.96; --twinkle-duration: 1.2s; --twinkle-delay: -.4s; }
.slow-down-sparkle--4 { width: clamp(18px, 1.7vw, 32px); --sparkle-opacity: 0.92; --twinkle-duration: 1.95s; --twinkle-delay: -.9s; }
.slow-down-sparkle--5 { width: clamp(14px, 1.35vw, 25px); --sparkle-opacity: 0.84; --twinkle-duration: 1.55s; --twinkle-delay: -.6s; }
.slow-down-sparkle--6,
.slow-down-sparkle--7,
.slow-down-sparkle--8,
.slow-down-sparkle--9 {
  width: clamp(7px, 0.62vw, 12px);
  --sparkle-opacity: 0.82;
  filter: drop-shadow(0 0 4px rgba(255, 232, 183, 0.86));
}

.slow-down-sparkle--6  { --twinkle-duration: 1.1s; --twinkle-delay: -.3s; }
.slow-down-sparkle--7  { --twinkle-duration: 1.75s; --twinkle-delay: -1.4s; }
.slow-down-sparkle--8  { --twinkle-duration: 1.3s; --twinkle-delay: -.7s; }
.slow-down-sparkle--9  { --twinkle-duration: 1.9s; --twinkle-delay: -1.6s; }
.slow-down-sparkle--10 { width: clamp(6px, .52vw, 10px); --sparkle-opacity: .86; --twinkle-duration: 1.25s; --twinkle-delay: -.7s; }
.slow-down-sparkle--11 { width: clamp(8px, .68vw, 13px); --sparkle-opacity: .84; --twinkle-duration: 1.85s; --twinkle-delay: -.6s; }
.slow-down-sparkle--12 { width: clamp(5px, .46vw, 9px); --sparkle-opacity: .9; --twinkle-duration: 1.05s; --twinkle-delay: -.2s; }
.slow-down-sparkle--13 { width: clamp(7px, .58vw, 11px); --sparkle-opacity: .82; --twinkle-duration: 1.6s; --twinkle-delay: -1.2s; }
.slow-down-sparkle--14 { width: clamp(5px, .42vw, 8px); --sparkle-opacity: .92; --twinkle-duration: 1.35s; --twinkle-delay: -.9s; }
.slow-down-sparkle--15 { width: clamp(6px, .5vw, 10px); --sparkle-opacity: .9; --twinkle-duration: 1.15s; --twinkle-delay: -.5s; }
.slow-down-sparkle--16 { width: clamp(7px, .6vw, 11px); --sparkle-opacity: .86; --twinkle-duration: 1.7s; --twinkle-delay: -1.3s; }
.slow-down-sparkle--17 { width: clamp(5px, .44vw, 9px); --sparkle-opacity: .94; --twinkle-duration: 1.3s; --twinkle-delay: -.8s; }
.slow-down-sparkle--18 { width: clamp(8px, .64vw, 12px); --sparkle-opacity: .88; --twinkle-duration: 1.9s; --twinkle-delay: -.2s; }
.slow-down-sparkle--19 { width: clamp(5px, .4vw, 8px); --sparkle-opacity: .95; --twinkle-duration: 1.05s; --twinkle-delay: -.7s; }
.slow-down-sparkle--20 { width: clamp(7px, .56vw, 11px); --sparkle-opacity: .86; --twinkle-duration: 1.5s; --twinkle-delay: -1.1s; }
.slow-down-sparkle--21 { width: clamp(6px, .48vw, 9px); --sparkle-opacity: .92; --twinkle-duration: 1.22s; --twinkle-delay: -.4s; }
.slow-down-sparkle--22 { width: clamp(5px, .42vw, 8px); --sparkle-opacity: .9; --twinkle-duration: 1.65s; --twinkle-delay: -1.4s; }

.slow-down-marker {
  position: absolute;
  top: 50%;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 700px) {
  .slow-down-sparkle--1,
  .slow-down-sparkle--5,
  .slow-down-sparkle--6,
  .slow-down-sparkle--9,
  .slow-down-sparkle--10,
  .slow-down-sparkle--12,
  .slow-down-sparkle--14,
  .slow-down-sparkle--16,
  .slow-down-sparkle--18,
  .slow-down-sparkle--20,
  .slow-down-sparkle--22 { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .slow-down-sparkle,
  .slow-down-sparkle::before,
  .slow-down-sparkle::after {
    animation: none !important;
  }
  .slow-down-sparkle {
    will-change: opacity;
  }
}

/* ==========================================================================
   BUTTERFLY — two nested elements now (a continuous steering-based
   rewrite replaced the original waypoint-tween version — see BUTTERFLY
   in main.js for the flight model). .butterfly (outer) carries position
   AND depth-scale together, set directly as one `transform` string every
   frame by the ticker loop — translate+scale+rotate compose fine in a
   single transform, so one element can own all three at once here. <img>
   (inner) carries ONLY the left/right flip, kept separate so scaling the
   parent for depth never fights with mirroring the child. Never
   left/top, so none of this forces a layout reflow. */
.butterfly {
  position: absolute;
  top: 0;
  left: 0;
  width: 3.5vw;
  height: 3.5vw;
  pointer-events: none;
  z-index: 2;
  will-change: transform;
}

.butterfly img {
  width: 100%;
  height: 100%;
  display: block;
  transform: scaleX(1);
}

/* Keep the butterfly at the intended artwork scale on phones. The earlier
   mobile declaration appeared before the base rule and was therefore being
   overwritten back to 3.5vw (about 14px on an iPhone). */
@media (max-width: 700px) {
  .butterfly { width: 62px; height: 62px; }
}

/* ==========================================================================
   SCENE-PINNED TEXT — the services block lives INSIDE a panning screen
   (not .desert-text, which stays viewport-relative within the frame), so
   it travels with the background as it pans, the same way the easter egg
   does. Opacity/position are driven by main.js — see "SCENE-PINNED TEXT"
   there.
   ========================================================================== */
/* Services list, near the top of the machine screen once it's risen for
   the dusk transition (see main.js SERVICES_RISE_Y). Width is generous
   ("full width" per spec) specifically so every answer line fits on ONE
   line at the bigger font size below — nowrap on the item text is the
   actual guarantee, this width just keeps it from overflowing the frame. */
.scene-services {
  position: absolute;
  /* left is set in JS (see positionServices() in main.js) — panel 3 is
     often wider than the viewport on narrow screens, so a flat % here
     could land outside the slice that's actually ever visible. */
  bottom: 16%;
  z-index: 2;
  width: min(90vw, 960px);
  /* Only the EXIT's brief counter-scale (main.js — canceling out
     desert-pan's own zoom while the block flies off left) touches the
     container's own scale; the entrance "pop" is per-line (see
     .scene-services-title/.scene-services-item below). Left-anchored so
     either one grows/shrinks from a fixed edge, matching the per-line
     origin below. */
  transform-origin: left bottom;
}
/* transform-origin: the staggered rise scales each of these lines up as
   it travels (see the services block in main.js). Default centre-origin
   made them grow outward from the middle, so the left edge crept in and
   out as they arrived — anchoring at the left keeps the block's edge
   dead still and lets the type grow rightward into open space. */
.scene-services-title,
.scene-services-item {
  transform-origin: left center;
}

/* Same treatment as "Now Serving" (.deck-eyebrow) — nav font, uppercase,
   wide tracking, gold. The two are the same kind of thing (a small label
   introducing the block under it), so they read as one system rather
   than two different label styles a screen apart. Size steps up slightly
   from the deck's 12px because this one sits over photography at a
   distance, not on a flat dark panel. */
.scene-services-title {
  display: block;
  font-family: var(--font-nav);
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--gold);
  margin-bottom: 20px;
}
.scene-services-list {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.scene-services-item {
  display: block;
  line-height: 1.4;
  font-size: 19px;
  font-weight: 400;
  color: var(--cream);
  white-space: nowrap; /* the actual one-line guarantee, not just width */
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The question line is gold, the answer under it stays cream — so each
   pair reads as prompt-then-response at a glance, and the gold ties back
   to the title above and the accent used everywhere else. */
.scene-services-item strong {
  display: block;
  /* Same font as the hero title (--font-display) — was font-nav, and a
     little bigger (was 19px, matching the answer line under it). */
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 700;
  font-style: italic;
  letter-spacing: 0.02em;
  color: var(--gold);
  margin-bottom: 4px;
  white-space: nowrap;
}

@media (max-width: 700px) {
  /* Narrower than the screen on purpose: the eye sits at the right edge
     during exactly this stretch of the scroll, and the answer lines were
     running under it. */
  .scene-services { width: min(82vw, 410px); }
  /* 15px here was sized for the old body-style title; as an uppercase
     tracked label it wants to sit UNDER the desktop size, not over it. */
  .scene-services-title { font-size: 13px; margin-bottom: 16px; }
  .scene-services-list { gap: 14px; }
  .scene-services-item { font-size: 18px; line-height: 1.45; white-space: normal; overflow: visible; text-overflow: clip; }
  .scene-services-item strong { font-size: 19px; white-space: normal; }
}

.desert-text {
  position: absolute;
  left: var(--gutter);
  top: calc(var(--nav-h) + 40px);
  max-width: 520px;
  z-index: 2;
}

/* Pure layout wrapper — no opacity/transform of its own; .desert-name is
   animated directly in main.js. */
.desert-name {
  display: block;
}

/* hero-eyebrow / hero-title are reused inside .desert-text */
.hero-eyebrow {
  font-family: var(--font-nav);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--cream);
  margin: 0 0 16px;
}

.hero-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(30px, 4.6vw, 56px);
  line-height: 1;
  margin: 0 0 16px;
  letter-spacing: -0.01em;
  color: var(--gold);
}

/* Identity list — one item per line
   (each separated by a <br> in the markup, not wrapped text) stacking
   downward. No max-height, so a long list still runs past the fold on
   its own rather than being capped. */
.desert-identity-list {
  /* Same font as the header nav (--font-mono) — was font-body. */
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.02em;
  line-height: 1.9;
  color: var(--cream);
  margin: 16px 0 0;
}

/* Lets .desert-name be tweened with a drift, for the
   "blown away by desert wind" feel (see main.js) — plain opacity fades
   look flat; this gives a soft dissolve/drift instead. No filter here —
   it used to include an animated blur too, dropped for performance (see
   the SWEEP comment in main.js). The "open for business" line now lives
   in the nav (.nav-callout, see NAV section) — it's a persistent header
   element, not a scroll-tied hero overlay anymore, so it no longer needs
   to be in this list. */
.desert-name, .scene-services {
  will-change: opacity, transform;
}

/* The timed world flight, chassis corrections, lens and spray have separate transforms. */
.aura-travel { position:absolute; left:0; top:0; z-index:3; pointer-events:none; visibility:hidden; contain:layout style; perspective:950px; }
.aura-shadow { position:absolute; left:0; top:0; z-index:2; pointer-events:none; visibility:hidden; background:radial-gradient(ellipse closest-side,rgba(48,37,31,.58) 0%,rgba(51,38,30,.51) 46%,rgba(57,41,31,.32) 68%,rgba(64,45,33,.10) 84%,transparent 100%); }
.aura-pod { position:relative; transform-origin:53% 52%; transform-style:preserve-3d; }
.aura-chassis { position:relative; transform-style:preserve-3d; }
/* Static atmospheric grading preserves the approved artwork and scale. */
.aura-image, .aura-undercarriage, .aura-lens { filter:sepia(.07) saturate(.92) contrast(.94) brightness(1.025); }
.aura-video { position:absolute; left:-14.733%; top:-23.649%; width:129.65%; height:auto; display:none; pointer-events:none; filter:sepia(.07) saturate(.92) contrast(.94) brightness(1.025); }
.aura-travel.has-video .aura-video { display:block; }
.aura-travel.has-video .aura-image, .aura-travel.has-video .aura-lens, .aura-travel.has-video .aura-undercarriage { visibility:hidden; }
.aura-image { display:block; width:100%; height:auto; }
.aura-undercarriage { position:absolute; inset:0; width:100%; height:100%; transform-origin:64% 83%; }
.aura-lens { position:absolute; left:23.2044%; top:64.1892%; width:12.8913%; height:21.9595%; transform-origin:50% 50%; }
.aura-irrigation { position:absolute; inset:0; opacity:0; transform:translateZ(5px); }
.aura-spray { position:absolute; left:55%; top:90.4%; width:18%; height:var(--aura-stream-length); transform:translateX(-50%) skewX(-7deg); transform-origin:50% 0; overflow:hidden; clip-path:polygon(38% 0,62% 0,100% 100%,0 100%); mask-image:linear-gradient(#000 72%,transparent); }
.aura-spray--2 { left:70.5%; top:86.5%; }
.aura-spray i { position:absolute; top:-100%; left:var(--x); width:1.6px; height:200%; opacity:var(--alpha); background:repeating-linear-gradient(to bottom,rgba(255,249,230,.14) 0 6px,rgba(255,249,230,.94) 7px 19px,transparent 24px 38px); animation:aura-water var(--speed) linear infinite; animation-delay:var(--delay); animation-play-state:paused; }
.aura-spray::after { content:''; position:absolute; inset:0; background:linear-gradient(rgba(255,245,222,.14),transparent 45%),radial-gradient(ellipse at 50% 90%,rgba(245,234,212,.2),transparent 65%); }
.aura-travel.is-active .aura-spray i { animation-play-state:running; }
.aura-travel.is-active, .aura-travel.is-active .aura-pod { will-change:transform; }
.aura-sign-occlusion { position:absolute; inset:0; z-index:4; pointer-events:none; visibility:hidden; }
.aura-sign-image { position:absolute; top:0; left:0; display:block; height:auto; }
.aura-wip-image {
  /* Reuse the scene's single sign-and-shadow PNG for AURA occlusion, but
     clip away its ground shadow so only the metal sign paints above AURA. */
  clip-path: inset(0 0 20.64% 0);
}
/* z-index:115, not the old 70 — .aura-sound is already a top-level
   position:fixed sibling of <body> (see the markup comment in index.html:
   moved out of .hero-pin specifically so it would NOT be trapped in a
   transformed ancestor's stacking context), so this was never a stacking-
   context trap, just a plain number too low. .site-nav is z-index:100 and
   goes solid opaque (.is-scrolled) once scrolled, which it always is by
   the time .aura-sound reaches its .is-compact position (top:14-18px —
   inside the nav's own height band). 115 clears .site-nav (100),
   .nav-menu-panel (101) and .contact-dropdown (105)/.find-toast (110),
   while staying below the true modal-scale overlays in this codebase
   (freewall's confirm panel and work.css's lightbox, both z-index:200). */
.aura-sound { position:fixed; top:25vh!important; right:auto!important; bottom:auto!important; left:calc(100% - clamp(24px,5vw,72px) - 178px)!important; width:178px; max-width:calc(100vw - 36px); box-sizing:border-box; z-index:115; display:flex; align-items:center; gap:7px; min-height:48px; padding:6px 10px 6px 6px; border:1px solid rgba(255,242,211,.58); border-radius:28px; background:rgba(32,27,23,.76); box-shadow:0 0 0 1px rgba(239,186,83,.06),0 0 18px rgba(239,186,83,.12),0 10px 30px rgba(24,18,14,.18); color:var(--cream); font-family:var(--font-nav); text-align:left; cursor:pointer; -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px); transition:top .6s cubic-bezier(.22,.7,.25,1),left .6s cubic-bezier(.22,.7,.25,1),width .55s ease,padding .55s ease,min-height .55s ease,border-radius .55s ease,background .4s ease,box-shadow .4s ease,color .3s ease; }
.aura-sound-icon { display:grid; place-items:center; flex:0 0 34px; width:34px; height:34px; border-radius:50%; background:rgba(239,186,83,.1); color:var(--cream); box-shadow:inset 0 0 0 1px rgba(255,226,161,.32),0 0 11px rgba(246,186,2,.18); }
.aura-sound-icon svg { width:21px; height:21px; fill:currentColor; stroke:currentColor; stroke-width:1.55; stroke-linecap:round; fill-rule:evenodd; }
.aura-sound-icon .sound-wave { fill:none; transition:opacity .25s ease; }
.aura-sound-icon .sound-off-line { fill:none; opacity:0; transition:opacity .25s ease; }
.aura-sound-copy { display:flex; flex-direction:column; gap:1px; white-space:nowrap; overflow:hidden; max-width:164px; opacity:1; transition:max-width .5s ease,opacity .28s ease; }
.aura-sound-title { font-size:12px; font-weight:600; letter-spacing:.065em; text-transform:uppercase; }
.aura-sound-detail { font:9.5px/1.2 var(--font-body); letter-spacing:.015em; color:rgba(255,248,230,.72); }
.aura-sound[aria-pressed="false"]:not(.is-compact) { animation:cta-pulse-glow 5s ease-in-out infinite; }
.aura-sound[aria-pressed="false"] .aura-sound-icon { animation:sound-icon-pulse 5s ease-in-out infinite; }
.aura-sound[aria-pressed="true"] { border-color:var(--gold); color:var(--gold); box-shadow:0 0 0 1px rgba(239,186,83,.14),0 0 24px rgba(239,186,83,.2),0 10px 30px rgba(24,18,14,.2); }
.aura-sound[aria-pressed="true"] .aura-sound-icon { animation:none; background:var(--gold); color:var(--ink); box-shadow:inset 0 0 0 1px rgba(255,240,185,.72),0 0 14px rgba(246,186,2,.65),0 0 26px rgba(246,186,2,.28); }
.aura-sound[aria-pressed="false"] .sound-wave { opacity:.18; }
.aura-sound[aria-pressed="false"] .sound-off-line { opacity:.9; }
.aura-sound.is-compact { top:18px!important; right:auto!important; left:18px!important; width:46px; gap:0; min-height:46px; padding:5px; border-radius:50%; background:rgba(13,13,12,.92); border-color:rgba(246,186,2,.8); box-shadow:0 0 0 1px rgba(246,186,2,.4),0 0 14px rgba(246,186,2,.32),0 4px 16px rgba(0,0,0,.55); animation:none; }
.aura-sound.is-compact .aura-sound-icon { flex-basis:34px; }
.aura-sound.is-compact .aura-sound-copy { max-width:0; opacity:0; }
.aura-sound:focus-visible { outline:2px solid var(--gold); outline-offset:4px; }
@media (max-width:700px) {
  .aura-sound { top:37vh!important; right:auto!important; left:calc(100% - 14px - 178px)!important; max-width:calc(100vw - 28px); min-height:48px; padding:6px 10px 6px 6px; }
  .aura-sound.is-compact { top:14px!important; right:auto!important; left:14px!important; width:42px; min-height:42px; padding:4px; }
}
@media (prefers-reduced-motion:reduce) { .aura-sound, .aura-sound-copy { transition:none; } }
@keyframes sound-icon-pulse {
  0%,85%,100% { background:rgba(239,186,83,.1); color:var(--cream); box-shadow:inset 0 0 0 1px rgba(255,226,161,.32),0 0 11px rgba(246,186,2,.18); }
  92% { background:#ffd44f; color:var(--ink); box-shadow:inset 0 0 0 1px rgba(255,250,220,.9),0 0 17px rgba(255,205,50,.92),0 0 32px rgba(246,186,2,.48); }
}
@keyframes aura-water { to { transform:translateY(50%); } }
@media (prefers-reduced-motion:reduce) { .aura-travel, .aura-shadow { display:none; } }

/* ========================================================================== 
   AMBIENT LIFE — exceptionally sparse, scene-locked environmental motion.
   These layers are decorative and live inside a panorama panel, so every
   percentage below is tied to the source composition rather than viewport.
   ========================================================================== */
.ambient-life {
  position: absolute;
  pointer-events: none;
  user-select: none;
  /* All authored panorama overlays begin at z-index:1 or 2. Keeping birds
     at 0 lets signs, flowers, dusk and machines naturally pass in front. */
  z-index: 0;
}

.ambient-bird {
  left: var(--bird-left, 10%);
  top: var(--bird-top, 22%);
  width: clamp(14px, 1.25%, 23px);
  height: auto;
  opacity: 0;
}
.ambient-bird--2 {
  width: clamp(9px, .82%, 16px);
}
.ambient-bird.is-crossing {
  animation: ambient-bird-pass var(--bird-duration, 21s) linear both;
  will-change: transform, opacity;
}
@keyframes ambient-bird-pass {
  0%   { opacity:0; transform:translate3d(var(--bird-start-x),var(--bird-start-y),0) scaleX(var(--bird-facing)) scale(.9); }
  10%  { opacity:.16; }
  19%  { opacity:var(--bird-opacity,.66); }
  50%  { opacity:var(--bird-opacity,.66); transform:translate3d(var(--bird-mid-x),var(--bird-mid-y),0) scaleX(var(--bird-facing)) scale(.96); }
  81%  { opacity:var(--bird-opacity,.66); }
  92%  { opacity:.12; }
  100% { opacity:0; transform:translate3d(var(--bird-end-x),var(--bird-end-y),0) scaleX(var(--bird-facing)) scale(.91); }
}

@media (max-width: 768px) {
  .ambient-bird { width:11px; }
  .ambient-bird--2 { display:none; }
}

@media (prefers-reduced-motion: reduce) {
  .ambient-life { display:none !important; }
}

/* ==========================================================================
   DUSK OVERLAY — the pan-end "sun goes down" transition. Lives inside
   .screen-soda now, not as a .desert-parallax sibling — see the markup
   comment in index.html for why (a stacking-context trap was putting
   .scene-services behind this despite its higher z-index). inset:0 here
   covers .screen-soda's own box, which is always sized to at least the
   viewport (see FRAME GEOMETRY), so it still reads as full-frame. Its own
   opacity (0 -> 1) is the only thing scroll-driven (see main.js) — the
   gradient SHAPE below is static and purely vertical (top to bottom, no
   left-right variation): fully dark at the top, straight linear fade down
   to fully transparent by 75% of the way down, then flat/clear for the
   remaining bottom quarter (the bright soda machine stays bright, no
   darkening reaches it at all). Explicitly a NEUTRAL/cool dark, no warm
   sunset tint — reads as after the sun is already down, not the sunset
   itself. z-index sits below .scene-services (2), above the plain
   panorama (z-index:auto) — both siblings within .screen-soda now, so
   normal z-index comparison applies directly. */
/* The panel clips its children by default (.desert-screen sets
   overflow:hidden), which kept the night wash trapped inside panel 3's
   own box. On a window wider than one panel that box doesn't reach the
   screen edges, so the overlay ended in a hard vertical line with panel
   2 still in bright daylight beside it. Unclipping this one screen lets
   the wash below span the viewport at any width. Its only other child is
   .scene-services, which is positioned within the panel and so doesn't
   escape. */
.screen-soda { overflow: visible; }

/* ==========================================================================
   MACHINE SCREEN — see the markup comment on .machine-hotspot in
   index.html. Percentages are against #screen-soda's own box (always
   exactly one frameWidth x frameHeight slice of the panorama — see FRAME
   GEOMETRY in main.js), measured directly off the source art, so these
   never need JS/viewport math the way .scene-services' left does.
   z-index 3 — above .dusk-overlay (1) and level with/above
   .scene-services (2), so the dusk transition never dims it.
   ========================================================================== */
.machine-hotspot {
  position: absolute;
  left: 52.8%;
  top: 38.9323%;
  width: 9.4545%;
  height: 35.8073%;
  z-index: 3;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Inert until playMachineIdle() (main.js) marks it active — before
     that beat, this is meant to read as scenery, not a button waiting to
     be found early. */
  pointer-events: none;
}
.machine-hotspot.is-active { pointer-events: auto; }

/* The ONLY thing added to the baked-bright artwork: a slow, restrained
   brightness "breathing" — main.js loops this opacity a few percent up
   and down once the machine goes active. Screen-blend so it reads as the
   screen's own light shifting, not a tinted layer sitting on top of it. */
.machine-glow {
  position: absolute;
  inset: -10%;
  border-radius: 24% / 16%;
  background: radial-gradient(ellipse at center, rgba(255, 236, 200, 0.9), rgba(255, 236, 200, 0) 72%);
  mix-blend-mode: screen;
  opacity: 0;
  pointer-events: none;
}

.machine-hotspot:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
  border-radius: 24% / 16%;
}

@media (prefers-reduced-motion: reduce) {
  .machine-glow { display: none; }
}

/* ==========================================================================
   MACHINE SCREEN SURFACE — see the markup comment in index.html for why
   this is a plain full-viewport glow now, not a rectangle that grows out
   of the screen. Sits at .hero-pin level (z-index 2 — above the panned/
   zoomed scene, below .deck-content's 3), always inset:0, no clip-path.

   THIRD PASS: the gray/graphite version read as flat and cold — not the
   machine. Back to warm, gold-forward tones (the brief specifically:
   "gold tones, not only gray"), pulling from the actual scene's own
   palette rather than either extreme — warm amber/gold low in the frame
   (the machine's own light, the desert's sunset colors) with a cool
   blue-violet hint up high (the dusk sky just above it), like the
   panel's own colors softened and lit from behind — "frosted glass over
   panel 3," not a spotlight and not a slab of gray. .machine-screen-
   radiance adds a slow diagonal sheen on top for the "slightly magical"
   build. Once fully arrived it stays in the exact state established by
   the scrubbed timeline; a separate looping "live" motion was removed
   because it fought that timeline at the deck's forward/reverse edges. */
.machine-screen-surface {
  position: absolute;
  /* Slight overscan (not a flush 0) so the continuous slow drift
     playMachineScreenLive() applies (main.js) has room to scale/shift
     without ever exposing an edge. */
  inset: -4%;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  background:
    linear-gradient(120deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 28%),
    radial-gradient(ellipse 85% 70% at 50% 62%, rgba(255, 197, 110, 0.32), rgba(255, 197, 110, 0) 65%),
    radial-gradient(ellipse 90% 75% at 12% 4%, rgba(122, 132, 202, 0.16), rgba(122, 132, 202, 0) 60%),
    linear-gradient(160deg, #4a3218 0%, #2c1f10 48%, #19130b 100%);
}

/* A slow diagonal glass sheen, not colored spots — screen-blended so it
   only ever adds light. Fades in later and slower than the base surface
   so the color/light visibly deepens near the end of the approach rather
   than the whole thing landing as one flat wash. */
.machine-screen-radiance {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: linear-gradient(100deg,
    rgba(255, 255, 255, 0) 32%,
    rgba(255, 214, 158, 0.16) 50%,
    rgba(255, 255, 255, 0) 68%
  );
  mix-blend-mode: screen;
}

/* Faint pixel/LED structure — the brief's "barely noticeable, invisible
   until you go looking for it" texture. A repeating gradient rather than
   an image: free to scale, no asset, no request. */
.machine-screen-texture {
  position: absolute;
  inset: 0;
  opacity: 0.05;
  background-image: repeating-linear-gradient(
    to bottom,
    #000 0px, #000 1px, transparent 1px, transparent 3px
  );
  mix-blend-mode: overlay;
}

.dusk-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  pointer-events: none;
  /* The gradient itself moved to .dusk-wash below — this element keeps
     only the scroll-driven opacity and its children (stars, scrim), so
     they stay measured against the PANEL's box exactly as before while
     the wash extends past it. */
}

/* The night wash, deliberately far wider than the panel so there is no
   edge to find at any window width. One gradient, same as the overlay
   carried before — not an additional layer on top of it, so nothing
   double-darkens.

   Used to start fading immediately from 0%, so the top — exactly where
   .dusk-stars sits — was already losing darkness right from the first
   pixel instead of holding a true night sky. Now holds fully opaque
   through 22% (past the star band) before it starts easing off, so the
   part of the frame with stars in it stays properly dark. It also used
   to fade all the way to transparent by 75% down, which left the
   remaining quarter of the frame — exactly where .scene-services sits —
   completely undarkened: night above, full daylight brightness right
   behind the services text, with a hard edge between the two. Holding at
   a flat 15% dark instead of 0% from 75% through the bottom keeps that
   whole lower band reading as dusk rather than cutting off. Same rule for
   both desktop and mobile — it's a % gradient with no breakpoint
   overrides, so one value covers both. Stars paint AFTER this in DOM
   order (see .dusk-stars being the last child of .dusk-overlay), so they
   are never dimmed by this getting darker — only helped by the added
   contrast. */
.dusk-wash {
  position: absolute;
  inset: 0 -100vw;
  background: linear-gradient(to bottom,
    rgba(5, 6, 12, 1) 0%,
    rgba(5, 6, 12, 1) 22%,
    rgba(5, 6, 12, 0.15) 75%,
    rgba(5, 6, 12, 0.15) 100%
  );
  pointer-events: none;
}

/* Left-side darkening under the services text. Lives INSIDE
   .dusk-overlay, so it needs no animation of its own — it inherits the
   overlay's scroll-driven opacity and therefore arrives on exactly the
   same beat as the night sky, by construction rather than by two tweens
   being kept in sync by hand. (A split-off, independently-timed version
   of this was tried — see the markup comment in index.html — and
   reverted: it started too dark too early and read as a hard edge, not a
   deliberate mood.)

   It's a wide, soft horizontal fade to nothing by 68% across, with no
   edge anywhere: it reads as a shadow pocket in the scene (the sun going
   down behind the machine) rather than a panel someone laid over the
   photo. Sitting inside the overlay also keeps it below .scene-services
   (z-index 1 vs 2) with no extra stacking work. */
.dusk-left-scrim {
  position: absolute;
  /* Extended ~3in (96px/in) past the panel's own left edge. The scrim's
     gradient starts at its box's left edge, and on a window wider than
     one panel that edge sits INSIDE the viewport — so the darkening
     began with a visible vertical seam next to the lit scene. Starting
     it well off to the left means the fade is already at full strength
     by the time it enters the frame, with no edge to see. Possible now
     that .screen-soda no longer clips its children. */
  inset: 0 0 0 -288px;
  background: linear-gradient(to right,
    rgba(13, 13, 12, 0.82) 0%,
    rgba(13, 13, 12, 0.62) 22%,
    rgba(13, 13, 12, 0.28) 45%,
    rgba(13, 13, 12, 0) 68%
  );
  pointer-events: none;
}

/* Bottom-left night pool, layered under the services copy specifically —
   a big soft RADIAL falloff anchored at the panel's own bottom-left
   corner (0% 100%), every stop still well short of opaque so there's no
   ring/edge to spot anywhere in it, only a gradual dark-to-nothing pool.
   Reach into panel 2 is deliberately smaller than .dusk-left-scrim's
   -288px (that one exists purely for edge-safety at any window width —
   this is a bounded accent meant to sit close behind the text, not a
   full wash). Inherits .dusk-overlay's own scroll-driven opacity via the
   same parent-child trick .dusk-left-scrim uses, so it arrives exactly
   as the services text rises and reverses out exactly the same way on
   scroll-up — no independent timing, per the note on .dusk-left-scrim
   above about why an independently-timed version was reverted before. */
.dusk-services-underlay {
  position: absolute;
  inset: 0 0 0 -170px;
  background: radial-gradient(
    120% 95% at 0% 100%,
    rgba(5, 6, 12, 0.6) 0%,
    rgba(5, 6, 12, 0.4) 28%,
    rgba(5, 6, 12, 0.22) 48%,
    rgba(5, 6, 12, 0.08) 68%,
    rgba(5, 6, 12, 0) 86%
  );
  pointer-events: none;
}

/* Type-level legibility, working hardest exactly where the scrim can't:
   the services block fades in at 0.62 of the pan, while the sky is still
   bright gold and the dusk overlay (and so the scrim) hasn't begun. Two
   layers — a tight dark one that actually buys the contrast, and a wide
   soft one under it so the result reads as a shadow pocket instead of a
   hard outline. Static, and these lines are transform-animated, so the
   shadow rasterizes once and just composites; it costs nothing per
   frame. Both fade into irrelevance as the background darkens, which is
   precisely when they stop being needed. */
.scene-services-title,
.scene-services-item,
.scene-services-item strong {
  text-shadow:
    0 1px 2px rgba(13, 13, 12, 0.55),
    0 2px 14px rgba(13, 13, 12, 0.45);
}
/* Gold on a bright sky is the worst pairing on the screen — the title and
   the question lines get a touch more of the tight layer. */
.scene-services-title,
.scene-services-item strong {
  text-shadow:
    0 1px 2px rgba(13, 13, 12, 0.7),
    0 2px 14px rgba(13, 13, 12, 0.5);
}

/* Stars confined to a thin band of plain sky — NOT a fraction of the full
   frame, which was reaching low enough to sit over the scene's own
   artwork (the gas station graphic).

   Used to start at the very top (inset 0), which put roughly 3/4 of the
   44 randomly-scattered stars (top: 0-85% of this box) directly behind
   .site-nav — position:fixed, z-index:100, and opaque (background:
   var(--ink)) once .is-scrolled, which it always is by the time dusk is
   visible. Most of the field was there but invisible, hidden behind the
   nav bar. Starting this band BELOW the nav instead puts the whole
   scattered field in the visible strip of sky under it, so the stars
   actually read. */
.dusk-stars {
  position: absolute;
  inset: var(--nav-h) 0 auto 0;
  height: 170px;
  overflow: hidden;
}

.dusk-star {
  position: absolute;
  width: var(--star-size, 1.5px);
  height: var(--star-size, 1.5px);
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  animation: dusk-star-twinkle var(--star-duration, 6s) ease-in-out infinite;
  animation-delay: var(--star-delay, 0s);
}
@keyframes dusk-star-twinkle {
  /* Was 0.12 at rest — low enough that between twinkles a star basically
     vanished rather than dimming. 0.22 keeps the whole field readable at
     all times; the --star-peak swing on top of it is still what reads as
     twinkling. */
  0%, 100% { opacity: 0.22; }
  50% { opacity: var(--star-peak, 0.75); }
}

/* Shooting star easter egg — hidden at rest (opacity:0, parked at its
   start position); JS toggles .is-shooting on an interval (every 25-30s,
   randomized, see main.js) to run the streak once, then removes the class
   so it resets for the next pass. Diagonal path, shortened to stay inside
   the (now much thinner) star band. */
.shooting-star {
  position: absolute;
  top: 20%;
  left: 65%;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
}
/* The trailing streak — points back toward where it came from (opposite
   the -120px,26px travel direction below), fixed to the dot so it
   travels with it. */
.shooting-star::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 55px;
  height: 1px;
  background: linear-gradient(to left, rgba(255, 255, 255, 0.75), transparent);
  transform-origin: left center;
  transform: translateY(-50%) rotate(-12.2deg);
}
.shooting-star.is-shooting {
  animation: dusk-shooting-star 1s ease-in;
}
@keyframes dusk-shooting-star {
  0% { opacity: 0; transform: translate(0, 0); }
  8% { opacity: 1; }
  80% { opacity: 1; }
  100% { opacity: 0; transform: translate(-120px, 26px); }
}

/* Mobile + tablet: .desert-text (the name) is positioned so the EYEBROW
   ("Brand, Campaign & Experience Designer") lands centered in the
   viewport, not the block as a whole — anchored at top:50% then nudged
   up by a fixed px offset roughly matching the title's own height plus
   half the eyebrow's line height, so it holds steady across device
   heights (a %-based translateY(-50%) here would instead center on the
   full block including the long identity list below, dragging the
   eyebrow off-center). The identity list is intentionally allowed to run
   off the bottom of the screen as a result. Still anchored left (not the
   top-anchored desktop placement).

   Gated on shape, not just width: this is meant for an actual portrait
   tablet/phone, where frameWidth() crops a much narrower slice of the
   panorama than the viewport shows and the desktop text placement no
   longer fits. A desktop BROWSER WINDOW dragged narrower is still wide
   relative to its own height — the panorama slice it shows is nowhere
   near as cropped — so without the aspect-ratio check here, someone
   just narrowing their window (not actually on a tablet) would hit
   this same "cropped-slice" repositioning AND the ink-colored text
   below purely from width, well before the window was ever
   tablet-shaped. max-aspect-ratio:99/100 means "height is at least a
   hair more than width" — a perfect square intentionally does NOT
   count (stays desktop), only genuinely taller-than-wide does. */
@media (max-width: 1024px) and (max-aspect-ratio: 99/100) {
  .desert-text {
    left: var(--gutter);
    right: var(--gutter);
    top: 50%;
    /* Raised ~100px from the old -56px: on these screens the block sat
       low enough to crowd the horizon. */
    transform: translateY(-156px);
    max-width: none;
  }
  /* Gold and black only on tablet/mobile — cream competed with the bright
     sky at these sizes. Desktop keeps white and gold. */
  .desert-identity-list,
  .hero-eyebrow {
    color: var(--ink);
  }
}

/* Mobile: sized against the viewport so the whole block fits the slice
   that's actually on screen as it pans through. Same aspect-ratio gate
   as the block above and for the same reason — see the note there. */
@media (max-width: 700px) and (max-aspect-ratio: 99/100) {
  .desert-text {
    left: var(--gutter);
    right: auto;
    top: 38%;
    width: min(38vw, 148px);
    max-width: 148px;
    transform: none;
  }
  .hero-title { display: none; }
  .hero-eyebrow { max-width: 140px; margin-bottom: 10px; font-size: 12px; line-height: 1.45; }
  .desert-identity-list { margin-top: 8px; font-size: 14px; line-height: 1.7; }
}

/* ==========================================================================
   DECK CONTENT — heading + gallery, as an OVERLAY inside .hero-pin (not
   normal document flow below it). Sits at inset:0 over the desert scene;
   js/main.js starts it translated below the viewport and animates it up
   to this centered rest position during the "gallery rise" phase, so it
   visibly comes up and over the (by then fully panned) soda-machine
   screen. No background of its own — the hero art shows through around
   the heading/cards, which is what makes it read as a menu pulling up
   over the machine rather than a solid panel replacing it. Once centered,
   it travels away together with the rest of .hero-pin when the pin
   releases (they're the same pinned element, so they can't drift apart).
   ========================================================================== */
.deck-content {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 0 var(--gutter);
  /* No longer offscreen via a y-transform (see MACHINE APPROACH in
     main.js) — it sits at its natural centered position throughout, and
     opacity is what main.js scrubs in as the screen takeover resolves. */
  opacity: 0;
}

/* A real pill button, not a quiet trailing link — this is the one way
   from "a curated few" to "everything," and it was getting missed sitting
   under the deck as plain underlined text (see the deck-content HTML
   comment for why the deck itself is deliberately a curated subset). */
/* Outline at rest with an occasional glow pulse (cta-pulse-glow, defined
   once below) rather than a constant solid fill — reads as an invitation,
   not shouting the whole time. Fills solid gold on hover/click. */
.deck-more-work {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 26px;
  border-radius: 999px;
  border: 1.5px solid var(--gold);
  background: transparent;
  color: var(--gold);
  /* Same font as work.html's "A closer look" (.tile__more) — was
     font-nav. */
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  animation: cta-pulse-glow 5s ease-in-out infinite;
  transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease, color 0.25s ease;
}
.deck-more-work:hover {
  animation: none;
  background: var(--gold);
  color: var(--ink);
  transform: translateY(-2px);
  box-shadow: 0 12px 30px -8px rgba(246, 186, 2, 0.7);
}

@keyframes cta-pulse-glow {
  0%, 85%, 100% { box-shadow: 0 0 0 0 rgba(246, 186, 2, 0); }
  92% { box-shadow: 0 0 20px 2px rgba(246, 186, 2, 0.5); }
}

.deck-heading {
  text-align: center;
  max-width: 640px;
  padding: 0 var(--gutter) 0;
}

/* "Now Serving" — the deck heading's own eyebrow, not to be confused
   with .deck-card-eyebrow (the small category tag inside each gallery
   card). These two used to share the class name .deck-eyebrow, which
   silently broke this one's color (the later, unrelated rule always won
   on specificity) — renamed to avoid that colliding again. */
.deck-eyebrow {
  display: block;
  font-family: var(--font-nav);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 8px;
}

.deck-heading-line {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(17px, 2.1vw, 25px);
  line-height: 1.3;
  color: var(--cream);
  margin: 0;
}

/* Downward-movement cue — fixed to the viewport (not a child of
   .hero-pin, which scrolls away once its pin releases). Hidden at rest;
   js/main.js toggles it on starting the instant the hero pan ends,
   through to the bottom of the case-study grid — see the ScrollTrigger
   near .scroll-hint in main.js. */
.scroll-hint {
  position: fixed;
  left: 50%;
  bottom: 5%;
  transform: translateX(-50%);
  z-index: 60;
  width: 1px;
  height: 40px;
  background: var(--line);
  overflow: hidden;
}
@media (min-width: 701px) {
  .scroll-hint {
    left: auto;
    right: clamp(24px, 3vw, 48px);
    bottom: clamp(24px, 5vh, 46px);
    transform: none;
    width: 2px;
    height: 50px;
    background: rgba(255, 248, 230, .3);
    box-shadow: 0 0 10px rgba(246, 186, 2, .14);
  }
}
.scroll-hint span {
  display: block;
  width: 100%;
  height: 40%;
  background: var(--gold);
  animation: hint-drop 2s ease-in-out infinite;
}
@media (min-width: 701px) {
  .scroll-hint span { height: 44%; box-shadow: 0 0 8px rgba(246, 186, 2, .42); }
}
@keyframes hint-drop {
  0%   { transform: translateY(-100%); }
  100% { transform: translateY(250%); }
}

/* ==========================================================================
   DECK GALLERY — fanned carousel. Arrow-controlled only; scroll does not
   move it. Cards are positioned with plain CSS custom properties (--x,
   --r, --s, --o, --z) that main.js sets per card based on its distance
   from the active index, then animates with GSAP on each arrow click.
   ========================================================================== */
.deck-gallery {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  width: 100%;
}

.deck-track {
  position: relative;
  z-index: 1;
  /* Third min() term is a HEIGHT budget, not a width one: .deck-track's
     height is width*4/3 (aspect-ratio 3/4 below), and .deck-content
     (its ancestor, an absolute inset:0 flex column inside .hero-pin,
     which is overflow:hidden) has no scroll of its own — a track sized
     purely off viewport WIDTH could ask for more vertical room than a
     short/wide window has, clipping .deck-more-work off the bottom
     with no way to reach it. 44vh keeps the track's own height under
     ~59vh, leaving room for the heading/nav/button/gaps above and
     below it regardless of window shape. */
  width: min(34vw, 360px, 44vh);
  aspect-ratio: 3 / 4;
  /* For .deck-card's rotateX spin-in (see main.js MACHINE APPROACH) — a
     flip with no perspective just reads as a squash, not a turn. */
  perspective: 1200px;
}

@media (max-width: 700px) {
  .deck-track { width: min(74vw, 340px); }
}

/* TABLET — its own sizes for the two blocks that were inheriting
   desktop's. Desktop's .deck-track is min(34vw, 360px), which on a
   768px-wide tablet resolves to 261px: smaller than the phone gets, on a
   much bigger screen. The services block had the same problem in reverse
   — desktop type on a screen with far less width to spend. Both are
   scaled to the tablet frame here rather than inheriting either
   neighbour's.

   Aspect-ratio gated, same as the hero .desert-text tablet block above
   — a desktop window merely narrowed (still wider than it is tall)
   shouldn't get tablet-only sizing just for crossing under 1024px; only
   a genuinely tablet-shaped (portrait-leaning) viewport should. See the
   note on that block for the full reasoning. */
@media (min-width: 701px) and (max-width: 1024px) and (max-aspect-ratio: 99/100) {
  /* Same height budget as the base rule above (see its comment) — this
     is the exact case that was cutting .deck-more-work off: a portrait-
     ish tablet width pushed .deck-track's WIDTH up to 460px (614px
     tall), which on a merely-tablet-TALL viewport left no room for the
     heading/nav/button stacked around it inside the non-scrolling
     .deck-content. */
  .deck-track { width: min(58vw, 460px, 44vh); }
  /* The rest of the tablet sizing lives in one block at the END of this
     file — see "TABLET LEGIBILITY". Rules for .deck-title, .deck-desc
     etc. can't live here: those selectors' base definitions come later in
     the file, so at equal specificity the base wins and these were
     silently doing nothing (measured: .deck-title still 19px when this
     block asked for 21). */
}

/* Real touch tablets held landscape use the same readable Services scale
   and wrapping as portrait tablets. Pointer capability prevents a narrowed
   desktop browser from being restyled as a tablet. */
@media (pointer: coarse) and (min-width: 701px) and (max-width: 1366px) {
  .scene-services { width: min(84vw, 560px); }
  .scene-services-title { font-size: 16px; margin-bottom: 24px; }
  .scene-services-list { gap: 22px; }
  .scene-services-item {
    font-size: 21px;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
  }
  .scene-services-item strong { font-size: 22px; white-space: normal; }
}

.deck-card {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 100%;
  height: 100%;
  border: none;
  padding: 0;
  border-radius: var(--card-radius);
  overflow: hidden;
  cursor: default;
  pointer-events: auto;
  background: var(--ink-raised);
  box-shadow: 0 40px 80px -30px rgba(0, 0, 0, 0.6);
  /* transform/opacity/z-index/filter are driven by main.js per card,
     based on distance from the active index. --tz (translateZ) is what
     actually pushes off-center cards BACK into the screen rather than
     just shrinking them in 2D — real depth, using .deck-track's own
     `perspective` — closer to a vending-machine option wheel than a flat
     fanned stack. The activation variables are a second transform layer:
     they cancel the finished fan at rest, compressing every card into the
     machine centre, then animate to zero/one together to reveal the fan. */
  transform:
    translateX(var(--activation-x, 0px))
    translateZ(var(--activation-z, 0px))
    rotate(var(--activation-r, 0deg))
    scale(var(--activation-scale, 1))
    translateX(var(--x, 0px))
    translateZ(var(--tz, 0px))
    rotate(var(--r, 0deg))
    scale(var(--s, 1));
  opacity: calc(var(--o, 1) * var(--activation-o, 1));
  z-index: var(--z, 1);
  filter: var(--card-filter, none);
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1),
              opacity 0.6s ease,
              filter 0.6s ease;
}

/* The activation timeline owns these transform variables while opening or
   closing, so the carousel's normal CSS transition is temporarily disabled. */
.deck-track.is-activating .deck-card { transition: none; }

/* Both images are stacked, absolute layers — img-1 visible at rest,
   img-2 (when the row has one) only ever shown after the arrow is
   clicked (see main.js .show-img-2 toggle), never on hover alone.
   --look-scale/--look-x/--look-y are set on .deck-card itself (see
   initDeckLookAround in main.js) and cascade down to whichever layer is
   currently visible, so the zoom + look-around always tracks the image
   on screen even after a swap. The idle bounce plays automatically on the
   front/center card (.is-active-card, set in main.js renderDeck) — no
   hover required — and is folded into the SAME transform via the
   keyframes below so it never fights with the look-around zoom. */
.deck-img {
  position: absolute;
  inset: -4px; /* slightly oversized so the idle bounce never exposes an edge */
  z-index: 1;
  background-size: cover;
  background-position: center;
  transform: scale(var(--look-scale, 1)) translate(var(--look-x, 0px), var(--look-y, 0px));
  transition: opacity 0.4s ease, transform 0.3s ease-out;
}
/* video.deck-img (an animated card — see isVideoFile in main.js) needs
   object-fit instead of the background-size/-position above, which only
   apply to elements with a CSS background-image. */
video.deck-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}
.deck-img-2 { opacity: 0; }
.deck-card.show-img-2 .deck-img-1 { opacity: 0; }
.deck-card.show-img-2 .deck-img-2 { opacity: 1; }

.deck-card.is-active-card .deck-img {
  animation: deck-hover-idle 3.2s ease-in-out infinite;
}
@keyframes deck-hover-idle {
  0%, 100% { transform: scale(var(--look-scale, 1)) translate(var(--look-x, 0px), var(--look-y, 0px)) translateY(0); }
  50%      { transform: scale(var(--look-scale, 1)) translate(var(--look-x, 0px), var(--look-y, 0px)) translateY(-3px); }
}

.deck-card.is-placeholder {
  cursor: default;
}
.deck-card.is-placeholder .deck-img { opacity: 0.5; }

/* Dark gradient behind the text, ~50% of the card height from the bottom —
   noticeably darker/taller than before, and visible at REST (not just on
   hover) since Type + Client are always shown per the current spec. */
.deck-gradient {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 50%;
  z-index: 2;
  background: linear-gradient(0deg, rgba(13,13,12,0.96) 0%, rgba(13,13,12,0.55) 55%, rgba(13,13,12,0) 100%);
  pointer-events: none;
}

/* Year — top-left, plain text, no background/pill behind it. */
.deck-year {
  position: absolute;
  top: 16px; left: 16px;
  z-index: 3;
  font-family: var(--font-nav);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--cream);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.65);
}

/* Client — top-right, mirrors .deck-year's own treatment exactly, just on
   the opposite corner. */
.deck-client-badge {
  position: absolute;
  top: 16px; right: 16px;
  z-index: 3;
  font-family: var(--font-nav);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--cream);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.65);
  text-align: right;
}

/* Text block — bottom anchored. .deck-text-primary (Client/title only) is
   always visible; .deck-text-reveal (Org + Type, then Contribution) grows
   in on hover via grid-template-rows, so the block visually "rises" from
   its bottom anchor with no extra transform needed. */
.deck-text {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 3;
  padding: 20px;
  display: flex;
  flex-direction: column;
  text-align: left;
}

/* Now lives inside .deck-text-reveal-inner (see renderGalleryCards in
   main.js) instead of .deck-text-primary — Org + Type is hover-revealed,
   not always-visible, per the Portfolio Master Field Guide. Margin flipped
   from -bottom to -top accordingly since it now follows the title instead
   of leading it. */
.deck-card-eyebrow {
  display: block;
  /* Same font as the work page's tile__cat (which is itself the header/
     nav font, --font-mono) — was font-nav. */
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 4px;
}

.deck-title {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
  /* Same color as the work page's tile__name — was var(--gold). */
  color: var(--cream);
  margin: 0;
  text-align: left;
}

/* grid-template-rows 0fr -> 1fr, not max-height: a fixed max-height value
   stalls partway through the transition whenever the real content is
   shorter than that value (the box finishes growing to its true size
   early, then the transition just keeps "counting" with nothing left to
   show) — that was the visible stutter. The fr-unit technique interpolates
   against the content's OWN measured height, so the motion stays smooth
   and finishes exactly when the box reaches full size, regardless of how
   long the description text runs. */
.deck-text-reveal {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  margin-top: 0;
  transition: grid-template-rows 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease, margin-top 0.4s ease;
}
.deck-text-reveal-inner {
  overflow: hidden;
  min-height: 0;
}
.deck-card:hover .deck-text-reveal,
.deck-card:focus-visible .deck-text-reveal {
  grid-template-rows: 1fr;
  opacity: 1;
  margin-top: 8px;
}

.deck-desc {
  font-size: 11px;
  color: var(--cream);
  max-width: 30ch;
  margin: 0;
  line-height: 1.45;
  text-align: left;
}

/* Arrow — right-center, only rendered by main.js when a row has a second
   photo. Hidden at rest (clean image + text only); fades in on hover and
   idle-bounces (a small horizontal nudge) the same way the photo does, so
   it reads as part of the same "alive" hover state. Click swaps the image
   — see main.js .deck-swap-arrow listener — never on hover alone. */
/* Legibility over size: the footprint is unchanged (32px), but the
   caret is gold on a darker, blurred pill with a gold hairline and a
   soft gold halo that breathes on the same cycle as the nudge — it now
   separates from any photo underneath it instead of dissolving into the
   bright ones. */
.deck-swap-arrow {
  position: absolute;
  top: 50%; right: 12px;
  z-index: 4;
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 1px solid rgba(246, 186, 2, 0.55);
  background: rgba(13, 13, 12, 0.72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--gold);
  font-size: 18px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  transform: translateY(-50%);
  box-shadow: 0 0 0 rgba(246, 186, 2, 0);
  transition: opacity 0.3s ease, background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.deck-card:hover .deck-swap-arrow,
.deck-card:focus-visible .deck-swap-arrow {
  opacity: 1;
  animation: deck-arrow-idle 3.2s ease-in-out infinite;
}
.deck-swap-arrow:hover {
  background: rgba(13, 13, 12, 0.9);
  border-color: var(--gold);
  box-shadow: 0 0 14px rgba(246, 186, 2, 0.35);
}
/* Nudge + halo on one keyframe track: two separate animations on the
   same element would fight over `transform`. 0% and 100% are the rest
   state, so the reduced-motion rule up top (which collapses duration to
   ~0 and lands on the final keyframe) settles here, not mid-pulse. */
@keyframes deck-arrow-idle {
  0%, 100% {
    transform: translateY(-50%) translateX(0);
    box-shadow: 0 0 0 rgba(246, 186, 2, 0);
  }
  50% {
    transform: translateY(-50%) translateX(3px);
    box-shadow: 0 0 16px rgba(246, 186, 2, 0.4);
  }
}

@media (max-width: 700px) {
  .deck-text { padding: 16px; }
  .deck-card-eyebrow { font-size: 9px; margin-bottom: 3px; }
  .deck-title { font-size: 16px; }
  .deck-desc { font-size: 10px; max-width: 26ch; line-height: 1.35; }
  .deck-year { font-size: 10px; top: 12px; left: 12px; }
  .deck-client-badge { font-size: 10px; top: 12px; right: 12px; }
  .deck-swap-arrow { width: 28px; height: 28px; font-size: 15px; right: 8px; }
}

/* TOUCH DEVICES (tablet + mobile) — the hover-gated fold and arrow above
   have no equivalent on a device that can't hover: nothing would ever
   trigger them, permanently hiding the Org+Type/Contribution lines and
   the second-image affordance. hover:none is the standard signal for
   "this device's primary input can't hover" — forcing the reveal open
   and the arrow visible here means everything above stays exactly as-is
   for desktop (mouse/trackpad), unchanged. */
@media (hover: none) {
  .deck-text-reveal {
    grid-template-rows: 1fr;
    opacity: 1;
    margin-top: 8px;
  }
  .deck-swap-arrow {
    opacity: 1;
  }
}

.deck-nav {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 14px;
}

/* Sits between the two arrows (see index.html) — tabular-nums so the
   digits don't shift width as the count changes. */
.deck-counter {
  min-width: 46px;
  font-family: var(--font-nav);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--cream-dim);
  font-variant-numeric: tabular-nums;
  text-align: center;
  user-select: none;
}

/* The carets shimmer cream -> gold on a slow loop, offset between prev
   and next (see .deck-next's negative delay) so they alternate rather
   than pulsing in unison — a pointer, not a strobe. Hovering settles it
   to solid gold and stops the loop: once you've found it, it stops
   asking. `--gold` here is the same accent as the .scroll-hint bar.

   The press state is JS-driven (.is-pressed, toggled in initDeckCarousel)
   rather than :active, so the brighten can snap in fast and release on
   its own slower curve — :active would drop the whole effect the instant
   the mouse comes up. Two transition timings on one rule do that: the
   release (default, ~0.42s eased) is what's declared here, and the
   .is-pressed rule overrides with the fast ~0.09s attack. */
.deck-arrow {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: rgba(13, 13, 12, 0.5);
  color: var(--cream);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition:
    background 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    border-color 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    color 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.3s ease;
}
.deck-arrow:not(:disabled) {
  animation: deck-caret-tempt 2.6s ease-in-out infinite;
}
.deck-next:not(:disabled) { animation-delay: -1.3s; }
.deck-arrow:hover {
  animation: none;
  color: var(--gold);
  background: rgba(13, 13, 12, 0.8);
  border-color: rgba(246, 186, 2, 0.6);
}
.deck-arrow.is-pressed {
  transition-duration: 0.09s;
  transform: scale(0.94);
  color: #fff;
  background: rgba(246, 186, 2, 0.22);
  border-color: var(--gold);
  box-shadow: 0 0 22px rgba(246, 186, 2, 0.45);
}
.deck-arrow:disabled {
  opacity: 0.3;
  cursor: default;
  pointer-events: none;
}
/* Rest state at both 0% and 100% — see the note on deck-arrow-idle. */
@keyframes deck-caret-tempt {
  0%, 100% { color: var(--cream); border-color: var(--line); }
  50%      { color: var(--gold);  border-color: rgba(246, 186, 2, 0.45); }
}

/* ==========================================================================
   ABOUT + CONTACT — same layout/type spec as work.html's about section
   (css/work.css's .about__* — kept in sync by hand, same reasoning as
   every other main.js/work.js duplication on this site) so the two pages'
   "about me" reads as one thing, not two different sections a click apart.

   LEFT: a real CSS float — the photo floats top-right, the bio text
   starts in the narrower space to its left, then widens to the full
   column once it runs past the photo's bottom. RIGHT: the heading, a
   gap, the "OPEN TO WORK." eyebrow + email, then the LinkedIn/Book-a-
   call actions. Both columns start at the same grid-row top edge (grid
   items' default cross-axis alignment), which is what puts the photo's
   top and "Available for..."'s top at the same height — the "About"
   label sits ABOVE that shared line via absolute positioning (see
   .about-section-eyebrow) rather than pushing the photo down, so it
   doesn't have to give up that alignment to exist.

   No breakpoint between desktop and tablet — every size in that range
   is one continuous fluid layout (clamp()'d gap/columns/type), so
   nothing suddenly jumps to a different arrangement; only the mobile
   breakpoint at the bottom actually swaps to the stacked, full-width
   photo. */
.about {
  position: relative;
  z-index: 2;
  padding: clamp(90px, 12vw, 160px) var(--gutter) clamp(70px, 9vw, 120px);
}

.about-grid {
  display: grid;
  /* Right (contact) column widened — was 0.85fr, which left the
     "Available for projects..." heading so little room that narrowing
     the window past desktop made one of its sheet-authored line breaks
     wrap a second time into a short, orphaned line. 1fr still leaves the
     left (photo/bio) column the larger share, just by less. */
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: clamp(28px, 4.5vw, 64px);
  max-width: 1240px;
  margin: 0 auto;
}

/* position:relative anchors .about-section-eyebrow, which sits above
   this box without being part of its flow (see below) — everything
   else here (the floated photo + wrapping bio text) is plain block
   flow, not flex, which is what lets the text actually wrap under the
   photo instead of sitting in a fixed-height row beside it. */
.about-copy {
  position: relative;
}
/* Clearfix: a float doesn't contribute to its parent's height on its
   own, so a bio short enough to clear the photo well before its bottom
   would otherwise leave .about-copy's own box shorter than the photo —
   this makes sure the section's layout below still accounts for the
   photo's real height. */
.about-copy::after {
  content: '';
  display: table;
  clear: both;
}

.about-section-eyebrow {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  /* Own height + a fixed gap, both via one transform (not a negative
     margin) so it doesn't need updating if the eyebrow's font-size ever
     changes — -100% is relative to THIS element's own box, always. */
  transform: translateY(calc(-100% - 14px));
  font-family: var(--font-nav);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  margin: 0;
}

/* Bigger than the old flex-row version, but capped well under half the
   column's width even at its widest (per an explicit "bigger, but not
   half-width" ask) — 280px against a left column that's normally
   400-600px+ wide. margin-left is the gap the wrapped bio text keeps
   from the photo; margin-bottom is the gap below it once the text
   clears it and goes full-width. */
.about-photo {
  float: right;
  width: clamp(160px, 21vw, 280px);
  margin: 0 0 18px 24px;
  aspect-ratio: 1 / 1;
  border-radius: 16px;
  overflow: hidden;
}
.about-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Font size matches the case-study description preview (.tile--case
   .tile__desc in css/work.css) — the two "preview paragraph" voices on
   the site are now the same size. .about-lead (just the opening clause —
   "I'm Sheri" — written directly into the fallback markup and reproduced
   by renderBioHtml() in main.js for the live sheet copy) steps up to the
   project-title voice — same font/size as .deck-title — so it reads as
   a lead-in, not just the first few words of the paragraph. .about-dash
   is the em dash between it and the rest of the sentence, plain body
   color, not gold. */
.about-body {
  text-align: left;
  font-family: var(--font-body);
  /* Fluid, tied to viewport width like every other clamp() in this
     section — as the column narrows toward tablet the text shrinks
     with it instead of holding a fixed size and reading oddly large
     against the (also fluid, but proportionally smaller) photo. */
  font-size: clamp(14px, 1.4vw, 15.5px);
  line-height: 1.65;
  color: var(--cream-dim);
  margin: 0;
}
.about-lead {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(16px, 1.6vw, 19px);
  color: var(--gold);
}
.about-dash { color: var(--cream-dim); }

.about-heading {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(20px, 2.3vw, 29px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--cream);
  margin: 0;
  /* The sheet writes this heading as several pipe-separated (= <br>)
     lines on purpose (see loadSiteContent() in main.js), so it's never
     truly "one paragraph" that could reflow freely — text-wrap:balance
     just keeps each of THOSE lines from unevenly re-wrapping into a
     short orphaned line of its own when the column narrows, spreading
     any forced wrap evenly instead. */
  text-wrap: balance;
}

/* The eyebrow + email pair, moved down to sit together — a deliberate
   gap from the heading above marks them as a separate "here's how"
   beat, not a continuation of the "available for" statement. */
.contact-callout-group {
  margin-top: 32px;
}

.about-eyebrow {
  display: block;
  font-family: var(--font-nav);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  margin: 0 0 10px;
}

/* Size matches the project-title voice (.deck-title/.tile__name) — was a
   much larger standalone statement; now it reads as part of the same
   scale as everything else instead of its own oversized thing. The
   underline is permanently gold — no gray resting state, no hover
   color-change on the line itself (only the text still shifts to gold
   on hover, which does nothing visually once the line is already gold,
   but keeps the affordance that this is clickable). */
.contact-email {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  color: var(--cream);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--gold);
  transition: color 0.25s ease;
  word-break: break-word;
  cursor: pointer;
}
.contact-email:hover { color: var(--gold); }

/* Same pill-button pair work.css defines as .btn/.btn--solid — kept as
   the same two class names here so a change to one is easy to mirror to
   the other. Font matches "A closer look" (.tile__more in css/work.css),
   not the eyebrow font — was font-nav. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 11px 18px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.06em;
  transition: border-color 0.22s ease, background 0.22s ease, color 0.22s ease, transform 0.22s ease;
}
.btn:hover { border-color: var(--gold); color: var(--gold); transform: translateY(-1px); }
.btn--solid { background: var(--gold); border-color: var(--gold); color: var(--ink); font-weight: 600; }
.btn--solid:hover { background: var(--cream); border-color: var(--cream); color: var(--ink); }

.contact-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }

/* True mobile only (matches the site's other mobile breakpoints, e.g.
   .scene-services-item/.deck-heading-line below) — deliberately left
   unchanged in spirit from before this section's tablet/desktop rework:
   single column, full-width square photo above the bio text (a plain
   stack, not the float-wrap the tablet/desktop version uses — a floated
   square photo at full column width would just BE full width with
   nothing left for text to wrap into). The "About" eyebrow is desktop/
   tablet-only polish, not part of the untouched mobile design, so it's
   hidden here rather than adding a new element to a layout that was
   asked to stay as-is. */
@media (max-width: 700px) {
  .about-grid { grid-template-columns: 1fr; gap: 32px; }
  .about-section-eyebrow {
    display: block;
    position: static;
    transform: none;
    margin: 0 0 14px;
  }
  .about-photo { float: none; width: 100%; margin: 0 0 16px; }
}

/* A narrowed mouse-driven desktop must not inherit the phone's full-width
   portrait. It keeps the compact single-column reading order, with portrait
   and bio sharing the first row above the contact statement. */
@media (max-width: 700px) and (any-pointer: fine) {
  .about-copy {
    display: grid;
    grid-template-columns: minmax(150px, 34%) minmax(0, 1fr);
    column-gap: 24px;
    align-items: start;
  }
  .about-copy::after { display: none; }
  .about-section-eyebrow { grid-column: 1 / -1; }
  .about-photo { width: 100%; margin: 0; }
}

/* Touch tablets follow the same hierarchy: portrait and bio first, then the
   full-width availability/contact statement beneath them. */
@media (pointer: coarse) and (min-width: 701px) and (max-width: 1366px) {
  .about-grid { grid-template-columns: 1fr; gap: 42px; }
  .about-copy {
    display: grid;
    grid-template-columns: minmax(190px, 31%) minmax(0, 1fr);
    column-gap: 28px;
    align-items: start;
  }
  .about-copy::after { display: none; }
  .about-section-eyebrow {
    position: static;
    grid-column: 1 / -1;
    transform: none;
    margin: 0 0 16px;
  }
  .about-photo { float: none; width: 100%; margin: 0; }
}

/* Desktop About is an editorial three-column composition: portrait and bio
   share a deliberate row on the left, while the availability/contact block
   forms a quieter final column. This replaces the old float-wrap shape on
   desktop only; the approved full-width mobile portrait and tablet layout
   above remain unchanged. */
@media (min-width: 1025px) and (any-pointer: fine) {
  .about {
    background: #12110f;
  }

  .about-grid {
    grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.75fr);
    gap: clamp(40px, 5vw, 72px);
  }

  .about-copy {
    display: grid;
    grid-template-columns: clamp(210px, 18vw, 270px) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    column-gap: clamp(28px, 3vw, 44px);
    row-gap: 26px;
    align-items: start;
  }

  .about-copy::after { display: none; }

  .about-section-eyebrow {
    position: static;
    grid-column: 1 / -1;
    transform: none;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--line);
  }

  .about-photo {
    float: none;
    width: 100%;
    margin: 0;
    border-radius: 12px;
  }

  .about-body {
    font-size: 15.5px;
    line-height: 1.72;
    max-width: 46ch;
  }

  .contact-block {
    margin-top: 40px;
    padding: 28px 0 0 clamp(24px, 3vw, 42px);
    border-top: 1px solid var(--line);
    border-left: 1px solid var(--line-soft);
  }

  .about-heading {
    font-size: clamp(22px, 2vw, 28px);
    line-height: 1.25;
  }

  .contact-callout-group { margin-top: 26px; }
}

/* ==========================================================================
   FOOTER — same spec as work.html's .wfoot/.wfoot__base: one row, nav
   links then copyright, font-mono, 11px. Kept the .site-footer/.footer-*
   class names (HTML unchanged) but the numbers below are work.html's,
   copied by hand — see css/work.css's own .wfoot__base for the twin.
   ========================================================================== */
.site-footer {
  position: relative;
  z-index: 2;
  border-top: 1px solid var(--line);
  padding: 24px var(--gutter) 32px;
}

/* Three-part row — nav left, LinkedIn centered and on its own (not grouped
   with the other links), copyright right. */
.footer-links {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px 26px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--cream-dim);
}

.footer-links nav {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  justify-self: start;
}

.footer-link {
  color: var(--cream-dim);
  transition: color 0.2s ease;
}
.footer-link:hover { color: var(--gold); }
.footer-linkedin { justify-self: center; }
/* Credits sits just before the copyright, sharing the grid's right-hand
   slot. As two separate grid items they overflowed the three-column row
   and the copyright wrapped onto a line of its own. */
.footer-end {
  display: flex;
  align-items: center;
  gap: 20px;
  justify-self: end;
}

@media (max-width: 700px) {
  .footer-links { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .footer-links nav, .footer-linkedin, .footer-end { justify-self: center; }
}

.footer-copy {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--cream-dim);
  white-space: nowrap;
}

/* ==========================================================================
   END-OF-PAN PEEK — the eye. Every device: press and hold with a finger
   or the mouse. See "END-OF-PAN PEEK" in main.js.

   Fixed to the right edge, vertically centred: it points at the thing it
   reveals. Hidden at rest and toggled by a ScrollTrigger spanning the
   end of the pan to the start of the gallery rise.
   ========================================================================== */
.peek-eye {
  position: fixed;
  top: 50%;
  right: 14px;
  transform: translateY(-50%);
  z-index: 60;
  width: 52px;
  height: 52px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(246, 186, 2, 0.55);
  background: rgba(13, 13, 12, 0.72);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  /* The whole point is holding it without the page scrolling underneath;
     touch-action:none stops the browser claiming the gesture first. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.peek-eye svg { width: 26px; height: 26px; display: block; }

/* Held: brightens and the pupil slides toward the edge it's looking at —
   the affordance does the thing it's describing. */
.peek-eye:hover {
  background: rgba(13, 13, 12, 0.88);
  border-color: var(--gold);
  box-shadow: 0 0 14px rgba(246, 186, 2, 0.28);
}
.peek-eye.is-held {
  background: rgba(246, 186, 2, 0.2);
  border-color: var(--gold);
  box-shadow: 0 0 22px rgba(246, 186, 2, 0.4);
}
.peek-eye-pupil {
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
  transform-origin: center;
}
.peek-eye.is-held .peek-eye-pupil { transform: translateX(3.4px); }

/* At rest it glances toward the edge every few seconds — enough to read
   as "there's something over there", not enough to nag. Rest state at
   0% and 100% so the global reduced-motion rule settles it centred. */
.peek-eye:not(.is-held) .peek-eye-pupil {
  animation: peek-eye-glance 4.5s ease-in-out infinite;
}
@keyframes peek-eye-glance {
  0%, 62%, 100% { transform: translateX(0); }
  74%, 86%      { transform: translateX(2.6px); }
}

@media (max-width: 700px) {
  .peek-eye { width: 46px; height: 46px; right: 10px; }
  .peek-eye svg { width: 23px; height: 23px; }
}

/* While a scroll gate is holding, the existing scroll cue brightens and
   quickens — the page paused deliberately, and this says so. Nothing is
   required of the visitor: the hold releases itself and scrolling
   continues. See SCROLL GATES in main.js. */
.scroll-hint.is-gate-holding {
  background: rgba(246, 186, 2, 0.35);
  box-shadow: 0 0 12px rgba(246, 186, 2, 0.35);
}
.scroll-hint.is-gate-holding span {
  animation-duration: 0.7s;
}


/* ==========================================================================
   TABLET LEGIBILITY (701–1024px)

   A tablet inherited desktop's type almost everywhere, which is sized for
   a screen you sit close to with far more width to spend. Measured at
   768px, fifteen elements were under 14px — body copy at 11px on a device
   usually held at arm's length. This block raises the things you actually
   READ, and nudges the small tracked labels just enough to stay crisp
   without losing their deliberate label quality.

   It sits at the END of the file on purpose: several of these selectors
   are defined further down than the earlier tablet block, and at equal
   specificity the later rule wins — which is why an earlier attempt at
   .deck-title/.deck-desc here silently did nothing.

   Aspect-ratio gated (max-aspect-ratio:99/100), same as the other two
   701-1024px blocks above — a desktop browser window just narrowed
   (still wider than it is tall) isn't a tablet and shouldn't flip into
   tablet-only type sizing purely for crossing a width number; a real
   tablet is normally held portrait, where this belongs. See the note on
   .desert-text's tablet block further up for the full reasoning. A
   perfect square intentionally does NOT count as "tablet-shaped"
   either — only genuinely taller-than-wide does. */
@media (min-width: 701px) and (max-width: 1024px) and (max-aspect-ratio: 99/100) {
  /* Hero */
  .hero-eyebrow { font-size: 13px; }
  .desert-identity-list { font-size: 15px; line-height: 2; }

  /* Services — sized to the "Concepts, campaigns, launches…" line, which
     measures 499px at 20px type; the two longer answers then WRAP onto a
     second line instead of being cut off. The base rule keeps these on
     one line with an ellipsis (that's its one-line guarantee on desktop,
     where there's width to spare) — at tablet width that was silently
     eating the ends of two of the four answers, so nowrap/ellipsis is
     turned off here rather than the copy being trimmed to fit. */
  .scene-services { width: min(84vw, 512px); }
  .scene-services-title { font-size: 16px; margin-bottom: 24px; }
  .scene-services-list { gap: 22px; }
  .scene-services-item { font-size: 21px; }
  .scene-services-item strong { font-size: 22px; }
  .scene-services-item {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
  }
  .scene-services-item strong { white-space: normal; }

  /* Gallery deck */
  .deck-heading-line { font-size: 22px; }
  .deck-title { font-size: 21px; }
  .deck-desc { font-size: 14px; max-width: 32ch; line-height: 1.5; }
  .deck-card-eyebrow { font-size: 12px; }
  .deck-year,
  .deck-client-badge { font-size: 12px; }

  /* About + contact + chrome */
  /* .about-heading/.about-body used to get fixed, bumped-up sizes here
     (24px/17px) — removed now that both use a fluid clamp() instead
     (see .about-heading/.about-body in the "ABOUT + CONTACT" section):
     a fixed floor at exactly this range is what was letting the
     heading's sheet-authored line breaks wrap a second time into short
     orphaned lines as the tablet column narrowed — the whole reason
     that section moved to fluid sizing. The clamp()s' own minimums
     (20px / 14px) already keep both comfortably above the old "under
     14px, read from arm's length" problem this block exists to fix. */
  .about-eyebrow { font-size: 14px; }
  .contact-email { font-size: 24px; }
  .nav-link { font-size: 14px; }
  .nav-callout { font-size: 12px; }
  .footer-link { font-size: 14px; }
  .footer-copy { font-size: 13px; }
}

/* ==========================================================================
   IMAGE PROTECTION — deterrence, not security.

   Blocks the two casual gestures: dragging a picture off the page, and
   right-click > Save Image As. That is genuinely most people.

   It is NOT protection. Anyone can still screenshot, open devtools, or read
   the URL straight out of the page source — the file has to reach the
   browser to be displayed at all, so it can always be saved by someone who
   wants to. Treat this as a speed bump, not a lock. The real protections for
   published work are watermarking, posting at display resolution (already
   the case here: nothing is above 1800px), and copyright.
   ========================================================================== */
img, video {
  -webkit-user-drag: none;
  -khtml-user-drag: none;
  -moz-user-drag: none;
  -o-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none; /* stops iOS long-press "Save Image" */
}
img { user-select: none; -webkit-user-select: none; }
