/* ── Design tokens ────────────────────────────────────────────────── */
:root {
  --bg:            #080808;
  --surface:       rgba(10, 10, 10, 0.72);
  --surface-hi:    rgba(18, 18, 18, 0.80);
  --border:        rgba(255, 255, 255, 0.07);
  --border-hi:     rgba(255, 255, 255, 0.14);
  --text:          #ffffff;
  --text-dim:      rgba(255, 255, 255, 0.45);
  --text-faint:    rgba(255, 255, 255, 0.2);
  --accent:        #C8A96E;
  --accent-dim:    rgba(200, 169, 110, 0.12);
  --accent-glow:   rgba(200, 169, 110, 0.25);
  --fps-good:      #32D74B;
  --fps-warn:      #FF9F0A;
  --fps-bad:       #FF453A;
  --blur:          blur(24px) saturate(160%);
  --r:             14px;
  --r-sm:          9px;
  --r-pill:        100px;
  --ease:          cubic-bezier(0.25, 0.46, 0.45, 0.94);
  /* Item switch — two different windows, and the split is the point. The
     outgoing content leaves quickly so the swap underneath happens sooner;
     the incoming one arrives on the longer, softer curve. FADE_OUT_MS in
     app.js must match --fade-out: it is what times the swap. */
  --fade-out:      0.26s;
  --fade-in:       0.42s;
  --ease-out-fade: cubic-bezier(0.4, 0, 1, 1);      /* accelerating — leaves */
  --ease-in-fade:  cubic-bezier(0.16, 0.84, 0.44, 1); /* decelerating — arrives */
  --bottom-inset:  60px; /* space for OEM nav bar — adjust in Fully Kiosk */
  --carousel-w:    130px; /* width of the item carousel (left of the category dock) */
  --catdock-w:    178px;  /* width of the right-edge vertical category switcher */
  --info-w:        15%;   /* width of the left-edge info panel (the text box) */
  --info-x:        24px;  /* its inset from the left edge of the viewport */
  --header-base:   72px;  /* minimum height of the full-width top header */
  --strip-h:       104px; /* height of the bottom asset strip (multi-asset items only) */
  --logo-w:        140px; /* brand logo width — set in admin → Global Settings */
  --logo-h:        0px;   /* logo's rendered height — measured in app.js */
  /* The header grows to fit the logo rather than cropping it, so a wider logo
     actually renders wider. Everything below the header (carousel, category
     dock, HUD rail, PERF panel) is offset by this. */
  --header-h:      max(var(--header-base), calc(var(--logo-h) + 20px));

  /* ── Type faces — set in admin → Global settings ────────────────────
     --font-en  the full Latin stack (a chosen face in front of the native
                one, or just the native one when "System default" is picked)
     --font-ar  the Arabic face ALONE, no generic fallback, so it can be
                composed below without stranding Latin on the browser default
     --font-both  Arabic face first, Latin stack behind it. Anything that can
                hold either script — item names, descriptions, category labels
                — uses this: Arabic glyphs come from the Arabic face, and any
                Latin the face lacks falls through to the chosen Latin one.
     app.js overrides the first two on <html> from global-settings.php; these
     values are what a kiosk renders before that arrives (and forever, if it
     was never online). */
  --font-en:       -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
  --font-ar:       'Almarai';
  --font-both:     var(--font-ar), var(--font-en);

  /* ── Info-panel text sizes — set in admin → Global settings ─────────
     One per (title | description) × (Latin | Arabic), because the two
     scripts do not read at the same size: Arabic needs more height than
     Latin at the same nominal px. The values below are the DEFAULTS a
     kiosk renders at when nothing is configured — deliberately fluid, so
     an untouched installation still scales with the screen. applyTextSizes()
     in app.js replaces one only when an admin has set a number, and removes
     it again when they clear the field, which restores exactly these.
     An individual item may override any of the four; that lands as an inline
     font-size on the element and so outranks these. */
  --size-title-en: clamp(1.2rem, 2vw, 1.65rem);
  --size-title-ar: clamp(1.2rem, 2vw, 1.65rem);
  --size-desc-en:  0.8125rem;
  --size-desc-ar:  0.8125rem;

  /* ── Home-screen scale ─────────────────────────────────────────────
     One unit of the home screen's design grid, as a length. The attract
     view was drawn at 1280×720 and every size on it is written as a
     multiple of this, so a 4K wall panel or a 55" 1080p display gets the
     same composition at the size that screen deserves instead of a
     1280-sized island marooned in the middle of it.

     The factor is min(width, height)-driven on purpose: the home screen is
     a full-height stack (hero + headline + subtitle + video card, clear of
     the category rail), so a wide but short screen must not scale past what
     its height can hold.

     It scales *down* as well as up, and that direction matters as much as the
     other one: a window shorter than the design — a laptop at 1600×620, a
     display in a 4:3 cabinet — used to be floored at the full 1280×720 size and
     drew a composition taller than the room it had, which is what put the
     welcome copy underneath the category rail. The 0.62 floor is the point past
     which the ≤900px block at the end of this file takes over with a layout of
     its own; the 3 ceiling is exactly 4K (1280×3 = 3840), so a wall panel gets
     the composition at its own size rather than a 1280-sized island in the
     middle of itself.

     0.078125vw = 100/1280.  0.13889vh = 100/720. */
  --hu:     clamp(0.62px, min(0.078125vw, 0.13889vh), 3px);

  /* A damped unit for the bottom category rail. Every category is on screen
     at once with nothing to scroll, so the cards cannot take the full factor
     without a collection of seven or eight of them wrapping into a second
     row — which would climb into the welcome copy. Their widths are
     additionally fitted to the count in .home-cat below.

     The damping applies to *growth only* — the min() is what makes that true.
     Above the design size the second term is the smaller one and the cards
     scale at 0.65 of the screen's factor; below it the damping would work the
     wrong way round, holding the cards near their drawn size on exactly the
     short screen that has no room for them, so --hu itself wins there and the
     rail shrinks with everything else. */
  --hu-cat: min(var(--hu), calc(1px + (var(--hu) - 1px) * 0.65));

  /* The home search box lives in the top bar, so it scales with the screen
     but never grows taller than the header that holds it. Everything inside
     it is sized off this rather than off --hu, so the box and its contents
     stay in proportion whichever of the two terms wins. */
  --home-search-h: min(calc(48 * var(--hu)), calc(var(--header-h) - 12px));
}

/* ── Reset / base ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  font-family: var(--font-en);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

/* Kiosk hardening */
body {
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
  touch-action: none;
}

/* Prevent context menu & image save */
body { -webkit-touch-callout: none; }

/* ── Ambient background glow ──────────────────────────────────────── */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  background: radial-gradient(ellipse 70% 55% at 50% 48%, rgba(200,169,110,0.045) 0%, transparent 70%);
  pointer-events: none;
  z-index: 0;
}

/* ── 3-D Viewport ─────────────────────────────────────────────────── */
#viewer {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  background-color: var(--bg);
  --mv-background-color: #080808;
  z-index: 0;
}

/* ── Image / Video viewers ────────────────────────────────────────── */
#media-image,
#media-video {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--bg);
  z-index: 0;
  display: none;
}

/* Photos are zoomable — the rail buttons, the wheel and pinch gestures all write
   one transform here (wireImageZoom() in app.js). `touch-action: none` keeps the
   two-finger gesture ours; the origin must stay centred, since the pan clamp and
   the pinch anchor are both computed around the element's centre. */
#media-image {
  touch-action: none;
  transform-origin: 50% 50%;
}
#media-image.zoomed { cursor: grab; }
#media-image.zoomed:active { cursor: grabbing; }
/* Only the buttons animate the transform — a gesture has to track the finger.
   app.js adds this class for a button press and strips it on pointerdown. */
#media-image.zoom-anim { transition: opacity 0.4s var(--ease), transform 0.22s var(--ease); }

/* ── Loading overlay ──────────────────────────────────────────────── */
.loading-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s var(--ease);
  z-index: 2;
}
.loading-overlay.visible { opacity: 1; }

.ring-spinner {
  width: 44px;
  height: 44px;
  border: 2px solid rgba(255,255,255,0.08);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.75s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── First-run init overlay ───────────────────────────────────────── */
.init-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transition: opacity 0.5s var(--ease);
}
.init-overlay[hidden] { display: none; }
.init-overlay.fading  { opacity: 0; pointer-events: none; }

.init-box {
  width: min(480px, 82vw);
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.init-eyebrow {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  color: var(--text-faint);
}

.init-bar-track {
  height: 2px;
  background: rgba(255, 255, 255, 0.07);
  border-radius: 1px;
  overflow: hidden;
}

.init-bar-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 1px;
  transition: width 0.12s linear;
}

.init-status-row {
  display: flex;
  align-items: baseline;
  gap: 14px;
}

.init-percent {
  font-size: 2rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
  line-height: 1;
  letter-spacing: -0.02em;
}

.init-file {
  font-size: 0.7rem;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  letter-spacing: 0.04em;
}

/* ── Glass mixin (applied via class) ─────────────────────────────── */
.glass {
  background: var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border);
}

/* ── Top bar ──────────────────────────────────────────────────────────
   Full-width fixed header spanning the whole viewport: language toggle +
   PERF chip on the left, brand logo on the right. Transparent — the chips
   carry their own glass, so the 3-D stage shows through. It sits above the
   carousel/category rails, which start below it (top: var(--header-h)). */
.top-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-h);
  z-index: 30;
  pointer-events: none;   /* re-enabled on the interactive children */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 24px;
}

