/* ==========================================================================
   JOYBOX for Media — site styles

   One fixed ink film sits behind the entire page; every section
   floats over it in glass. Arabic is the default; English sits behind the
   toggle. Direction-sensitive rules use CSS logical properties so one rule
   serves both LTR and RTL.
   ========================================================================== */

:root{
  --bg:       #07071a;
  --white:    #ffffff;
  --text:     rgba(255,255,255,.92);
  --text-2:   rgba(255,255,255,.72);
  --text-3:   rgba(255,255,255,.55);
  --yellow:   #ffd230;

  --g-bg:     rgba(255,255,255,.10);   /* glass fill  */
  --g-bg-2:   rgba(255,255,255,.15);   /* glass chip  */
  --g-line:   rgba(255,255,255,.15);   /* glass edge  */
  --g-line-2: rgba(255,255,255,.25);

  --sans:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --sans-ar: "IBM Plex Sans Arabic", "Inter", system-ui, sans-serif;

  --gut:   clamp(20px, 4vw, 56px);
  --story-h: 490vh;      /* the story runway + the zoom-through outro (see js: STORY_END) */
  --nav-h: 68px;
  --ease:  cubic-bezier(.22,.61,.36,1);

  --shadow-text: 0 2px 6px rgba(0,0,0,.28), 0 10px 32px rgba(0,0,0,.42);
  --shadow-text-sm: 0 1px 3px rgba(0,0,0,.35), 0 6px 18px rgba(0,0,0,.35);

  /* The mark floats over the same live film as the type, and it was the only
     element out there without the separation .display, .intro, .body and
     .disciplines li all get — which is why it read pasted-on. drop-shadow, not
     box-shadow: the halo follows the PNG's alpha silhouette instead of drawing
     a bounding box the artwork never fills, and its blur is the only thing
     softening the asset's hard-cut, un-antialiased edge. Neutral, never
     tinted — the artwork already carries red, yellow and blue, so a coloured
     glow behind it would compete with the mark rather than seat it. */
  --shadow-mark:    drop-shadow(0 2px 5px rgba(0,0,0,.34)) drop-shadow(0 14px 30px rgba(0,0,0,.30));
  --shadow-mark-sm: drop-shadow(0 1px 2px rgba(0,0,0,.42));
}

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

/* No scroll-snap. It was tried, to make the act handovers feel "locked", and
   it has to stay out: a mouse wheel scrolls in discrete ~120px notches, and
   proximity snap re-runs its search after every one, so the page is
   continually yanked back toward the nearest rest point. Measured over 24
   real wheel notches at 1440×900 — deltas swinging between −97px and +92px
   with EIGHT of the 24 moving the page backwards, against a clean uniform
   120px per notch with snap off. A trackpad's continuous deltas hide this
   almost completely, which is how it survived the first pass. The locked
   feeling comes from the pinned stage, not from snapping. */
html{
  scroll-behavior:smooth;scroll-padding-top:var(--nav-h);background:var(--bg);
}

body{
  margin:0;
  background:var(--bg);
  color:var(--text);
  font-family:var(--sans-ar);
  font-size:15px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
}
html[lang="en"] body{font-family:var(--sans)}

::selection{background:rgba(255,255,255,.22)}

img,video,canvas{display:block;max-width:100%}
ul{margin:0;padding:0;list-style:none}
h1,h2,p{margin:0}
a{color:inherit}
button{font:inherit;color:inherit}

/* ==========================================================================
   TYPE
   ========================================================================== */

.dim{color:var(--text-3)}

/* Mono-style labels: uppercase, tracked, small. Arabic has no case and must
   never be letter-spaced — tracking breaks the joins between letters. */
.label{
  font-family:var(--sans);
  font-size:11px;font-weight:500;
  letter-spacing:.15em;text-transform:uppercase;
  line-height:1.4;
}
html[lang="ar"] .label{font-family:var(--sans-ar);font-size:12px;letter-spacing:0;text-transform:none}

.display{
  font-family:var(--sans);font-weight:400;
  line-height:1.04;letter-spacing:-.025em;
  text-wrap:balance;
}
html[lang="ar"] .display{font-family:var(--sans-ar);font-weight:500;line-height:1.22;letter-spacing:0}