.top-bar-left {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

/* ── Brand logo (right side of the header) ───────────────────────────
   Width is driven by --logo-w, set from the global admin settings; the
   height follows the image's aspect ratio and the header grows to match
   (see --header-h above). Deliberately NO max-height: capping the height
   would letterbox the image inside its box, so widening it would stop
   having any visible effect. */
.brand-logo {
  flex: 0 0 auto;
  width: var(--logo-w);
  height: auto;
  object-fit: contain;
  object-position: right center;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Language toggle (EN / ع) */
.lang-toggle {
  pointer-events: all;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px;
  background: var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
.lang-opt {
  pointer-events: all;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  height: 36px;
  padding: 0 14px;
  background: transparent;
  border: none;
  border-radius: var(--r-pill);
  color: var(--text-dim);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s var(--ease), background 0.2s var(--ease);
}
.lang-opt:hover { color: var(--text); }
.lang-opt.active {
  color: var(--bg, #080808);
  background: var(--accent);
}
.lang-opt[data-lang="ar"] {
  font-family: var(--font-both);
  font-size: 1.25rem;
  letter-spacing: 0;
  padding-bottom: 3px;
}

/* Performance toggle chip */
.perf-chip {
  pointer-events: all;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  background: var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--text-dim);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease);
}
.perf-chip:hover,
.perf-chip[aria-expanded="true"] {
  color: var(--text);
  border-color: var(--border-hi);
  background: var(--surface-hi);
}
.perf-chip[aria-expanded="true"] { color: var(--accent); }
/* `display: flex` above overrides the UA [hidden] rule, so hiding the chip
   via its `hidden` attribute needs this more-specific override. */
.perf-chip[hidden] { display: none; }

/* ── Info side panel (glass) ──────────────────────────────────────── */
.info-panel {
  position: fixed;
  left: var(--info-x);
  top: 50%;
  transform: translateY(-50%);
  width: var(--info-w);
  z-index: 10;
  pointer-events: none;
  border-radius: 22px;
  overflow: hidden;

  background: linear-gradient(
    145deg,
    rgba(10, 10, 10, 0.88) 0%,
    rgba(8, 8, 8, 0.82) 50%,
    rgba(18, 14, 6, 0.86) 100%
  );
  backdrop-filter: blur(44px) saturate(200%);
  -webkit-backdrop-filter: blur(44px) saturate(200%);
  border: 1px solid rgba(255, 255, 255, 0.11);
  box-shadow:
    0 28px 72px rgba(0, 0, 0, 0.6),
    0 6px 20px rgba(0, 0, 0, 0.35),
    0 0 0 0.5px rgba(200, 169, 110, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.2),
    inset 0 0 70px rgba(200, 169, 110, 0.03);

  animation: infoPanelIn 0.55s var(--ease) both;
  transition: top 0.4s var(--ease), bottom 0.4s var(--ease), transform 0.4s var(--ease);
}

/* Top shimmer line */
.info-panel::before {
  content: '';
  position: absolute;
  top: 0;
  left: 8%;
  right: 8%;
  height: 1.5px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(200, 169, 110, 0.5) 25%,
    rgba(220, 195, 140, 1) 50%,
    rgba(200, 169, 110, 0.5) 75%,
    transparent 100%
  );
  z-index: 2;
}

/* Inner ambient glow */
.info-panel::after {
  content: '';
  position: absolute;
  top: -40%;
  left: -20%;
  width: 140%;
  height: 80%;
  background: radial-gradient(
    ellipse at 45% 65%,
    rgba(200, 169, 110, 0.09) 0%,
    transparent 65%
  );
  pointer-events: none;
  z-index: 0;
}

@keyframes infoPanelIn {
  from { opacity: 0; transform: translateY(-50%) translateX(-20px); }
  to   { opacity: 1; transform: translateY(-50%) translateX(0); }
}

/* Left accent stripe */
.info-panel-accent {
  position: absolute;
  top: 22px;
  bottom: 22px;
  left: 0;
  width: 2px;
  background: linear-gradient(
    180deg,
    transparent 0%,
    rgba(200, 169, 110, 0.8) 20%,
    var(--accent) 50%,
    rgba(200, 169, 110, 0.8) 80%,
    transparent 100%
  );
  border-radius: 0 2px 2px 0;
  z-index: 2;
}

.info-panel-content {
  position: relative;
  z-index: 1;
  padding: 22px 20px 22px 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-family: var(--font-both);
}

/* ── Model identity ───────────────────────────────────────────────── */
.model-tag {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  background: rgba(200, 169, 110, 0.12);
  border: 1px solid rgba(200, 169, 110, 0.22);
  border-radius: var(--r-pill);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--accent);
  align-self: flex-start;
  font-family: var(--font-en);
}

/* Category pill on the leading edge, QR code pinned to the trailing edge.
   Flips automatically with the panel's dir (LTR ⇄ RTL). */
.tag-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  min-height: 22px;
  flex: 0 0 auto;
}

/* ── Item QR code ─────────────────────────────────────────────────── */
.info-qr {
  flex-shrink: 0;
  margin-top: -2px;
  padding: 7px;
  border-radius: 12px;
  background: linear-gradient(150deg, rgba(22, 18, 9, 0.92) 0%, rgba(12, 11, 8, 0.92) 100%);
  border: 1px solid rgba(200, 169, 110, 0.28);
  box-shadow:
    0 4px 14px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 0 16px rgba(200, 169, 110, 0.08);
  line-height: 0;
  animation: infoQrIn 0.45s var(--ease) both;
}
.info-qr[hidden] { display: none; }

.info-qr-canvas {
  display: block;
  width: 72px;
  height: 72px;
  /* crisp module edges — QR is rendered to a small backing store */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

@keyframes infoQrIn {
  from { opacity: 0; transform: scale(0.9); }
  to   { opacity: 1; transform: scale(1); }
}

/* The Latin size is the rule, the Arabic one an override keyed on the script
   actually on screen — showIdentity() sets data-script from the same slot the
   type face is chosen by, so an item with no Arabic title (which renders its
   Latin one) is sized as Latin too. */
.model-name {
  font-family: var(--font-both);
  font-size: var(--size-title-en);
  font-weight: 800;
  letter-spacing: 0;
  line-height: 1.45;
  /* Title sits on its own full-width row; align to the reading direction
     (left for LTR / right for RTL) instead of justified. */
  text-align: start;
  width: 100%;
  /* gradient shimmer: bright white fading to warm gold */
  background: linear-gradient(160deg, #ffffff 15%, rgba(230, 210, 170, 0.92) 65%, rgba(200, 169, 110, 0.8) 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  transition: opacity 0.25s var(--ease);
  max-width: 100%;
  white-space: normal;
  overflow: visible;
  /* Stay full height in the flex column so the scrollable description
     can never ride up and overlap the title when expanded. */
  flex: 0 0 auto;
}
.model-name[data-script="ar"] { font-size: var(--size-title-ar); }
.model-name.fading { opacity: 0; }


.model-desc {
  font-family: var(--font-both);
  font-size: var(--size-desc-en);
  font-weight: 300;
  line-height: 1.9;
  color: rgba(255, 255, 255, 0.52);
  text-align: justify;
  max-width: 100%;
  /* Height-based clamp (~5 lines) — robust with rich block content
     such as lists and paragraphs, unlike -webkit-line-clamp. */
  display: block;
  max-height: calc(1.9em * 5 + 11px);
  overflow: hidden;
  transition: opacity 0.25s var(--ease);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  padding-top: 11px;
  margin-top: 2px;
}
.model-desc[data-script="ar"] { font-size: var(--size-desc-ar); }
.model-desc[hidden] { display: none; }
.model-desc.fading  { opacity: 0; }

/* Rich-text content authored in the admin WYSIWYG editor. The global
   reset zeroes list/paragraph spacing, so restore it here. */
.model-desc strong, .model-desc b { font-weight: 700; color: rgba(255, 255, 255, 0.72); }
.model-desc em, .model-desc i     { font-style: italic; }
.model-desc u                      { text-decoration: underline; }
.model-desc a {
  color: var(--accent);
  text-decoration: underline;
  pointer-events: auto;
}
.model-desc p { margin: 0 0 0.6em; }
.model-desc p:last-child { margin-bottom: 0; }
/* <div> lines come from pressing Enter in the editor — treat as a plain
   line break (block, no paragraph gap). */
.model-desc div { margin: 0; }
.model-desc ul, .model-desc ol { margin: 0.3em 0 0.6em; padding-inline-start: 1.3em; }
.model-desc ul { list-style: disc; }
.model-desc ol { list-style: decimal; }
.model-desc li { margin: 0.15em 0; }
.model-desc > :first-child { margin-top: 0; }

/* ── Expand / collapse arrow ──────────────────────────────────────── */
/* Hidden by default; JS adds .has-overflow to the panel when the
   description is taller than its collapsed (5-line) box.              */
.info-expand-btn {
  display: none;
  align-self: flex-start;          /* follows text direction (left LTR / right RTL) */
  align-items: center;
  justify-content: center;
  padding: 6px 16px;
  margin-top: 4px;
  border: 1px solid rgba(200, 169, 110, 0.32);
  background: rgba(200, 169, 110, 0.12);
  border-radius: var(--r-pill);
  color: var(--accent);
  cursor: pointer;
  pointer-events: auto;            /* clickable even though panel is pass-through */
  font-family: var(--font-both);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.info-expand-btn:hover {
  background: rgba(200, 169, 110, 0.2);
  border-color: rgba(200, 169, 110, 0.45);
}
.info-expand-btn[hidden] { display: none; }

.info-panel.has-overflow .info-expand-btn { display: inline-flex; }

/* ── Expanded panel: full-height, scrollable, same width ──────────── */
/* Starts below the fixed header rather than at the top of the viewport — the
   header is above it in the stack and the panel must not run under it. */
.info-panel.expanded {
  top: calc(var(--header-h) + 12px);
  bottom: 16px;
  transform: none;                 /* drop the vertical centring */
  display: flex;
  flex-direction: column;
  animation: none;                 /* don't replay the slide-in */
}
.info-panel.expanded .info-panel-content {
  flex: 1 1 auto;
  min-height: 0;                   /* let the desc shrink so it can scroll */
}
.info-panel.expanded .model-desc {
  display: block;
  max-height: none;                /* drop the 5-line clamp */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  pointer-events: auto;            /* allow scrolling the text */
  overscroll-behavior: contain;
  /* Keep the text off the scrollbar on both LTR and RTL. */
  padding-right: 14px;
  scrollbar-gutter: stable;
}
.info-panel.expanded .model-desc[dir="rtl"],
.info-panel.expanded .model-desc:dir(rtl) {
  padding-right: 0;
  padding-left: 14px;
}
/* Slimmer scrollbar inside the expanded description. */
.info-panel.expanded .model-desc::-webkit-scrollbar { width: 5px; }
.info-panel.expanded .model-desc::-webkit-scrollbar-thumb {
  background: rgba(200, 169, 110, 0.35);
  border-radius: 3px;
}

/* ── Info panel collapse tab ──────────────────────────────────────────
   The same control as the carousel's (.carousel-collapse-btn), mirrored onto
   the leading edge: a glass tab riding the panel's right edge, and sitting at
   the viewport edge once the panel has slid away. The panel is moved with
   `left` rather than a transform, because its transform is already spoken for
   — the vertical centring, and the asset-strip offset that adjusts it. */
.info-panel.panel-hidden {
  left: calc(-1 * (var(--info-w) + var(--info-x) + 40px));
  opacity: 0;
  pointer-events: none;
}

.info-collapse-btn {
  position: fixed;
  left: calc(var(--info-w) + var(--info-x));
  top: 50%;
  transform: translateY(-50%);
  z-index: 11;
  width: 22px;
  height: 52px;
  padding: 0;
  background: linear-gradient(180deg,
    rgba(10, 10, 10, 0.88) 0%,
    rgba(8, 8, 8, 0.83)    55%,
    rgba(5, 5, 5, 0.90)    100%);
  backdrop-filter: blur(32px) saturate(200%);
  -webkit-backdrop-filter: blur(32px) saturate(200%);
  border: 1px solid rgba(255,255,255,0.09);
  border-left: none;
  border-radius: 0 10px 10px 0;
  box-shadow: 4px 0 16px rgba(0,0,0,0.4);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,0.45);
  transition: left 0.38s var(--ease), background 0.2s, color 0.2s;
}
.info-collapse-btn:hover {
  background: rgba(255,255,255,0.12);
  color: rgba(255,255,255,0.95);
}
.info-collapse-btn svg {
  transition: transform 0.32s var(--ease);
  /* panel visible → chevron points left (‹), the way it will go */
}
.info-collapse-btn.bar-hidden {
  left: 0;
}
.info-collapse-btn.bar-hidden svg {
  transform: rotate(180deg);   /* panel hidden → chevron points right (›) */
}

/* ── Performance panel ────────────────────────────────────────────── */
.perf-panel {
  position: fixed;
  top: calc(var(--header-h) + 8px);
  left: 24px;
  width: 248px;
  background: var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 16px;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 14px;
  animation: slideDown 0.22s var(--ease);
}
.perf-panel[hidden] { display: none; }

@keyframes slideDown {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.perf-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.panel-eyebrow {
  font-size: 0.5625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--text-dim);
}

.icon-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  border-radius: 6px;
  transition: color 0.15s, background 0.15s;
}
.icon-btn:hover { color: var(--text); background: rgba(255,255,255,0.06); }

/* FPS block */
.fps-block { display: flex; flex-direction: column; gap: 6px; }
.fps-label-row { display: flex; align-items: center; }

.fps-main {
  display: flex;
  align-items: baseline;
  gap: 12px;
}

.fps-big {
  font-size: 2.5rem;
  font-weight: 700;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text);
  transition: color 0.3s;
  min-width: 3ch;
}
.fps-big.good  { color: var(--fps-good); }
.stat-val.good { color: var(--fps-good); }
.stat-val.warn { color: var(--fps-warn); }
.stat-val.bad  { color: var(--fps-bad); }
.fps-big.warn  { color: var(--fps-warn); }
.fps-big.bad   { color: var(--fps-bad); }