.d-xl{font-size:clamp(2.7rem,5.8vw,84px)}
.d-lg{font-size:clamp(2.2rem,4.2vw,60px)}
.d-md{font-size:clamp(1.8rem,3vw,44px)}
html[lang="ar"] .d-xl{font-size:clamp(2.2rem,4.8vw,68px)}
html[lang="ar"] .d-lg{font-size:clamp(1.9rem,3.6vw,52px)}
html[lang="ar"] .d-md{font-size:clamp(1.6rem,2.6vw,38px)}

.intro{
  max-width:22rem;
  font-size:clamp(1.05rem,1.3vw,1.25rem);line-height:1.6;
  text-align:end;
  text-shadow:var(--shadow-text-sm);
}
html[lang="ar"] .intro{line-height:1.85}

.body{
  max-width:30rem;
  font-size:clamp(.95rem,1.1vw,1.05rem);line-height:1.65;
  color:var(--text-2);
  text-shadow:var(--shadow-text-sm);
}
html[lang="ar"] .body{line-height:1.9}

.lede{
  max-width:34rem;margin:18px 0 34px;
  font-size:clamp(1rem,1.15vw,1.1rem);line-height:1.7;color:var(--text-2);
}
html[lang="ar"] .lede{line-height:1.95}

.link{color:var(--text);text-decoration:none;border-bottom:1px solid rgba(255,255,255,.3);transition:border-color .3s}
.link:hover{border-color:var(--white)}

/* ==========================================================================
   REVEAL — fade-up on enter; delay per element via --d
   ========================================================================== */

html.js .reveal{
  opacity:0;
  transform:translateY(32px);
  transition:opacity .7s var(--ease) var(--d,0ms), transform .7s var(--ease) var(--d,0ms);
  will-change:transform,opacity;
}
html.js .reveal.in{opacity:1;transform:none}
html.js .reveal.done{will-change:auto}

/* ==========================================================================
   GLASS
   ========================================================================== */

/* Real glass: a blurred backdrop. Only for surfaces that sit over the *live*
   film — the nav, and everything inside the story. */
.glass{
  background:var(--g-bg);
  border:1px solid var(--g-line);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
}

/* Flat pane: the same material, without the blur, for the bands below the
   story. Not a compromise — by the time the reader reaches them the film is
   under an 80% veil, so there is essentially nothing left to blur and the
   effect is invisible. What it is *not* is free: measured over a scroll of
   work → footer at 1440×900, six blurred work cards plus the photo slot and
   the contact panel each re-sample and re-blur the backdrop every frame.

     band surfaces blurred      mean 44.6 ms   p95 70.8   27 dropped frames
     work cards unblurred       mean 26.9 ms   p95 50.0    7 dropped frames
     no band surface blurred    mean 17.1 ms   p95 20.9    0 dropped frames

   A 2.6× difference in the worst region of the page, for an effect nobody can
   see. The fill is nudged up a little to carry the frosting on its own. */
.pane{
  background:rgba(255,255,255,.055);
  border:1px solid var(--g-line);
}

.chip{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  min-height:38px;padding:0 16px;border-radius:8px;
  background:var(--g-bg-2);border:1px solid rgba(255,255,255,.2);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  font-size:13px;font-weight:500;color:var(--white);
  cursor:pointer;transition:background .3s;
}
.chip:hover{background:rgba(255,255,255,.25)}

.badge{
  display:inline-flex;align-items:center;
  padding:7px 12px;
  border-inline-start:2px solid var(--yellow);
  background:var(--g-bg-2);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.badge > span{font-family:var(--sans);font-size:11px;font-weight:500;letter-spacing:.15em;text-transform:uppercase;line-height:1.4}
html[lang="ar"] .badge > span{font-family:var(--sans-ar);font-size:12px;letter-spacing:0;text-transform:none}

/* --- buttons ------------------------------------------------------------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  min-height:44px;padding:0 22px;border-radius:999px;
  background:var(--white);color:#0a0a0a;
  font-family:inherit;font-size:14px;font-weight:500;line-height:1;
  text-decoration:none;white-space:nowrap;cursor:pointer;
  transition:background .3s,color .3s,transform .3s var(--ease);
}
.btn:hover{background:rgba(255,255,255,.85)}
.btn-sm{min-height:36px;padding:0 16px;font-size:12.5px}
.btn-lg{min-height:52px;padding:0 28px;font-size:15px}
.btn-ghost{
  background:var(--g-bg);color:var(--white);
  border:1px solid var(--g-line-2);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.btn-ghost:hover{background:rgba(255,255,255,.2)}

.chev{flex:none;transition:transform .3s var(--ease)}
html[dir="rtl"] .chev{transform:scaleX(-1)}
.btn:hover .chev{transform:translateX(2px)}
html[dir="rtl"] .btn:hover .chev{transform:scaleX(-1) translateX(2px)}

:focus-visible{outline:2px solid var(--yellow);outline-offset:3px;border-radius:4px}
.btn:focus-visible{border-radius:999px}
.chip:focus-visible{border-radius:8px}

/* ==========================================================================
   FILM — fixed behind everything; the canvas is drawn by js/site.js
   ========================================================================== */

/* The first frame is painted as this element's own background, so the page
   opens on ink rather than on a navy void while the <video> and the frame
   sequence are still arriving. It costs one raster at first paint and nothing
   per frame, and js clears it in activateSeq() once the canvas is live.

   Deliberately on .film and NOT on .film-stage: the stage carries
   will-change:transform and is scaled to 3.5x during the zoom-through, so
   giving it paintable content would mean rasterising that content at the
   scaled size — its emptiness is exactly why the zoom is cheap. The cost of
   painting here instead is that this background does not carry the stage's
   1.03 scale, so there is a ~3% jump when the video takes over. On frame 0,
   which is dark rays with the ink only just arriving, it is not perceptible.

   f001 is also the first frame the sequence loader requests (coarse-first,
   stride 8 from index 0), so this is the same URL and costs no extra fetch.
   It is the one thing that also gives the no-JS case a film: loadFilm() never
   runs there, so without this the whole page sits on flat navy. */
.film{
  position:fixed;inset:0;z-index:0;
  background:var(--bg) url(../assets/frames24/land/f001.webp) center/cover no-repeat;
  overflow:hidden;pointer-events:none;
}
.film-stage{position:absolute;inset:0;will-change:transform;transform-origin:50% 50%}
.film-media{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

/* Hidden until enough frames are decoded; then it replaces the <video>,
   which stays in the markup as the early-paint and fallback scrubber. */
#filmcanvas{display:none}
#filmcanvas.on{display:block}

/* The veil is a light rig, not a letterbox.

   It used to be a linear gradient only: dark at the top, dark at the bottom,
   nothing horizontally. That vertical treatment works — it is why the headline
   reads over busy ink — but the horizontal half of the same rig was never
   built, and .act is space-between on a column of space-between rows, so
   .intro, .card and .panel all sit hard against the horizontal edges with no
   help at all. A vignette tells the eye there is a camera and a lamp, which is
   most of the difference between a video background and a shot.

   REBALANCED, not stacked. Adding a .30 radial on top of a veil already at .58
   would put the top corners at 1-(1-.58)(1-.30) = .706 — a 22% density jump
   exactly where act 1's arriving rays live, which would crush them. The linear
   stops are pulled back by what the radial now contributes on the vertical
   axis, so top-centre lands at .545 (essentially unchanged) while the top
   corner reaches .612. That .067 differential is about 6 code values over
   mid-luma ink: a real corner, not a crushed one.

   It behaves correctly through the zoom-through for free, by virtue of where
   this element already sits. .film-veil is a SIBLING of .film-stage, not a
   child, so it neither scales to 3.4x nor carries the 12px cursor parallax.
   The film rushes THROUGH the lens while the lens stays nailed to the
   viewport. The lens does not zoom; the subject does. That is also why the
   held frame reads as held in space rather than pasted on — a lens edge
   belongs to the frame, not to the subject.

   Nothing in js/site.js touches this element (grep -c veil js/site.js == 0),
   so it is painted once and never invalidated. Zero per frame, zero at idle. */
.film-veil{
  position:absolute;inset:0;
  background:
    radial-gradient(ellipse 78% 72% at 50% 46%,
      transparent 38%, rgba(7,7,26,.30) 100%),
    linear-gradient(180deg,
      rgba(7,7,26,.48) 0%, rgba(7,7,26,.10) 26%,
      rgba(7,7,26,.06) 58%, rgba(7,7,26,.58) 100%);
}

/* Emulsion tooth. Sampling 8x8 blocks across the landscape frames, the 25th
   percentile block standard deviation is exactly 0.00 on f010 and on f225 — a
   quarter or more of every held frame is byte-identical dead flat, and f225 is
   the logo landing the whole rest of the site rests on. "Reads as vector fill"
   is literally true, and it is the real reason a stopped frame looks like a
   render rather than a photograph.

   64px tile, values uniform 0-128 so the tile mean is 64 rather than 128, at
   2%. A mean-128 tile at 3.5% — the obvious first guess — lifts the darkest
   decile by 19-23% and raises the true black floor from 0.66 to 3.35. On a
   page that argues about whether to spend the yellow on a loading bar, milky
   blacks are not a rounding error. This lands sigma at 0.74 levels: enough to
   dither a one-level band, well below where anyone reads it as grain.

   A file, not an inline data URI — uniform noise is incompressible, so the
   base64 of this is 21.6KB on a render-blocking stylesheet.

   NOT mix-blend-mode:overlay, which some would call the correct zero-lift
   grain. It forces its own compositor layer and blends against a backdrop
   containing .film-stage — a will-change:transform layer scaled to 3.4x
   through the zoom. That is a readback of the exact layer whose emptiness is
   why the zoom is cheap. Pre-darkening the tile buys most of the benefit for
   nothing. Animated grain is likewise out: a clock-driven loop over a held
   scene, repainting every frame, is rejected approach #1 exactly. This is a
   still texture on a still plate, and it is at its best precisely when the
   scroll has stopped. */
.film-grain{
  position:absolute;inset:0;
  background:url(../assets/grain64.png) repeat;
  background-size:64px 64px;
  image-rendering:pixelated;
  opacity:.02;
}

/* --------------------------------------------------------------- the motes --
   When the reader stops, the frame exhales once.

   A FINITE burst, not a field. Nine motes fade up, drift for a few seconds,
   fade out, and genuinely END — animation-iteration-count is 1 and nothing
   re-arms them but the reader's next scroll. That distinction is the whole
   design, on measurement and on taste.

   On measurement: a standing field costs 28-48% of a GPU core plus 9-17% of a
   renderer core, continuously and forever, on a page that currently costs
   exactly zero when parked. There is no position in the stack that avoids it.
   A backdrop-filter's input is the DESTINATION FRAMEBUFFER inside the filtered
   element's bounds, not the animating layer's own raster — so any pixel change
   overlapping a glass rect re-runs that blur, including a compositor-only
   translate3d that never re-rasters, and including painting in front of .page.
   Depth in the stack was measured to make no difference. Duration is the only
   term in (damage area x frame rate x duration) that can be driven to zero.

   On taste: the drift is not the effect, the ENVELOPE is. Something that
   arrives and leaves is a gesture; something that drifts forever is a
   screensaver, which is exactly what the scene player was rejected for being.
   And it is continuous with this page's own philosophy — the cursor answers
   the hand MOVING, this answers the hand STOPPING. It is a response to the
   reader, not the page pretending to be alive on a clock.

   Upward, never downward: upward reads as air, downward reads as snow.
   Warm-neutral off-white, never --yellow; that colour appears exactly once in
   this UI, on the badge's border-inline-start.

   No will-change. An active transform/opacity animation promotes the element
   by itself — declaring it permanently is the mistake documented above. */
.motes{position:absolute;inset:0;pointer-events:none;opacity:1}
.mote{
  position:absolute;
  width:var(--s);height:var(--s);
  border-radius:50%;
  background:radial-gradient(circle,
    rgba(255,250,240,.9) 0%, rgba(255,250,240,.55) 45%, rgba(255,250,240,0) 68%);
  opacity:0;
  transform:translate3d(0,0,0);
}
.motes.on .mote{
  animation:mote var(--life) cubic-bezier(.33,0,.4,1) var(--delay) 1 both;
}
@keyframes mote{
  0%   {opacity:0;              transform:translate3d(0,0,0)}
  /* the envelope: ~20% in, ~24% out, held between */
  20%  {opacity:var(--peak);    transform:translate3d(calc(var(--dx) * .2), calc(var(--dy) * .2), 0)}
  76%  {opacity:var(--peak);    transform:translate3d(calc(var(--dx) * .76), calc(var(--dy) * .76), 0)}
  100% {opacity:0;              transform:translate3d(var(--dx), var(--dy), 0)}
}

/* The portal. Driven per frame by JS: during the zoom-through it is a
   clip-path circle opening inside the O of BOX; once it has swallowed the
   screen it becomes the resting 80% dim the sections after the story sit on,
   so the mark stays as a soft glow behind them. */
.film-dim{position:absolute;inset:-2px;background:var(--bg);opacity:0;will-change:opacity}

/* --------------------------------------------------- the loading hairline --
   240 frames per orientation is ~11.4 MB and the canvas does not take over
   until 30% of them have decoded. A hairline at the top edge fills toward that
   threshold, then fades out. Two pixels, one transform — no chrome, nothing to
   read, nothing that survives into the experience.

   The fill is white rather than --yellow on purpose: the yellow appears
   exactly once in this UI, on the badge's border-inline-start, and spending it
   on a transient loading bar would cost it that. */
.filmload{
  position:fixed;inset-block-start:0;inset-inline:0;z-index:60;
  height:2px;background:rgba(255,255,255,.07);
  opacity:0;transition:opacity .45s var(--ease);
  pointer-events:none;
}
.filmload.on{opacity:1}
.filmload > i{
  display:block;height:100%;
  background:rgba(255,255,255,.62);
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform .3s var(--ease);
  will-change:transform;
}
/* the bar fills from the side the reader's language starts on */
html[dir="rtl"] .filmload > i{transform-origin:100% 50%}

.page{position:relative;z-index:1}

/* ==========================================================================
   NAV
   ========================================================================== */

.nav{
  position:fixed;inset-inline:0;top:0;z-index:50;
  height:var(--nav-h);padding:0 var(--gut);
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  border-bottom:1px solid rgba(255,255,255,.12);
  transition:background .45s,border-color .45s;
}
.nav::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:rgba(7,7,26,.38);
  -webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);
  opacity:0;transition:opacity .45s;
}
.nav.scrolled::before{opacity:1}
.nav.scrolled{border-color:rgba(255,255,255,.16)}