.fps-subs {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.fps-sub-item {
  display: flex;
  align-items: baseline;
  gap: 4px;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.fps-sub-label {
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  color: var(--text-faint);
  min-width: 2ch;
}

/* Perf stats */
.perf-stat-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.perf-stat {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.stat-val {
  font-size: 0.8125rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.stat-val.mono {
  font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
  font-size: 0.75rem;
}

/* Eyebrow / label style */
.eyebrow {
  font-size: 0.5625rem;
  font-weight: 600;
  letter-spacing: 0.13em;
  color: var(--text-faint);
  text-transform: uppercase;
}

.perf-divider {
  height: 1px;
  background: var(--border);
}

/* Auto mode toggle */
.auto-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  color: var(--text-dim);
  width: 100%;
  transition: color 0.2s;
}
.auto-btn:hover { color: var(--text); }
.auto-btn[aria-checked="true"] { color: var(--accent); }

.auto-icon { flex-shrink: 0; }
.auto-label {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  flex: 1;
  text-align: left;
}

.pill {
  position: relative;
  width: 36px;
  height: 20px;
  border-radius: 10px;
  background: rgba(255,255,255,0.12);
  flex-shrink: 0;
  transition: background 0.25s var(--ease);
}
.auto-btn[aria-checked="true"] .pill { background: var(--accent); }

.pill-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  background: white;
  border-radius: 50%;
  transition: transform 0.25s var(--ease);
  box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.auto-btn[aria-checked="true"] .pill-thumb { transform: translateX(16px); }

/* ── Clear cache button ───────────────────────────────────────────── */
.clear-cache-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 0;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  cursor: pointer;
  transition: color 0.2s var(--ease);
}
.clear-cache-btn:hover { color: var(--accent); }
.clear-cache-btn span { flex: 1; text-align: left; }

/* ── Bottom-right HUD rail ────────────────────────────────────────── */
.hud-rail {
  position: fixed;
  bottom: calc(var(--bottom-inset) + 12px);  /* clear the OEM nav bar */
  right: calc(var(--carousel-w) + var(--catdock-w) + 24px);  /* sit left of the category dock + carousel */
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  z-index: 10;
  transition: bottom 0.38s var(--ease);
}
/* Multi-asset items show the bottom strip — float the rail above it. */
body.has-asset-strip .hud-rail {
  bottom: calc(var(--strip-h) + 12px);
}

.hud-btn {
  width: 60px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border);
  border-radius: 50%;
  color: var(--text-dim);
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease), transform 0.15s var(--ease);
}
.hud-btn:hover {
  color: var(--text);
  border-color: var(--border-hi);
  background: var(--surface-hi);
  transform: scale(1.06);
}
.hud-btn:active { transform: scale(0.96); }

/* `.hud-btn` sets display:flex and the global `svg { display: block }` below
   both outrank the UA stylesheet's [hidden] rule — without these, hiding the
   animation button or swapping its play/pause icon silently does nothing. */
.hud-btn[hidden],
.hud-btn svg[hidden] { display: none; }

.hud-btn--accent { color: var(--accent); border-color: rgba(200,169,110,0.2); }
.hud-btn--accent:hover { border-color: var(--accent); background: var(--accent-dim); }

.hud-sep {
  width: 1px;
  height: 36px;
  background: var(--border);
}

/* ── Video transport (inside the HUD rail) ────────────────────────────── */
/* Same glass pill as the round buttons, stretched to hold a scrub track and a
   clock. Kept `direction: ltr` so time runs left-to-right in Arabic too — this
   is a timeline, not text. */
.hud-seek {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 60px;
  padding: 0 20px;
  direction: ltr;
  background: var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border);
  border-radius: 30px;
}
/* `display: flex` above outranks the UA [hidden] rule — same trap as .hud-btn. */
.hud-seek[hidden] { display: none; }

/* Generous hit area around a thin track: the visible bar is 4px, the finger
   target is the full 60px height of the rail. */
.hud-seek-track {
  position: relative;
  width: clamp(120px, 16vw, 240px);
  height: 60px;
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;
}
.hud-seek-track::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: 4px;
  border-radius: 2px;
  background: rgba(255,255,255,0.13);
}
.hud-seek-fill {
  position: absolute;
  left: 0;
  width: 0;
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
}
.hud-seek-knob {
  position: absolute;
  left: 0;
  width: 13px;
  height: 13px;
  margin-left: -6.5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px rgba(200,169,110,0.16);
  transition: transform 0.15s var(--ease);
}
.hud-seek-track:hover .hud-seek-knob,
.hud-seek-track.scrubbing .hud-seek-knob { transform: scale(1.25); }

.hud-seek-time {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ── Category dock background — vertical glass panel ─────────────────── */
/* Separate element so the glass/blur stays put while the scroll container  */
/* (overflow-y: auto) scrolls the tags over it.                             */
.category-filter-bg {
  position: fixed;
  top: var(--header-h);            /* start below the full-width header */
  bottom: var(--bottom-inset);
  right: 0;                        /* flush with the right edge of the viewport */
  width: var(--catdock-w);
  z-index: 9;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(10, 10, 10, 0.86) 0%,
    rgba(7, 7, 7, 0.80)    55%,
    rgba(5, 5, 5, 0.88)    100%);
  backdrop-filter: blur(36px) saturate(210%);
  -webkit-backdrop-filter: blur(36px) saturate(210%);
  border-left: 1px solid rgba(255, 255, 255, 0.10);
  /* fade the panel's top and bottom edges into the viewport */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, black 7%, black 93%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%, black 7%, black 93%, transparent 100%);
  transition: transform 0.38s var(--ease), opacity 0.32s var(--ease);
}
.category-filter-bg.bar-hidden {
  transform: translateX(100%);
  opacity: 0;
}

/* ── Category dock scroll container — transparent, sits above bg ─────── */
.category-filter {
  position: fixed;
  top: var(--header-h);
  bottom: var(--bottom-inset);
  right: 0;
  width: var(--catdock-w);
  display: flex;
  flex-direction: column;
  align-items: center;
  z-index: 10;
  padding: 0;
  background: transparent;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%,
    black 7%,
    black 93%,
    transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%,
    black 7%,
    black 93%,
    transparent 100%);
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: none;
  touch-action: none;
  transition: transform 0.38s var(--ease), opacity 0.32s var(--ease);
}
.category-filter::-webkit-scrollbar { display: none; }
.category-filter:empty { display: none; }
.category-filter.bar-hidden {
  transform: translateX(100%);
  opacity: 0;
  pointer-events: none;
}

.cat-spacer {
  flex-shrink: 0;
  width: 100%;
  pointer-events: none;
}

/* Each category is a glass card. Colour is opt-in per category (set in the
   admin's category settings) and arrives as `--cat-rgb: r, g, b`, from which
   every tint below is derived. The saturated colour is confined to the accent
   edge — the card body stays a low-alpha wash so a shelf of eight categories
   never turns into a paint chart. */