.brand{display:inline-flex;align-items:center;gap:10px;text-decoration:none;color:var(--white)}
/* The real wordmark, not Inter faking one. Same treatment as .brand-mark —
   width is the only knob, height is auto off the img's own ratio, and the
   asset is the tight-cropped joybox-wordmark-tight.png (341x78) so there is
   no dead transparent margin throwing off the gap to the icon beside it. */
.brand-word{width:120px;height:auto;flex:none;filter:var(--shadow-mark-sm)}   /* 341/120 = 2.84x */
/* One source everywhere, one knob. joybox-mark.png is joybox-box.png with the
   baked-in "For Media" strip cropped off — a clean silhouette, and 21% more
   box in the same slot, because that text used to occupy the lower-left of the
   canvas while the artwork was forced to share it. The descriptor is not lost:
   .badge and the footer .label both already read "JoyBox for Media".
   Width is the only knob on the site — the source is 306px, so whatever width
   is set here IS the retina divisor. Height stays auto and the ratio comes
   from the img's width/height attributes, never a hardcoded aspect-ratio, so a
   future re-export at a different crop cannot be distorted by a stale number
   sitting in this file. Any stray height breaks that; there are none. */
.brand-mark{
  width:30px;height:auto;flex:none;   /* 306/30 = 10.2x */
  filter:var(--shadow-mark-sm);
}

.nav-links{
  position:absolute;left:50%;top:0;bottom:0;transform:translateX(-50%);
  display:flex;align-items:center;gap:40px;
}
.nav-links a{
  font-size:14px;color:rgba(255,255,255,.85);text-decoration:none;
  transition:color .3s;
}
.nav-links a:hover{color:var(--white)}
.nav-links sup{
  font-family:var(--sans);font-size:10px;font-weight:500;
  color:rgba(255,255,255,.55);margin-inline-start:4px;vertical-align:top;line-height:1;
}

/* ==========================================================================
   SCREENS — the three acts of the story, each one viewport over the film
   ========================================================================== */

/* --- the scroll-locked track ---------------------------------------------
   .story is the scroll runway; .story-stage pins one viewport of it for the
   whole run, so the three acts trade places in the same frame while the film
   scrubs underneath. The track's height IS the transition length — this is
   what makes the handovers scroll-locked rather than merely scrolled-past. */
.story{position:relative;height:var(--story-h)}
.story-stage{
  position:sticky;top:0;
  height:100vh;height:100svh;
  overflow:hidden;
}

.act{
  position:absolute;inset:0;
  display:flex;flex-direction:column;justify-content:space-between;gap:40px;
  padding:calc(var(--nav-h) + 40px) var(--gut) 64px;
}
/* will-change is applied by js/site.js only while an act is mid-handover, and
   removed once it settles. Declaring it here instead costs three permanent
   full-viewport compositor layers for a property that changes during 16% of
   the track — measured at ~2.3 ms a frame across the story, and a doubling of
   dropped frames. */