.filter-tag {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: calc(100% - 24px);
  min-height: 58px;
  margin: 6px 0;
  padding: 13px 22px 13px 16px;   /* extra inline-end room for the accent edge */
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.045);
  backdrop-filter: blur(16px) saturate(150%);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  color: rgba(255, 255, 255, 0.72);
  font-size: 0.92rem;
  font-weight: 700;
  letter-spacing: 0;
  cursor: pointer;
  white-space: normal;
  text-align: right;
  line-height: 1.25;
  word-break: break-word;
  flex-shrink: 0;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
  transition: color 0.25s var(--ease), background 0.25s var(--ease),
              border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
  transform-origin: center center;  /* magnify in place within the vertical dock */
  will-change: transform, opacity;
}
.filter-tag-label { display: block; }

/* Accent edge — the one saturated element, and the only thing that carries the
   category's colour at full strength. Neutral categories get a faint white
   rule so the card still reads as a card.

   It is drawn as a rounded box carrying *only* a right border, so the stroke
   sweeps around the card's top-right and bottom-right corners and tapers out
   where it meets the (zero-width) top and bottom borders — the edge follows the
   card's curve instead of cutting a straight bar across it. `inset: -1px` puts
   the overlay on the border box exactly, so `border-radius: inherit` lines the
   arc up with the card's own outer radius at any size. */
.filter-tag::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  border-right: 5px solid rgba(255, 255, 255, 0.16);
  pointer-events: none;
  transition: border-color 0.25s var(--ease);
}
.filter-tag.has-cat-color::after {
  border-right-color: rgb(var(--cat-rgb));
}
.filter-tag.has-cat-color {
  background:
    linear-gradient(270deg,
      rgba(var(--cat-rgb), 0.22) 0%,
      rgba(var(--cat-rgb), 0.07) 62%,
      rgba(255, 255, 255, 0.035) 100%);
  border-color: rgba(var(--cat-rgb), 0.24);
}

.filter-tag.active {
  color: #fff;
  border-color: rgba(255, 255, 255, 0.20);
  background: rgba(255, 255, 255, 0.09);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
}
/* Active: the arc thickens rather than glowing — a box-shadow here would halo
   the whole rounded rect, not just the edge that carries the colour. */
.filter-tag.active::after {
  border-right-width: 6px;
  border-right-color: rgba(255, 255, 255, 0.34);
}
.filter-tag.active.has-cat-color {
  background:
    linear-gradient(270deg,
      rgba(var(--cat-rgb), 0.30) 0%,
      rgba(var(--cat-rgb), 0.11) 62%,
      rgba(255, 255, 255, 0.05) 100%);
  border-color: rgba(var(--cat-rgb), 0.42);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45),
              0 0 0 1px rgba(var(--cat-rgb), 0.12);
}
.filter-tag.active.has-cat-color::after {
  border-right-color: rgb(var(--cat-rgb));
}
/* ── Asset strip ──────────────────────────────────────────────────────── */
/* Horizontal twin of the category dock: same glass-panel-behind-a-scroller     */
/* structure, same magnification wave (see wireDock in app.js), rotated 90°.    */
/* Spans the viewport width minus the two right-edge docks so it stays centred  */
/* under the object rather than under the whole window.                          */
.asset-strip-bg,
.asset-strip {
  position: fixed;
  bottom: 0;                /* flush with the bottom edge of the page */
  left: 0;
  right: calc(var(--carousel-w) + var(--catdock-w));
  height: var(--strip-h);
  transition: transform 0.38s var(--ease), opacity 0.32s var(--ease);
}

.asset-strip-bg {
  z-index: 9;
  pointer-events: none;
  background: linear-gradient(0deg,
    rgba(10, 10, 10, 0.88) 0%,
    rgba(7, 7, 7, 0.78)    60%,
    rgba(5, 5, 5, 0.00)    100%);
  backdrop-filter: blur(30px) saturate(200%);
  -webkit-backdrop-filter: blur(30px) saturate(200%);
  /* fade the panel's left and right edges into the viewport */
  -webkit-mask-image: linear-gradient(to right,
    transparent 0%, black 10%, black 90%, transparent 100%);
  mask-image: linear-gradient(to right,
    transparent 0%, black 10%, black 90%, transparent 100%);
}
.asset-strip-bg.bar-hidden {
  transform: translateY(100%);
  opacity: 0;
}

.asset-strip {
  display: flex;
  flex-direction: row;
  align-items: center;
  z-index: 10;
  background: transparent;
  -webkit-mask-image: linear-gradient(to right,
    transparent 0%, black 10%, black 90%, transparent 100%);
  mask-image: linear-gradient(to right,
    transparent 0%, black 10%, black 90%, transparent 100%);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  touch-action: none;
}
.asset-strip::-webkit-scrollbar { display: none; }
.asset-strip[hidden] { display: none; }
.asset-strip.bar-hidden {
  transform: translateY(100%);
  opacity: 0;
  pointer-events: none;
}

.asset-spacer {
  flex-shrink: 0;
  height: 100%;
  pointer-events: none;
}

.asset-tile {
  flex-shrink: 0;
  width: 78px;
  height: 58px;
  margin: 0 7px;
  padding: 0;
  border: 1px solid var(--border-hi);
  border-radius: var(--r-sm);
  background: #111;
  overflow: hidden;
  cursor: pointer;
  transform-origin: center center;   /* magnify in place within the strip */
  will-change: transform, opacity;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
/* `contain` so a square GLB render or a tall photo keeps its proportions in
   the 78×58 tile — cropping to fill would slice the subject. */
.asset-tile img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  pointer-events: none;
}
.asset-tile[aria-selected="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-glow), 0 6px 18px rgba(0, 0, 0, 0.55);
}

/* Count pip on carousel cards for items carrying more than one asset. */
.card-asset-count {
  position: absolute;
  right: 3px;
  bottom: 3px;
  min-width: 15px;
  padding: 0 4px;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0.72);
  color: var(--accent);
  font-size: 0.58rem;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  pointer-events: none;
}

/* Re-centre the info panel in the space left above the strip, so a tall
   description never runs underneath it. The panel is centred with
   translateY(-50%), so half the strip height is the exact offset. */
body.has-asset-strip .info-panel {
  transform: translateY(calc(-50% - var(--strip-h) / 2));
}
/* The expanded panel drops the centring entirely and grows to fill the height,
   so it needs a floor rather than an offset. Must out-specify the rule above. */
body.has-asset-strip .info-panel.expanded {
  transform: none;
  bottom: calc(var(--strip-h) + 12px);
}

/* ── Carousel ─────────────────────────────────────────────────────── */
.carousel {
  position: fixed;
  right: var(--catdock-w);   /* sit immediately to the left of the category dock */
  top: var(--header-h);
  bottom: var(--bottom-inset);
  width: var(--carousel-w);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  background: linear-gradient(180deg,
    rgba(10, 10, 10, 0.88) 0%,
    rgba(8, 8, 8, 0.83)    55%,
    rgba(5, 5, 5, 0.90)    100%);
  backdrop-filter: blur(32px) saturate(200%);
  -webkit-backdrop-filter: blur(32px) saturate(200%);
  border-left: 1px solid rgba(255,255,255,0.09);
  border-radius: 18px 0 0 18px;
  box-shadow:
    -12px 0 48px rgba(0,0,0,0.55),
    -3px  0 12px rgba(0,0,0,0.3),
    inset 1px  0 0 rgba(255,255,255,0.09),
    inset -1px 0 0 rgba(0,0,0,0.2);
  /* infinity edges — content fades at top and bottom */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent  0%,
    black        9%,
    black        91%,
    transparent  100%);
  mask-image: linear-gradient(to bottom,
    transparent  0%,
    black        9%,
    black        91%,
    transparent  100%);
  z-index: 10;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  touch-action: none;             /* scroll handled in JS (wheel + pointer drag) */
  scroll-snap-type: y proximity;
  scroll-behavior: smooth;
  scrollbar-width: none;         /* Firefox */
  -webkit-overflow-scrolling: touch;
  transition: transform 0.38s var(--ease), opacity 0.32s var(--ease);
  will-change: transform;
}
.carousel::-webkit-scrollbar { display: none; }

.carousel-spacer {
  flex-shrink: 0;
  width: 100%;
  pointer-events: none;
}

/* Hidden in single-category mode (cross-category scrolling disabled) */
.card--hide { display: none !important; }
.carousel.bar-hidden {
  /* clear its own width *and* the category dock it now sits to the left of */
  transform: translateX(calc(100% + var(--catdock-w) + 20px));
  opacity: 0;
  pointer-events: none;
}

.carousel-collapse-btn {
  position: fixed;
  right: calc(var(--carousel-w) + var(--catdock-w));
  top: 50%;
  transform: translateY(-50%);
  z-index: 11;
  width: 22px;
  height: 52px;
  padding: 0;
  background: linear-gradient(180deg,
    rgba(10, 10, 10, 0.88) 0%,
    rgba(8, 8, 8, 0.83)    55%,
    rgba(5, 5, 5, 0.90)    100%);
  backdrop-filter: blur(32px) saturate(200%);
  -webkit-backdrop-filter: blur(32px) saturate(200%);
  border: 1px solid rgba(255,255,255,0.09);
  border-right: none;
  border-radius: 10px 0 0 10px;
  box-shadow: -4px 0 16px rgba(0,0,0,0.4);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,0.45);
  transition: right 0.38s var(--ease), background 0.2s, color 0.2s;
}
.carousel-collapse-btn:hover {
  background: rgba(255,255,255,0.12);
  color: rgba(255,255,255,0.95);
}
.carousel-collapse-btn svg {
  transition: transform 0.32s var(--ease);
  transform: rotate(180deg); /* carousel visible → chevron points right (›) */
}
.carousel-collapse-btn.bar-hidden {
  right: 0;
}
.carousel-collapse-btn.bar-hidden svg {
  transform: rotate(0deg); /* carousel hidden → chevron points left (‹) */
}
@media (max-width: 480px) {
  .carousel-collapse-btn.bar-hidden { right: 0; }
}