.act.moving{will-change:opacity,transform}
/* Acts are switched off with inline `visibility` by js/site.js — see
   styleAct(). Nothing to declare here beyond the stacking they share. */

.row{display:flex;justify-content:space-between;gap:48px}
.row-top{align-items:flex-start}
.row-bottom{align-items:flex-end}

.lead{max-width:40rem}
.lead .badge{margin-bottom:22px}
.lead .body{margin-top:24px}
.ctas{display:flex;flex-wrap:wrap;gap:12px;margin-top:32px}

.display{text-shadow:var(--shadow-text)}
.band .display{text-shadow:none}

/* --- act 1 --------------------------------------------------------------- */
.disciplines{display:flex;flex-direction:column;gap:10px}
.disciplines li{
  font-family:var(--sans);font-size:12px;font-weight:500;
  letter-spacing:.15em;text-transform:uppercase;
  color:rgba(255,255,255,.9);text-shadow:var(--shadow-text-sm);
}
html[lang="ar"] .disciplines li{font-family:var(--sans-ar);font-size:13.5px;letter-spacing:0;text-transform:none}

.card{
  display:flex;align-items:center;gap:16px;
  padding:12px;border-radius:16px;
  background:var(--g-bg-2);border:1px solid var(--g-line);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
  text-decoration:none;color:inherit;
  transition:background .3s,border-color .3s;
}
.card:hover{background:rgba(255,255,255,.19);border-color:var(--g-line-2)}
/* No inner tile. .card is already glass with its own fill, hairline and
   blur(14px); a second frosted plate 12px inside the first is a box in a box —
   and squaring it letterboxed the artwork into 84x84 so only 60px of mark ever
   drew. The tile was not decoration that helped, it was the letterboxing bug.
   Deleting it buys 61% more mark in a card whose height the copy column sets. */
.card-mark{width:96px;height:auto;flex:none;filter:var(--shadow-mark)}   /* 3.19x */
.card-body{display:flex;flex-direction:column;gap:6px;padding-inline-end:8px}
.card-t{font-size:14px;font-weight:500;color:var(--white)}
.card .btn{margin-top:4px;align-self:flex-start}

.cue{
  position:absolute;inset-inline:0;bottom:22px;z-index:2;
  display:flex;justify-content:center;align-items:center;gap:9px;
  color:rgba(255,255,255,.65);pointer-events:none;
}
.cue .label{font-size:10.5px;letter-spacing:.18em}
html[lang="ar"] .cue .label{font-size:12px;letter-spacing:0}
.cue-arrow{display:inline-block;font-size:12px;animation:jbcue 1.6s ease-in-out infinite}
@keyframes jbcue{0%,100%{transform:translateY(0);opacity:.55}50%{transform:translateY(5px);opacity:1}}

/* --- act 2: the frosted panel ------------------------------------------- */
.panel{
  width:100%;max-width:27rem;flex:none;
  padding:0 22px;border-radius:20px;
  background:var(--g-bg);border:1px solid var(--g-line);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
}
.panel li{display:flex;gap:20px;padding:20px 0;border-bottom:1px solid var(--g-line)}
.panel li:last-child{border-bottom:0}
.panel .n{font-family:var(--sans);font-size:11px;font-weight:500;letter-spacing:.15em;color:var(--text-3);padding-top:6px;flex:none}
.panel .t{display:flex;align-items:center;gap:8px;font-size:17px;font-weight:500;color:var(--white)}
/* Static, and static on purpose: these rows are copy, not links. A chevron
   that slides on hover promises a destination that does not exist. */
.panel .t .chev{color:rgba(255,255,255,.35)}
.panel .d{margin-top:6px;font-size:14px;line-height:1.6;color:var(--text-2)}
html[lang="ar"] .panel .d{line-height:1.85}

/* --- act 3 --------------------------------------------------------------- */
.act-3{justify-content:flex-end;padding-bottom:48px}

/* ==========================================================================
   BANDS — work / studio / contact float over the dimmed film
   ========================================================================== */

.band{padding:96px var(--gut)}
.band-head{display:grid;grid-template-columns:1fr auto;align-items:end;gap:18px 24px;margin-bottom:40px}
.band-head .badge{grid-column:1 / -1;justify-self:start}

/* --- work ---------------------------------------------------------------- */
.work{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
.work-card{
  display:block;overflow:hidden;border-radius:18px;
  background:rgba(255,255,255,.055);border:1px solid var(--g-line);
  transition:border-color .4s,background .4s;
}
.work-card:hover{border-color:var(--g-line-2);background:rgba(255,255,255,.09)}
.work-card .img{aspect-ratio:16/10;overflow:hidden}
.work-card img{width:100%;height:100%;object-fit:cover;transition:transform .9s var(--ease)}
.work-card.fit-topstart img{object-position:left top}
.work-card:hover img{transform:scale(1.04)}
.work-card .meta{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:16px 18px 18px}
.work-card .t{font-size:16px;font-weight:500;color:var(--white)}
.work-card .k{margin-top:5px;color:var(--text-3)}
.work-card .n{color:var(--text-3);font-family:var(--sans);font-size:11px;letter-spacing:.15em;flex:none}

/* --- studio -------------------------------------------------------------- */
.studio{display:grid;grid-template-columns:1.1fr .9fr;gap:56px;align-items:center}
.studio-copy .badge{margin-bottom:22px}
.stats{display:flex;flex-wrap:wrap;gap:44px}
.stats .v{font-family:var(--sans);font-weight:400;font-size:clamp(2rem,3vw,44px);line-height:1;letter-spacing:-.02em;color:var(--white)}
.stats .k{margin-top:8px;color:var(--text-3)}
.photoslot{
  min-height:320px;border-radius:22px;
  display:flex;align-items:center;justify-content:center;text-align:center;
  background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.05) 0 10px,rgba(255,255,255,.015) 10px 20px);
}

/* --- contact + footer ---------------------------------------------------- */
.contact{
  display:flex;align-items:center;justify-content:space-between;gap:40px;
  padding:44px 48px;border-radius:28px;
}
.contact-copy{flex:1}
.contact .display{margin-bottom:12px}
.contact .body{max-width:none}
/* The page's single largest brand moment. 306px of source puts the 2x wall at
   153px; 144 stops short of it on purpose — a hard-cut alpha edge has no
   antialiasing to hide a half-pixel sample if a browser rounds to a fractional
   device pixel. Width-driven, not height-driven, so the knob and the retina
   constraint are now literally the same number. */
.boxmark{width:144px;height:auto;flex:none;filter:var(--shadow-mark)}   /* 306/144 = 2.13x */

.foot{
  position:relative;z-index:1;
  margin-top:40px;padding:28px var(--gut);
  border-top:1px solid rgba(255,255,255,.12);
  display:flex;align-items:center;justify-content:space-between;gap:20px;
}
.foot .brand-mark{width:26px}   /* 11.8x */
.foot .brand-word{width:100px}   /* 341/100 = 3.41x — 100/120 mirrors the icon's 26/30 step */
.foot-links{display:flex;gap:24px}
.foot-links a{font-size:13px;color:var(--text-3);text-decoration:none;transition:color .3s}
.foot-links a:hover{color:var(--white)}

/* ==========================================================================
   TABLET
   ========================================================================== */
@media (max-width:1023px){
  .work{grid-template-columns:repeat(2,1fr)}
  .studio{grid-template-columns:1fr;gap:36px}
  .photoslot{min-height:240px}

  /* .contact is still a row here and still has to fit mark + copy + button in
     ~824px of inner width, so the mark steps down before the d-lg heading is
     forced to wrap awkwardly. Density goes up, not down. */
  .boxmark{width:128px}   /* 2.39x */
}

/* ==========================================================================
   MOBILE — the portrait cut of the film, everything stacked
   ========================================================================== */