/* ── Mobile responsive adjustments ───────────────────────────────── */
@media (max-width: 480px) {
  :root {
    --carousel-w:  100px;  /* narrower carousel + dock on small screens */
    --catdock-w:   124px;
    --header-base: 60px;
    --strip-h:     84px;
  }
  .asset-tile { width: 64px; height: 48px; margin: 0 5px; }
  .carousel {
    gap: 8px;
  }
  .card {
    width: 80px;
  }
  .card-thumb {
    width: 60px;
    height: 44px;
  }
  .filter-tag {
    width: calc(100% - 16px);
    min-height: 48px;
    margin: 4px 0;
    font-size: 0.78rem;
    padding: 11px 16px 11px 11px;
    border-radius: 12px;
  }
  .top-bar {
    padding: 0 12px;
  }
  /* Never let the logo crowd out the controls on a small screen, whatever
     width is configured in the admin. */
  .brand-logo {
    max-width: 40vw;
  }
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: 10px 12px;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
  flex-shrink: 0;
  scroll-snap-align: center;
  transform: scale(var(--card-scale, 0.82));
  transform-origin: center center;
  transition:
    background 0.22s var(--ease),
    border-color 0.22s var(--ease),
    box-shadow 0.22s var(--ease),
    transform 0.32s cubic-bezier(0.22, 0.61, 0.36, 1);
  width: 92px;
}

@keyframes update-dot-pulse {
  0%, 100% { opacity: 1;   transform: translateX(-50%) scale(1);   }
  50%       { opacity: 0.5; transform: translateX(-50%) scale(1.35); }
}
.update-dot {
  position: fixed;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #f97316;
  z-index: 9999;
  pointer-events: none;
  animation: update-dot-pulse 1.4s ease-in-out infinite;
}
.card:not([aria-selected="true"]):hover {
  background: rgba(255,255,255,0.06);
  border-color: var(--border-hi);
  transform: scale(var(--card-scale, 0.82)) translateY(-3px);
}
.card[aria-selected="true"] {
  background: rgba(200, 169, 110, 0.14);
  border-color: var(--accent);
  box-shadow:
    0 0 0 1px var(--accent),
    0 10px 40px rgba(200, 169, 110, 0.28),
    0 0 20px rgba(200, 169, 110, 0.15),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
}

.card-thumb {
  position: relative;   /* anchors .card-asset-count */
  width: 68px;
  height: 50px;
  border-radius: 6px;
  overflow: hidden;
  background: rgba(255,255,255,0.04);
  display: flex;
  align-items: center;
  justify-content: center;
}
.card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}

.card-label {
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  text-align: center;
  transition: color 0.2s;
  width: 100%;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.4;
  max-height: 2.8em;
  word-break: break-word;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
.card[aria-selected="true"] .card-label {
  color: var(--accent);
  font-weight: 700;
  letter-spacing: 0.04em;
}
.card-label--arabic {
  font-family: var(--font-both);
  direction: rtl;
  letter-spacing: 0;
}

.model-tag--arabic {
  font-family: var(--font-both);
  direction: rtl;
  letter-spacing: 0.04em;
}

.filter-tag--arabic {
  font-family: var(--font-both);
  direction: rtl;
  letter-spacing: 0;
}

.card-tris {
  font-size: 0.5rem;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}

/* ── Upload card ──────────────────────────────────────────────────── */
.card--upload {
  border-style: dashed;
  border-color: rgba(255, 255, 255, 0.1);
  color: var(--text-dim);
  cursor: pointer;
  display: flex;
}
.card--upload:hover {
  border-color: rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  transform: translateY(-2px);
}

.card-thumb--upload {
  background: rgba(255, 255, 255, 0.03);
  color: var(--text-faint);
  transition: color 0.2s;
}
.card--upload:hover .card-thumb--upload { color: var(--text-dim); }

/* ── Cache badge ──────────────────────────────────────────────────── */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}


/* ── Empty state ─────────────────────────────────────────────────── */
.empty-state {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  pointer-events: none;
  z-index: 1;
  animation: fadeUp 0.5s var(--ease);
}
.empty-state[hidden] { display: none; }
.empty-icon { opacity: 0.16; margin-bottom: 4px; }
.empty-title {
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: rgba(255, 255, 255, 0.28);
}
.empty-sub { font-size: 0.75rem; color: rgba(255, 255, 255, 0.15); }
.empty-formats {
  font-size: 0.5625rem;
  letter-spacing: 0.12em;
  color: rgba(255, 255, 255, 0.09);
  margin-top: 2px;
}

/* ── Content fade on item switch ─────────────────────────────────── */
/* Fades only the viewer and info panel — carousel is untouched.
   The base rule is the fade *in* (the class coming off); the
   .content-fading group below overrides the timing for the fade *out*, so
   the two halves of a switch are not forced to share one curve. */
model-viewer,
#media-image,
#media-video,
.info-panel,
.hud-rail {
  transition: opacity var(--fade-in) var(--ease-in-fade), bottom 0.38s var(--ease);
}
body.content-fading model-viewer,
body.content-fading #media-image,
body.content-fading #media-video,
body.content-fading .info-panel,
body.content-fading .hud-rail {
  opacity: 0;
  pointer-events: none;
  transition-duration: var(--fade-out), 0.38s;
  transition-timing-function: var(--ease-out-fade), var(--ease);
}
/* The group above replaces the panel's own transition list, so restate it with
   `left` — that is what animates the collapse (see .info-panel.panel-hidden). */
.info-panel {
  transition: opacity var(--fade-in) var(--ease-in-fade), bottom 0.38s var(--ease),
              left 0.38s var(--ease);
}
/* Same restatement for the fading state: the panel's list has a third entry, so
   the two-value overrides above would leave `left` on the fade-out duration. */
body.content-fading .info-panel {
  transition-duration: var(--fade-out), 0.38s, 0.38s;
  transition-timing-function: var(--ease-out-fade), var(--ease), var(--ease);
}

/* ── Utility ──────────────────────────────────────────────────────── */
button { cursor: pointer; }
svg { display: block; }

/* ── Home button ──────────────────────────────────────────────────────
   A header chip immediately to the left of the brand logo — same glass
   language as the PERF chip, with the accent tint that marks it as the way
   back to the home screen. */
.top-bar-right {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
}

.home-btn {
  pointer-events: all;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 0 15px;
  border: 1px solid rgba(200, 169, 110, 0.30);
  border-radius: var(--r-pill);
  background: linear-gradient(180deg,
    rgba(200, 169, 110, 0.18) 0%,
    rgba(200, 169, 110, 0.07) 100%),
    var(--surface);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  color: rgba(255, 255, 255, 0.88);
  font-family: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  cursor: pointer;
  transition: opacity 0.32s var(--ease), background 0.25s var(--ease),
              border-color 0.25s var(--ease), color 0.2s var(--ease);
}
.home-btn:hover {
  color: #fff;
  border-color: rgba(200, 169, 110, 0.55);
}
.home-btn-label { line-height: 1; }
/* display:flex above would otherwise win over the `hidden` attribute. */
.home-btn[hidden] { display: none; }

/* ── Screensaver ──────────────────────────────────────────────────────
   Covers everything except the first-run init overlay (z-index 9999): while it
   plays, the kiosk has no operable UI — the whole surface is one big "wake me"
   target. Fades in and out rather than cutting, so an attract loop starting up
   in a quiet gallery is not a flash. */
.screensaver {
  position: fixed;
  inset: 0;
  z-index: 9990;
  background: #000;
  opacity: 0;
  transition: opacity 0.6s var(--ease);
  cursor: none;
}
.screensaver.visible { opacity: 1; }
.screensaver[hidden] { display: none; }

.screensaver-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-select: none;
  /* The overlay itself takes the wake-up tap — the <video> must not swallow it. */
  pointer-events: none;
}

/* ── Category transition ──────────────────────────────────────────────
   The clip that plays while the viewer moves into a different category. One
   layer below the screensaver: an idle kiosk going to sleep still wins. The
   fade durations here are mirrored by CAT_TRANS_FADE_MS in app.js — the switch
   underneath happens exactly when this reaches full opacity, so the two must
   stay in step. */
.cat-transition {
  position: fixed;
  inset: 0;
  z-index: 9980;
  background: #000;
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  cursor: none;
  /* Swallows every tap while it runs, so a second category cannot be picked
     mid-transition and the switch underneath is never seen being made. */
  pointer-events: auto;
}
.cat-transition.visible { opacity: 1; }
.cat-transition[hidden] { display: none; }
/* Fading back out is slower than fading in: arriving somewhere should feel
   like the room lights coming up, not like the clip being cut off. */
.cat-transition.fading-out { transition-duration: 0.55s; }

.cat-transition-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
}

/* Skip control — the YouTube-ad shape, live from the first frame. It sits in
   the trailing bottom corner and swaps sides with the language, like the rest
   of the chrome. No opacity of its own: it dissolves with the overlay it lives
   in. */
.cat-skip {
  position: absolute;
  bottom: clamp(24px, 5vh, 56px);
  right: clamp(24px, 4vw, 56px);
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 22px;
  border: 1px solid rgba(200, 169, 110, 0.85);
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0.62);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  color: #fff;
  font-family: var(--font-both);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: background 0.2s var(--ease), transform 0.15s var(--ease);
}
.cat-skip[hidden] { display: none; }
.cat-skip:hover { background: rgba(200, 169, 110, 0.22); }
.cat-skip:active { transform: scale(0.97); }
html[lang="ar"] .cat-skip { right: auto; left: clamp(24px, 4vw, 56px); }

/* The ▶| glyph says the tap moves you on rather than closing something, so it
   points the way the visitor is going and flips with the script. */
.cat-skip-icon { width: 15px; height: 15px; flex: none; }
html[lang="ar"] .cat-skip-icon { transform: scaleX(-1); }

@media (max-width: 720px) {
  .cat-skip { bottom: 20px; right: 16px; padding: 11px 18px; font-size: 0.76rem; }
  html[lang="ar"] .cat-skip { right: auto; left: 16px; }
}

/* ── Home screen ──────────────────────────────────────────────────────
   Sits above the stage but below the header (z-index 30). The category dock
   is lifted above it while home is showing (see body.home-active), so the
   logo, language toggle and category rail all stay exactly where they are. */