@media (max-width:767px){
  :root{--gut:20px;--nav-h:58px}

  /* `.brand img{height:20px}` lived here — an element+class selector (0,1,1)
     outranking .brand-mark's (0,1,0), so it silently forced an 18x20 box the
     artwork could never fill, adding letterboxing on top of what desktop
     already had. With width the only knob and height auto, any stray height
     breaks the ratio outright, so it is deleted rather than reconciled.
     The mark keeps its desktop 30px: the bar is 10px shorter but .nav-links
     are gone, so there is more room across, not less. */
  .nav-links{display:none}
  .chip{min-height:34px;padding:0 12px;font-size:12.5px}

  /* the portrait cut of the opening frame — see .film */
  .film{background-image:url(../assets/frames24/port/f001.webp)}

  /* This re-declares the whole `background` shorthand, so it MUST carry the
     radial too or it silently deletes the light rig below 767px. Wider and
     shorter ellipse for the portrait crop, which is much taller. */
  .film-veil{background:
    radial-gradient(ellipse 92% 62% at 50% 46%,
      transparent 38%, rgba(7,7,26,.30) 100%),
    linear-gradient(180deg,
      rgba(7,7,26,.54) 0%, rgba(7,7,26,.22) 30%,
      rgba(7,7,26,.18) 55%, rgba(7,7,26,.74) 100%)}

  :root{--story-h:420vh}
  .act{padding:calc(var(--nav-h) + 28px) var(--gut) 56px;gap:32px}
  .row{flex-direction:column;align-items:stretch;gap:28px}
  .row-bottom{justify-content:flex-end}

  .intro{max-width:none;text-align:start;font-size:1rem}
  html[lang="ar"] .intro{line-height:1.75}
  .row > .badge{align-self:flex-start}
  .body{text-shadow:0 1px 4px rgba(0,0,0,.55),0 8px 24px rgba(0,0,0,.5)}
  .disciplines{gap:8px}
  .disciplines li{font-size:10.5px;letter-spacing:.14em}
  html[lang="ar"] .disciplines li{font-size:13px}

  .lead .badge{margin-bottom:16px}
  .lead .body{margin-top:18px}
  .ctas{margin-top:24px}
  .ctas .btn{flex:1}

  .card{align-self:stretch}
  .card-mark{width:84px}   /* 3.64x */

  .panel{max-width:none;padding:0 16px}
  .panel li{gap:14px;padding:16px 0}
  .panel .t{font-size:15.5px}
  .panel .d{font-size:13.5px}

  .act-3 .btn{align-self:stretch}

  .band{padding:64px var(--gut)}
  .band-head{grid-template-columns:1fr;gap:12px;margin-bottom:24px}

  /* work becomes a stack of thumb-sized rows */
  .work{display:flex;flex-direction:column;gap:12px}
  .work-card{display:flex;align-items:center;gap:14px;padding:10px;border-radius:14px}
  .work-card .img{width:96px;aspect-ratio:4/3;border-radius:8px;flex:none}
  .work-card .meta{flex:1;padding:0;gap:10px}
  .work-card .t{font-size:15px}
  .work-card .k{font-size:11px}
  html[lang="ar"] .work-card .k{font-size:12px}

  .stats{gap:26px}
  .photoslot{min-height:200px}

  .contact{flex-direction:column;align-items:flex-start;padding:28px 22px;border-radius:22px}
  .boxmark{width:120px}   /* 2.55x */
  .contact .btn{align-self:stretch}

  .foot{margin-top:16px;padding:22px var(--gut);flex-direction:column;align-items:flex-start;gap:14px}
}

/* ==========================================================================
   REDUCED MOTION
   The scrub stays — it is driven entirely by the reader and stops the moment
   they stop, so it is not the involuntary motion this preference suppresses.
   What goes: the entrance reveals, the cue bounce and the hover transitions.
   ========================================================================== */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  html.js .reveal{opacity:1;transform:none;transition:none}
  .cue-arrow{animation:none}
  /* Belt and braces: buildMotes() also returns early under this preference, so
     the elements never exist. This is here so that a mote which somehow did get
     built cannot animate. */
  .mote{animation:none;opacity:0}
  .btn,.chev,.card,.work-card,.work-card img,.chip,.nav,.nav::before{transition:none}
  .work-card:hover img{transform:none}
}