.home-screen {
  position: fixed;
  inset: 0;
  z-index: 20;
  overflow: hidden;
  background: var(--bg);
  opacity: 0;
  transition: opacity 0.45s var(--ease);

  /* The vertical room the bottom category rail needs — .home-content's bottom
     padding, so the welcome copy above always clears it.

     --home-rail-h is *measured*, by measureHomeRail() in app.js, and that is
     the whole point: how tall a card really is depends on things no formula
     here can know — a label that wraps to two lines, an icon height an admin
     set, a collection crowded enough to wrap onto a second row. Predicting it
     from the card's drawn height is what let the rail climb into the headline.
     The fallback is a single row of plain, unwrapped cards: what the frame
     before the observer's first answer renders with. Beside it, the rail's own
     bottom offset (20) plus the clearance between the two (22).

     Declared HERE and not in :root, which is where it does not work: a custom
     property is substituted where it is *declared*, and app.js writes
     --home-rail-h onto this element — a :root declaration would resolve against
     :root's own (unset) value and silently take the fallback forever.

     Deliberately uncapped: the reserve has to be whatever the rail *is*, or the
     overlap it exists to prevent comes straight back. Where the rail could grow
     past its share of the screen — a narrow one, which wraps a collection into
     several rows — it is the rail that is capped, by --home-rail-max in the
     ≤900px block, and this then follows it down. */
  --home-rail-reserve: calc(var(--bottom-inset)
                            + var(--home-rail-h, calc(152 * var(--hu-cat)))
                            + 42 * var(--hu-cat));
}
.home-screen.visible { opacity: 1; }
.home-screen[hidden] { display: none; }

.home-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
}
.home-bg[hidden] { display: none; }

/* Legibility wash under the headline + search. */
.home-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.15) 38%, rgba(0,0,0,0.72) 100%),
    radial-gradient(120% 90% at 30% 60%, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0) 70%);
}

.home-content {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(18 * var(--hu));
  /* The right-edge dock is hidden on home, so the copy gets the full width; the
     bottom padding is whatever the rail actually measures — see
     --home-rail-reserve on .home-screen above, inherited from there. */
  padding: calc(var(--header-h) + 32 * var(--hu)) calc(48 * var(--hu))
           var(--home-rail-reserve) calc(48 * var(--hu));
  text-align: center;
  /* The reserve is exact, so the space left over is exactly what the copy may
     take. Deliberately NOT `overflow: hidden` — the card's drop shadow reaches
     well past its box and would be sliced off. Nothing in here is allowed to
     overflow in the first place: everything below is capped against this box
     (100%) or against --hu, which is already short-screen aware. */
  min-height: 0;
}

/* The welcome column — hero image, headline, subtitle. A block of its own so
   the feature video can sit beside it rather than under it (.has-video). */
.home-copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(18 * var(--hu));
  min-width: 0;
  /* A flex item's default min-height is its content, which would let a long
     headline push the column past the room .home-content left it. */
  min-height: 0;
  max-height: 100%;
}

/* ── Two-column home ──────────────────────────────────────────────────
   Set by applyHomeVideoSettings() whenever a feature video is configured:
   the welcome copy takes the leading half, the video the trailing one. The
   direction follows the language rather than a `dir` attribute — the document
   stays dir="ltr" and only `lang` flips (setLanguage() in app.js) — so Arabic
   gets its own row-reverse rule below rather than relying on inheritance. */
.home-screen.has-video .home-content {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  /* Both in --hu, like everything else here. A vw term would have been the
     width alone deciding how much room the columns give up on a screen whose
     height is the tight dimension. */
  gap: calc(50 * var(--hu));
  /* Wider side padding than the stacked layout gets: the pair below is sized to
     its own content and centred, so this is breathing room at the screen edges,
     not space the columns expand into. */
  padding-inline: calc(112 * var(--hu));
}
/* Sized to the copy, NOT to the free space. `flex: 1 1 0` here would make the
   column eat the whole leading half and pin the text against the padding, with
   the gap opening up in the middle — the two blocks have to read as one group
   at the centre of the screen. */
.home-screen.has-video .home-copy {
  flex: 0 1 auto;
  max-width: min(calc(560 * var(--hu)), 44vw);
  align-items: flex-start;
  text-align: left;
}
html[lang="ar"] .home-screen.has-video .home-content { flex-direction: row-reverse; }
html[lang="ar"] .home-screen.has-video .home-copy {
  align-items: flex-end;
  text-align: right;
}

/* The still above the headline. Height-capped rather than width-capped so a
   wide wordmark and a tall crest both sit sensibly over the same title. The
   18vh term is a second opinion, not the scale: --hu already knows the screen
   is short, and this keeps a tall crest from taking the column on its own. */
.home-hero {
  max-width: min(calc(420 * var(--hu)), 100%);
  max-height: min(calc(220 * var(--hu)), 18vh);
  width: auto;
  height: auto;
  object-fit: contain;
  user-select: none;
  -webkit-user-select: none;
  filter: drop-shadow(0 6px 26px rgba(0, 0, 0, 0.55));
}
.home-hero[hidden] { display: none; }

/* Sized in --hu and nothing else — the same unit the space around it is drawn
   in, so the headline holds its proportion to the hero above it and the card
   beside it on every screen. It used to be a clamp with a 5vw middle term, and
   width-only was exactly wrong for it: on a wide, short screen the widest thing
   on the page set the size of the tallest, and the subtitle underneath went
   behind the category rail. The ceiling being a multiple of --hu rather than a
   fixed rem is the other half of that — a hard 4rem cap is what made the
   headline read as small furniture on a big panel.

   Both max-widths are in `ch`, so the measure is set by the type rather than by
   the screen and holds at every size the line can take. */
.home-title {
  max-width: 20ch;
  font-size: calc(64 * var(--hu));
  font-weight: 800;
  line-height: 1.12;
  letter-spacing: -0.01em;
  text-shadow: 0 4px 30px rgba(0, 0, 0, 0.75);
}
.home-subtitle {
  max-width: 46ch;
  font-size: calc(21 * var(--hu));
  font-weight: 400;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.78);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.7);
}
.home-title[hidden], .home-subtitle[hidden] { display: none; }

/* ── Home feature video — the play card ───────────────────────────────
   The same lit glass slab as a .home-cat below it, sized like a small screen:
   its opening frame under a gold play button, with an optional label. It sits
   in the flow of .home-content, so it lands between the welcome copy and the
   category rail whether or not either of those is configured. */
.home-video-card {
  position: relative;
  flex: 0 0 auto;
  width: min(calc(420 * var(--hu)), 62vw);
  aspect-ratio: 16 / 9;
  /* A short screen must not push the card down onto the category rail — the
     poster is object-fit: cover, so a wider-than-16:9 box still reads right.
     The 100% is against .home-content's own content box, which is the screen
     minus the header and minus the measured rail: the card can never be taller
     than the room the layout actually has, whatever the viewport says. */
  max-height: min(32vh, 100%);
  /* Plain breathing room now that .home-content's reserve is the rail's real
     height: this used to be the slack that covered the reserve's guess being
     short of what the cards actually stood. In --hu-cat, since the rail below
     is what it is spacing away from. */
  margin: calc(6 * var(--hu)) 0 calc(18 * var(--hu-cat));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 0;
  padding: 0;
  border: 1px solid rgba(200, 169, 110, 0.55);
  border-radius: calc(20 * var(--hu));
  background: #000;
  box-shadow:
    0 18px 42px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.14);
  color: #fff;
  font-family: inherit;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease),
              transform 0.2s var(--ease);
}
.home-video-card[hidden] { display: none; }

/* Beside the copy rather than under it: the card takes the trailing column, so
   it drops the stacking margin and grows to the half it now owns. The order is
   set on .home-content (row / row-reverse), not here — one rule decides which
   side each column is on. */
.home-screen.has-video .home-video-card {
  flex: 0 1 auto;
  width: min(calc(560 * var(--hu)), 40vw);
  /* Same 100% guard as the stacked card above: the column it now sits in is
     as tall as .home-content's content box and no taller. */
  max-height: min(52vh, 100%);
  margin: 0;
}
.home-video-card:hover {
  border-color: rgba(200, 169, 110, 0.9);
  transform: translateY(-3px);
  box-shadow:
    0 24px 52px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

.home-video-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* Play button + a wash under the label, so a bright frame never swallows
   either. */
.home-video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: calc(74 * var(--hu));
  height: calc(74 * var(--hu));
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(200, 169, 110, 0.75);
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--accent);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
  transition: background 0.25s var(--ease), transform 0.2s var(--ease);
}
.home-video-play svg {
  width: calc(34 * var(--hu));
  height: calc(34 * var(--hu));
  margin-left: calc(3 * var(--hu));
}
.home-video-card:hover .home-video-play {
  background: rgba(0, 0, 0, 0.62);
  transform: translate(-50%, -50%) scale(1.06);
}

.home-video-label {
  position: relative;
  width: 100%;
  padding: calc(34 * var(--hu)) calc(18 * var(--hu)) calc(14 * var(--hu));
  font-size: calc(15.2 * var(--hu));
  font-weight: 700;
  letter-spacing: 0.02em;
  text-align: center;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.7);
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.72) 100%);
}
.home-video-label:empty { display: none; }
/* An Arabic label takes the Arabic face, as everywhere else on this screen. */
.home-video-card--arabic .home-video-label {
  font-family: var(--font-both);
  letter-spacing: 0;
}

/* ── Home video player ────────────────────────────────────────────────
   Full-screen playback with the browser's own controls. Above the category
   transition (9980), below the screensaver (9990): an idle kiosk still wins,
   and showScreensaver() closes this before it covers the screen. */
.home-video-player {
  position: fixed;
  inset: 0;
  z-index: 9985;
  background: #000;
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}
.home-video-player.visible { opacity: 1; }
.home-video-player[hidden] { display: none; }

.home-video-full {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

.home-video-close {
  position: absolute;
  top: calc(22 * var(--hu));
  right: calc(22 * var(--hu));
  width: calc(52 * var(--hu));
  height: calc(52 * var(--hu));
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(200, 169, 110, 0.55);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  color: #fff;
  cursor: pointer;
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease);
}
.home-video-close svg { width: calc(22 * var(--hu)); height: calc(22 * var(--hu)); }
.home-video-close:hover {
  background: rgba(0, 0, 0, 0.75);
  border-color: rgba(200, 169, 110, 0.9);
}

/* ── Home search ───────────────────────────────────────────────────────
   Lives in the top bar, centred on the viewport rather than on the space
   between its neighbours — absolute positioning keeps it put no matter how
   wide the language toggle or the brand logo happen to be. It belongs to the
   home screen, so it only shows while that is up (body.home-active), and it
   re-enables pointer events the header switches off for its own background.
   The results panel and keyword chips hang below the header, which has no
   background of its own to clip them. */
.home-search {
  position: absolute;
  top: 50%;
  left: 50%;
  /* --kb-shift tracks the visual viewport while a phone keyboard is up, so the
     box stays on screen even if the browser scrolls the layout viewport out
     from under this fixed header (see pinSearchToViewport in app.js). */
  transform: translate(-50%, calc(-50% + var(--kb-shift, 0px)));
  /* --hu first, with a width term only as the guard against a very wide box on
     a narrow screen — the box has to stay clear of the logo and the language
     toggle either side of it. */
  width: min(calc(460 * var(--hu)), 40vw);
  pointer-events: all;
  display: none;
}
body.home-active .home-search:not([hidden]) { display: block; }
/* Sized to sit inside the header rather than to fill a hero area — see
   --home-search-h, which is what keeps a scaled-up box inside a header that
   is only as tall as the brand logo makes it. */
.home-search .home-search-box { height: var(--home-search-h); }

.home-search-box {
  display: flex;
  align-items: center;
  gap: calc(var(--home-search-h) * 0.208);
  padding: 0 calc(var(--home-search-h) * 0.292);
  height: calc(var(--home-search-h) * 1.167);
  background: var(--surface-hi);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid rgba(200, 169, 110, 0.55);
  border-radius: var(--r-pill);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45);
}
/* The markup carries width/height="20" as the no-CSS baseline; these override
   it so the glyph keeps its proportion to the box on a scaled-up screen. */
.home-search-icon {
  flex: 0 0 auto;
  width: calc(var(--home-search-h) * 0.417);
  height: calc(var(--home-search-h) * 0.417);
  color: var(--text-dim);
}
.home-search-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  font-family: inherit;
  font-size: calc(var(--home-search-h) * 0.34);
  font-weight: 500;
  /* body sets user-select/touch-action to none for kiosk hardening — the one
     field a visitor is meant to type into has to opt back in. */
  user-select: text;
  -webkit-user-select: text;
  touch-action: auto;
}
.home-search-input::placeholder { color: var(--text-faint); }
.home-search-clear {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--home-search-h) * 0.625);
  height: calc(var(--home-search-h) * 0.625);
  background: rgba(255, 255, 255, 0.08);
  border: none;
  border-radius: 50%;
  color: var(--text-dim);
  cursor: pointer;
}
.home-search-clear svg {
  width: calc(var(--home-search-h) * 0.292);
  height: calc(var(--home-search-h) * 0.292);
}
.home-search-clear[hidden] { display: none; }

/* Results drop below the box and float over the background. */
.home-results {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  right: 0;
  /* --kb-space is the room left under the box once a phone keyboard is up;
     it stays at 100vh on desktop, where the first two terms win anyway. */
  max-height: min(46vh, calc(420 * var(--hu)), var(--kb-space, 100vh));
  overflow-y: auto;
  touch-action: pan-y;   /* body disables touch scrolling globally */
  scrollbar-width: none;
  padding: calc(6 * var(--hu));
  background: var(--surface-hi);
  backdrop-filter: var(--blur);
  -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--border-hi);
  border-radius: var(--r);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.6);
  text-align: left;
  z-index: 2;
}
.home-results::-webkit-scrollbar { display: none; }
.home-results[hidden] { display: none; }

.home-result {
  display: flex;
  align-items: center;
  gap: calc(12 * var(--hu));
  width: 100%;
  padding: calc(8 * var(--hu)) calc(10 * var(--hu));
  background: transparent;
  border: none;
  border-radius: var(--r-sm);
  color: var(--text);
  font-family: inherit;
  font-size: calc(15.2 * var(--hu));
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.home-result:hover { background: rgba(255, 255, 255, 0.07); }
.home-result img {
  flex: 0 0 auto;
  width: calc(54 * var(--hu));
  height: calc(40 * var(--hu));
  object-fit: cover;
  border-radius: 6px;
  background: #111;
}
.home-result-text { min-width: 0; }
.home-result-name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-result-cat {
  display: block;
  margin-top: calc(2 * var(--hu));
  font-size: calc(10.9 * var(--hu));
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.home-result-empty {
  padding: calc(14 * var(--hu)) calc(12 * var(--hu));
  color: var(--text-dim);
  font-size: calc(14.4 * var(--hu));
  text-align: center;
}

/* Suggested keywords — item names and categories already in this viewer.
   Hidden until the visitor actually engages with the search box: they are a
   prompt for what to type, not permanent furniture. Shown while the box has
   focus and is still empty (once there is a query the results panel takes
   this space). `show-keywords` is toggled in app.js.
   Taken out of flow like the results panel: `.home-search` is centred on the
   header with a -50% translate, so anything that adds to its height would
   shunt the box itself upward the moment the chips appear. */
.home-keywords {
  display: none;
  position: absolute;
  top: calc(100% + 14 * var(--hu));
  left: 0;
  right: 0;
  flex-wrap: wrap;
  justify-content: center;
  gap: calc(8 * var(--hu));
}
.home-search.show-keywords .home-keywords { display: flex; }
.home-keywords:empty { display: none; }
.home-keyword {
  padding: calc(7 * var(--hu)) calc(15 * var(--hu));
  background: rgba(255, 255, 255, 0.07);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: rgba(255, 255, 255, 0.75);
  font-family: inherit;
  font-size: calc(12.8 * var(--hu));
  font-weight: 600;
  cursor: pointer;
  transition: color 0.2s var(--ease), background 0.2s var(--ease);
}
.home-keyword:hover { color: #fff; background: rgba(255, 255, 255, 0.13); }

/* ── Home categories — the bottom rail ────────────────────────────────
   The same glass card as a .filter-tag in the right-edge dock, turned on its
   side: the accent edge runs along the bottom instead of the trailing edge.
   Every category is laid out at once (wrapping if it has to) — there is
   deliberately nothing to scroll here, so a visitor sees the whole collection
   at a glance. */
.home-categories {
  position: absolute;
  left: calc(32 * var(--hu));
  right: calc(32 * var(--hu));
  bottom: calc(var(--bottom-inset) + 20 * var(--hu-cat));
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: center;
  gap: var(--home-cat-gap);
  z-index: 2;

  /* renderHomeCategories() in app.js writes the real category count here; the
     fallback is only what an unrendered rail would use. The width one card may
     take if the whole rail is to stay on a single row follows from it — see
     .home-cat below, which is where the two are reconciled. */
  --home-cat-n: 6;
  --home-cat-gap: calc(14 * var(--hu-cat));
  --home-cat-fit: calc(
    (100% - (var(--home-cat-n) - 1) * var(--home-cat-gap)) / var(--home-cat-n)
  );
}
.home-categories:empty { display: none; }

/* Width is the one size on this rail that cannot simply track --hu-cat: every
   category is laid out at once, so a card scaled for a 4K panel would push a
   collection of seven or eight onto a second row and climb into the welcome
   copy. Each card is therefore given the exact share of the rail that keeps
   them on one line (--home-cat-fit), which is also why this is a flex-basis
   and not a min-width: `flex-wrap` breaks the line by the items' *hypothetical*
   size — a card sized by its label — and never gets as far as shrinking them.
   The floor below is what a rail too crowded to fit falls back to: at that
   point wrapping is the better answer, and the min-width is what triggers it. */
.home-cat {
  position: relative;
  flex: 0 1 var(--home-cat-fit, auto);
  min-width: calc(118 * var(--hu-cat));
  max-width: calc(300 * var(--hu-cat));
  min-height: calc(132 * var(--hu-cat));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(12 * var(--hu-cat));
  padding: calc(26 * var(--hu-cat));
  border: 1px solid rgba(200, 169, 110, 0.55);
  border-radius: calc(22 * var(--hu-cat));
  background: rgba(255, 255, 255, 0.07);
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  /* Deep, soft drop plus a top inner highlight — the raised, lit-from-above
     glass slab in the reference. */
  box-shadow:
    0 18px 42px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.14);
  color: #fff;
  font-family: inherit;
  line-height: 1.2;
  text-align: center;
  word-break: break-word;
  cursor: pointer;
  overflow: hidden;
  transition: background 0.25s var(--ease), border-color 0.25s var(--ease),
              box-shadow 0.25s var(--ease), transform 0.2s var(--ease);
}
.home-cat:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(200, 169, 110, 0.85);
  transform: translateY(-3px);
  box-shadow:
    0 24px 52px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

/* A soft sheen sweeping down from the top-left corner — the glossy glass
   catch-light the reference cards carry. Sits above the tint, below the text. */
.home-cat::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    linear-gradient(150deg,
      rgba(255, 255, 255, 0.16) 0%,
      rgba(255, 255, 255, 0.04) 34%,
      transparent 60%);
  pointer-events: none;
}

/* The full-bleed accent tint. A category with no colour set keeps the neutral
   frosted glass above; a coloured one fills with its hue, brightest at the top
   like the reference swatches. */
.home-cat.has-cat-color {
  background:
    linear-gradient(160deg,
      rgba(var(--cat-rgb), 0.62) 0%,
      rgba(var(--cat-rgb), 0.40) 55%,
      rgba(var(--cat-rgb), 0.30) 100%);
  border-color: rgba(200, 169, 110, 0.55);
}
.home-cat.has-cat-color:hover {
  background:
    linear-gradient(160deg,
      rgba(var(--cat-rgb), 0.72) 0%,
      rgba(var(--cat-rgb), 0.48) 55%,
      rgba(var(--cat-rgb), 0.36) 100%);
  border-color: rgba(200, 169, 110, 0.85);
}

/* The category's own picture, above its name — a glyph, a crest, a pictogram.
   Uploaded per category in the admin (category-icon.php); a category without one
   simply has no <img> and the card is exactly what it always was.

   The height is the size that matters: these are mixed artwork — a wide wordmark
   and a square glyph both have to sit on the same rail without one of them
   towering over its neighbours — so the admin sets a max *height* and the width
   follows from the aspect ratio. Like every other size here it is a multiple of
   --hu-cat rather than a px literal, so it scales with the rail instead of
   marooning a 1280-sized icon on a 4K panel. renderHomeCategories() sets
   --home-cat-icon-h per card from the saved number; a category that has saved
   none falls through to the default below. */
.home-cat-icon {
  position: relative;
  display: block;
  flex: 0 0 auto;
  /* The fallback is the default height, written here rather than in .home-cat so
     the per-card override renderHomeCategories() sets on the *button* is the
     only definition that can win — a default declared on this element would
     outrank the inherited one and nothing would ever change size.

     The 13vh ceiling is what stops a generous admin number from turning the
     rail into half the screen: the reserve above the rail is measured, so an
     over-tall icon costs the welcome copy its room rather than overlapping it,
     and the copy disappearing is the worse failure of the two. */
  max-height: min(calc(var(--home-cat-icon-h, 44) * var(--hu-cat)), 13vh);
  /* Never wider than the card's content box, whatever the artwork's ratio —
     a panoramic wordmark scales down instead of forcing the card wider. */
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  /* Lifts flat, dark artwork off the glass the same way the text above it is
     lifted, so a single-colour glyph does not disappear into a tinted card. */
  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));
}

/* ── A vector icon, painted in the rail's gold ─────────────────────────
   An SVG is a glyph an admin drew to sit in this UI, so it takes the same warm
   accent as the divider under the name and the item count below it — a black or
   white pictogram arrives already matching the card instead of fighting it. A
   raster keeps its own colours: recolouring a photograph would be vandalism, and
   renderHomeCategories() picks between the two off the filename.

   The mechanism is a *mask*, not a filter: the SVG's alpha channel cuts the gold
   fill to the glyph's shape, so the artwork's own colours never matter and the
   file is never parsed as markup in this document — it stays exactly as inert as
   the <img> it replaces, which is the whole reason serve.php?i= can afford to
   accept SVG at all. Note the consequence: a glyph drawn on an *opaque*
   background plate masks to a gold rectangle. Vectors meant for this rail should
   be transparent; artwork that is not should be uploaded as a PNG. */
.home-cat-icon--gold {
  /* It is the mask that preserves the aspect ratio now (mask-size: contain), so
     the box itself is simply the full card width by the configured height, and
     the glyph is centred inside it. Nothing widens the card: the width resolves
     against the content box it already has. */
  width: 100%;
  height: min(calc(var(--home-cat-icon-h, 44) * var(--hu-cat)), 13vh);
  max-height: none;

  /* The same lit-from-above metallic sweep the cards themselves carry, running
     through --accent so a change to the brand gold reaches the icons too. */
  background: linear-gradient(160deg,
      #e8d3a2 0%,
      var(--accent) 46%,
      #a8874d 100%);

  /* Longhands rather than the `mask` shorthand: the kiosk browsers that still
     need -webkit- do not all parse `center / contain` inside it. */
  -webkit-mask-image:    var(--home-cat-icon-src);
          mask-image:    var(--home-cat-icon-src);
  -webkit-mask-repeat:   no-repeat;
          mask-repeat:   no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size:     contain;
          mask-size:     contain;

  /* No drop-shadow here, unlike the <img> above: filters are applied *before*
     masking, so the shadow would be cast by the full gold rectangle and then
     cut away with it. The gold carries its own contrast against the glass. */
  filter: none;
}

.home-cat-label {
  position: relative;
  display: block;
  font-size: calc(18.4 * var(--hu-cat));
  font-weight: 700;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.55);
}

/* Warm gold hairline between the name and the count, exactly the accent the
   rest of the UI uses — fades out at both ends so it reads as a divider, not a
   rule. */
.home-cat-rule {
  position: relative;
  display: block;
  width: calc(46 * var(--hu-cat));
  height: max(2px, calc(2 * var(--hu-cat)));
  border-radius: 2px;
  background: linear-gradient(90deg,
      transparent 0%,
      var(--accent) 50%,
      transparent 100%);
  opacity: 0.9;
}

.home-cat-count {
  position: relative;
  display: block;
  font-family: inherit;
  font-size: calc(11.5 * var(--hu-cat));
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--accent);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
}

/* Arabic category names take the Arabic face, exactly as .filter-tag--arabic
   does in the right-edge dock. Applied per-label in app.js. */
.home-cat--arabic {
  font-family: var(--font-both);
  direction: rtl;
  letter-spacing: 0;
}

/* ── Home mode: what the viewer hides, and what stays put ─────────────── */
/* The right-edge category dock steps aside for the bottom rail above — the
   same categories, all visible at once. Everything item-specific goes too. */
body.home-active .category-filter,
body.home-active .category-filter-bg,
body.home-active .home-btn,
body.home-active .carousel,
body.home-active .carousel-collapse-btn,
body.home-active .info-panel,
body.home-active .info-collapse-btn,
body.home-active .asset-strip,
body.home-active .asset-strip-bg,
body.home-active .hud-rail,
body.home-active .empty-state {
  opacity: 0;
  pointer-events: none;
}
body.home-active .carousel,
body.home-active .carousel-collapse-btn,
body.home-active .asset-strip,
body.home-active .asset-strip-bg { transform: translateX(120%); }

/* Arabic home copy uses the same face as the rest of the Arabic UI. */
html[lang="ar"] .home-title,
html[lang="ar"] .home-subtitle,
html[lang="ar"] .home-video-label,
html[lang="ar"] .home-search-input { font-family: var(--font-both); }

/* Arabic content on the home screen — keyword chips and search results — takes
   the Arabic face on its own, independent of the UI language: a collection can
   hold Arabic item and category names while the interface is in English.
   `--arabic` is set per element in app.js from isArabicText(). */
.home-keyword--arabic,
.home-result-name--arabic,
.home-result-cat--arabic { font-family: var(--font-both); letter-spacing: 0; }

/* Phones and small tablets. --hu is at its floor by the time a screen is this
   narrow, so this block is not fighting the scale — it is the tighter layout
   the design has always had below 900px, and it still overrides the unit-based
   sizes above. The two tokens are set rather than the properties they drive, so
   everything sized off them stays in proportion. */
@media (max-width: 900px) {
  .home-btn { height: 34px; padding: 0 11px; font-size: 0.62rem; }
  .home-content {
    /* Only the top and the sides tighten. The bottom reserve is the same
       measured one the full-size layout uses — the rail is the rail, and what
       it costs is what it costs on a phone too. */
    padding: calc(var(--header-h) + 20px) 20px var(--home-rail-reserve) 20px;
  }
  /* The copy is sized in --hu above, which bottoms out around the width of a
     phone; below that the headline is set against the viewport instead, since
     a 20ch measure on a 390px screen is what decides how big it can be. */
  .home-title    { font-size: clamp(1.75rem, 6.6vw, 2.6rem); }
  .home-subtitle { font-size: clamp(0.9rem, 3.4vw, 1.05rem); }
  /* The share of a phone the rail may take. It is the only screen that needs a
     ceiling: eight categories fit one row of a wall panel and four rows of a
     phone, and the welcome copy above is a fixed number of lines that do not
     get any shorter to make room. */
  .home-screen { --home-rail-max: 40vh; }
  .home-categories {
    left: 16px; right: 16px; --home-cat-gap: 10px;
    /* Every category is on screen at once with nothing to scroll — and that
       holds right up to the point where a screen this narrow cannot show them
       all: eight cards wrap into four rows taller than the phone itself, and
       the rail being anchored to the bottom puts the first row off the *top*,
       where nothing can reach it. So it is capped at exactly what the copy
       above reserves for it and scrolls past that, which is the one place in
       this design where the rail scrolls. The cards below are also narrower
       here than the full-size layout's, which is what fits three to a row
       rather than two and usually keeps this from arising at all. */
    max-height: var(--home-rail-max);
    overflow-y: auto;
    touch-action: pan-y;   /* body disables touch scrolling globally */
    scrollbar-width: none;
  }
  .home-categories::-webkit-scrollbar { display: none; }
  .home-cat {
    /* Three to a row on a 390px phone, and the side padding is trimmed with it
       so a name like WOODEN BOXES still breaks between its two words rather
       than through the middle of one. */
    min-width: 108px;
    min-height: 104px;
    gap: 9px;
    padding: 18px 10px;
    border-radius: 18px;
  }
  /* The 13vh ceiling the full-size rule uses is most of a phone card; this is
     the height an icon may take before the name under it stops being the thing
     that identifies the category. */
  .home-cat-icon       { max-height: min(calc(var(--home-cat-icon-h, 44) * var(--hu-cat)), 64px); }
  .home-cat-icon--gold { height:     min(calc(var(--home-cat-icon-h, 44) * var(--hu-cat)), 64px); }
  .home-cat-label { font-size: 0.98rem; }
  .home-cat-rule { width: 38px; }
  .home-cat-count { font-size: 0.68rem; }
  .home-search { width: clamp(160px, 40vw, 320px); }
  :root { --home-search-h: 42px; }
  .home-video-card { width: min(320px, 78vw); border-radius: 16px; }
  /* Too narrow to sit side by side — back to the stacked, centred column in
     both languages. */
  .home-screen.has-video .home-content,
  html[lang="ar"] .home-screen.has-video .home-content {
    flex-direction: column;
    gap: 14px;
    /* Back to the stacked layout's own side padding — the wide edge margin
       above exists only to frame the two columns. */
    padding-inline: 20px;
  }
  .home-screen.has-video .home-copy,
  html[lang="ar"] .home-screen.has-video .home-copy {
    flex: 0 1 auto;
    max-width: 100%;
    align-items: center;
    text-align: center;
  }
  /* Narrower than the stacked card above, and height-capped against the room
     .home-content actually left it: on a phone the copy, the card and the rail
     are competing for one screen, and the card is the one of the three that can
     give something up. */
  .home-screen.has-video .home-video-card {
    width: min(300px, 74vw);
    max-height: min(28vh, 100%);
    margin: 6px 0 16px;
  }
  .home-hero { max-height: clamp(70px, 13vh, 140px); }
  .home-video-play { width: 58px; height: 58px; }
  .home-video-play svg { width: 26px; height: 26px; }
  .home-video-label { padding: 26px 12px 10px; font-size: 0.85rem; }
  .home-video-close { top: 14px; right: 14px; width: 44px; height: 44px; }
}

