* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

/* ------------------------------------------------------------------ */
/*  Safe-area insets.                                                   */
/*                                                                     */
/*  index.html sets `viewport-fit=cover`, so on a notched phone the     */
/*  browser reports the notch / home-indicator / rounded-corner insets   */
/*  here. Every piece of chrome that hugs an edge offsets by these, so   */
/*  the back button, the mute light and the art arrows never end up      */
/*  under the hardware. On a device with no insets they resolve to 0px   */
/*  and every rule below behaves exactly as it did before.              */
/* ------------------------------------------------------------------ */
:root {
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000000;
  color: #e8ecf3;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* a phone is a touch device: no pull-to-refresh and no rubber-band on the
     fixed 3D stage */
  overscroll-behavior: none;
  /* kills the iOS long-press callout ("Copy Image", "Add to Calendar") over
     the 3D scene and the LED chrome. Deliberately NOT user-select:none — the
     CONTACT card injects a real form plus an off-screen textarea into <body>
     for execCommand('copy'), and both need to stay selectable/editable. */
  -webkit-touch-callout: none;
}

/* kills the 300ms tap delay without disabling panning, so taps on the
   arrows / flags / window buttons land as fast as a mouse click */
button,
label {
  touch-action: manipulation;
}

#app {
  position: fixed;
  inset: 0;
}

#stage {
  position: absolute;
  inset: 0;
}

#stage canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The 3D stage is a DRAG SURFACE — the CONTACT card is spun and tilted by
   dragging anywhere on the scene (its pointer handlers live on `document`).
   Without touch-action:none the browser claims a touch-drag as its own pan /
   swipe gesture, fires `pointercancel` and the card never moves. Declaring it
   here means the drag reaches the pointer handlers as a real swipe.
   Deliberately scoped to the stage rather than set globally: the art modal
   and the ABOUT window both scroll internally and still need to pan. */
#stage,
#stage canvas {
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

#ctrls {
  position: fixed;
  left: 50%;
  /* max(), not a fixed offset: on a notched phone the home indicator eats the
     bottom ~34px, so the cluster rides above it; everywhere else the safe-area
     inset is 0 and this is still exactly 1.4rem */
  bottom: max(1.4rem, calc(var(--safe-b) + 0.8rem));
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  z-index: 6;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

#ctrls button {
  pointer-events: auto;
}

#back {
  width: 2rem;
  height: 2rem;
  border: none;
  background-color: #1f4ef8;
  color: #ffffff;
  cursor: pointer;
  padding: 0;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 0;
  background-repeat: no-repeat;
  background-position: 55% 55%;
  background-size: contain;
  box-shadow: 0 0 14px rgba(0, 120, 255, 0.55);
}

#back:hover {
  background-color: #2f61ff;
}

#back:active {
  transform: scale(0.94);
}

/* zoom-out button: the down arrow that sits next to the back button while
   you're INSIDE an object (the ART laptop, the MUSIC stereo). It pulls the
   camera back out to the room's idle shot; the back button still leaves the
   scene entirely. Same dot-matrix LED look as every other arrow button. */
#zoomout {
  width: 2rem;
  height: 2rem;
  border: none;
  background-color: #1f4ef8;
  color: #ffffff;
  cursor: pointer;
  padding: 0;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 0;
  background-repeat: no-repeat;
  background-position: 55% 55%;
  background-size: contain;
  box-shadow: 0 0 14px rgba(0, 120, 255, 0.55);
}

#zoomout[hidden] {
  display: none;
}

#zoomout:hover {
  background-color: #2f61ff;
}

#zoomout:active {
  transform: scale(0.94);
}

/* ------------------------------------------------------------------ */
/*  MUTE button - top right, MAIN only.                                */
/*                                                                     */
/*  Deliberately the opposite treatment to the arrow buttons: no blue  */
/*  plate behind it, just the bare white LED dots on the scene, so it */
/*  reads as a status light rather than a control you are meant to     */
/*  press. Size and inset both use clamp() so it scales with the      */
/*  viewport instead of needing a breakpoint per screen size.          */
/* ------------------------------------------------------------------ */
#mute {
  --mute-size: clamp(1.6rem, 2.8vmin + 1.05rem, 2.4rem);
  position: fixed;
  /* max() against the safe-area inset: in portrait the notch/status bar eats
     the top of the screen, and a bare 11px offset would drop the light behind
     it. Landscape/desktop insets are 0 and this is unchanged. */
  top: max(clamp(0.7rem, 2vmin, 1.2rem), calc(var(--safe-t) + 0.5rem));
  right: max(clamp(0.7rem, 2vmin, 1.2rem), calc(var(--safe-r) + 0.5rem));
  width: var(--mute-size);
  height: var(--mute-size);
  border: none;
  border-radius: 0;
  background: none;          /* no plate - the dots float on the scene */
  padding: 0;
  cursor: pointer;
  z-index: 6;
  opacity: 0.7;
  transition: opacity 0.2s ease, transform 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}

#mute[hidden] {
  display: none;
}

#mute:hover,
#mute:focus-visible {
  opacity: 1;
  transform: scale(1.08);
  outline: none;
}

#mute:active {
  transform: scale(0.94);
}

/* muted: the glyph already carries a slash and has lost its waves, so this only
   dims it - a second, redundant cue, because a 2-dot diagonal at 33 px is legible
   but not shouty. Together the slash + the missing waves + the drop to 22% make
   the state obvious at a glance. */
#mute[aria-pressed="true"] {
  opacity: 0.22;
}

#mute[aria-pressed="true"]:hover,
#mute[aria-pressed="true"]:focus-visible {
  opacity: 0.5;
}

/* the dots come from DMATRIX_PIXELS via renderDotMatrixArrow, same as every
   other arrow; the button only supplies size/position, never a colour */

/* --------------------------------------------------------------------------
   INITIAL LOADING SCREEN
   Blue field, a small dot-matrix clip dead centre, the disclaimer laid over
   it and the status line beneath. Shown once, on the very first load; every
   later room change uses #room-loading instead, so this never interrupts a
   running site.

   The blue is the same #1f4ef8 the ATM buttons use, so the screen reads as
   part of the site rather than as a browser default.
   -------------------------------------------------------------------------- */
#loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Black field, blue plate. The plate is the object; the black is the room it
     is sitting in. */
  background: #000000;
  z-index: 10;
  transition: opacity 0.6s;
}

#loading.hide {
  opacity: 0;
  pointer-events: none;
}

/* The whole assembly is built at a fixed natural size and then scaled to fit
   the viewport by js/loadingScreen.js. Scaling beats shrinking the dot pitch:
   the pitch is what makes the matrix legible, and a non-integer pitch is what
   makes LED grids shimmer. */
.load-scale {
  transform-origin: 50% 50%;
  will-change: transform;
}

/* The blue plate: square, every child centred on one axis, and it clips the
   band at its own edges so the belt runs off both sides. */
.load-plate {
  background: #1f4ef8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.load-panel {
  position: relative;
  /* width/height are set from JS */
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}

#load-canvas {
  width: 100%;
  height: 100%;
  display: block;
  /* the clip is 1-bit; never let the browser smooth the dots together */
  image-rendering: pixelated;
}

/* Top line of the plate, in the same 5x7 LED face as LENGUAJE / LANGUAGE.
   JS scales it so its width lands exactly on the clip's width. */
.load-disclaimer {
  margin: 0;
  flex: 0 0 auto;
  background-repeat: no-repeat;
  background-position: center;
  line-height: 0;
  transform-origin: 50% 50%;
  pointer-events: none;
}

/* Belt under the clip, like a CAUTION band: one LED tile repeated and
   translated by exactly its own width, so the seam is invisible.
   --tile-w is the tile's RENDERED width, set by JS to the same dot scale as
   the top line, and the loop travels exactly that far.

   The animation is declared in longhand on purpose. With the `animation`
   shorthand, a single unparseable custom property invalidates the whole
   declaration at computed-value time and the belt silently stops - which is
   exactly the kind of failure that only shows up on one machine. */
.load-text {
  margin: 0;
  width: 100%;
  flex: 0 0 auto;
  background-repeat: repeat-x;
  background-position: 0 0;
  /* draw the tile at the top line's scale, not at its natural size */
  background-size: var(--tile-w, 552px) 100%;
  line-height: 0;
  animation-name: load-band;
  animation-duration: var(--tile-dur, 16s);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes load-band {
  from { background-position: 0 0; }
  /* exactly one rendered tile, so the loop point is invisible */
  to   { background-position: calc(0px - var(--tile-w, 552px)) 0; }
}

/* No reduced-motion override for the belt, deliberately. The belt is the only
   thing on this screen saying the load is alive, so it scrolls whatever the OS
   asks for - an earlier version froze it under `prefers-reduced-motion` and on a
   desktop with animation effects off that left a fragment of the message
   stranded mid-sentence, which read as a broken screen. The dot-matrix clip is
   the one thing here that is genuinely an effect, and it is not CSS. */

/* ------------------------------------------------------------------ */
/*  LOAD FAILED / RETRY                                                 */
/*                                                                      */
/*  A dead network is the one failure a visitor can actually do something */
/*  about, so this is a real control rather than a dead end. It sits in */
/*  the middle of the screen in the same 5x7 LED face as the loading    */
/*  screen, over a black field: the blue plate is the loading state,    */
/*  and a failed load is no longer loading.                             */
/* ------------------------------------------------------------------ */
.load-retry {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000000;
  z-index: 2;
}

.load-retry[hidden] { display: none; }

.load-retry-btn {
  appearance: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  background: transparent;
  /* the LED background and the box size are set from JS */
  background-repeat: no-repeat;
  background-position: center;
  line-height: 0;
  /* a visible hit area: the LED glyphs are only ~28px tall, which is a
     punishing tap target on a phone */
  min-height: 44px;
  min-width: 132px;
  transform-origin: 50% 50%;
  transition: transform 0.12s ease, filter 0.12s ease;
  filter: drop-shadow(0 0 0 rgba(255, 255, 255, 0));
}

.load-retry-btn:hover,
.load-retry-btn:focus-visible {
  filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.55));
  transform: scale(1.04);
}

.load-retry-btn:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 6px;
}

.load-retry-btn:active { transform: scale(0.98); }

/* Respect a user who has asked the OS to cut animation: the belt is content
   and always scrolls (see the note in .load-text), but this is a hover/press
   effect and is safe to drop. */
@media (prefers-reduced-motion: reduce) {
  .load-retry-btn { transition: none; }
  .load-retry-btn:hover,
  .load-retry-btn:focus-visible { transform: none; }
}

/* ------------------------------------------------------------------ */
/*  TEXT LAYER                                                         */
/*                                                                      */
/*  The visible site is a WebGL canvas, so this is the only prose a     */
/*  crawler - or a screen reader, or anyone whose browser cannot run    */
/*  WebGL - will ever see. It is the standard visually-hidden pattern:   */
/*  removed from the visual flow and from the accessibility tree's     */
/*  competitors, but never display:none and never zero-size, so it      */
/*  stays in the accessibility tree and is reachable by find-in-page.   */
/*                                                                      */
/*  It is deliberately NOT `display: none` and NOT `visibility: hidden` */
/*  - those remove the text from assistive tech as well, which would    */
/*  make it pure cloaking.                                              */
/* ------------------------------------------------------------------ */
.a11y-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
  /* the stage is a fixed full-bleed canvas; this must never scroll it */
  pointer-events: none;
}

/* When someone reaches it deliberately - keyboard focus, or find-in-page -
   it has to become genuinely readable rather than stay a 1px sliver. */
.a11y-text:focus-within,
.a11y-text:target {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  white-space: normal;
  clip-path: none;
  pointer-events: auto;
  max-width: 68ch;
  margin-inline: auto;
  padding: 3rem 1.5rem 5rem;
  color: #e8ecf6;
  background: #000000;
  font-size: 1rem;
  line-height: 1.65;
}

.a11y-text:focus-within h1,
.a11y-text:target h1 { font-size: 1.9rem; line-height: 1.2; }
.a11y-text h1 { margin: 0 0 1.2rem; }
.a11y-text h2 { margin: 2.4rem 0 0.7rem; font-size: 1.25rem; }
.a11y-text h3 { margin: 1.6rem 0 0.5rem; font-size: 1.05rem; }
.a11y-text p, .a11y-text li { margin: 0 0 0.9rem; }
.a11y-text dt { font-weight: 600; margin-top: 0.9rem; }
.a11y-text dd { margin: 0.15rem 0 0 1.2rem; }
.a11y-text a { color: #9fb0ff; }
.a11y-text strong { color: #ffffff; }

/* Room-change indicator. This exists because the full-screen #loading overlay
   is a hard cut: it blacks out the whole viewport, which reads as the site
   restarting even though the current room is still running perfectly well
   behind it. The 3D view is never interrupted for a room change — the world
   keeps playing while the next room fetches, and the handover simply happens
   when it is ready. All this adds is a small, quiet three-dot pulse near the
   bottom, clear of the control cluster and the home indicator. */
#room-loading {
  position: fixed;
  left: 50%;
  bottom: max(4.2rem, calc(var(--safe-b) + 3.6rem));
  transform: translateX(-50%);
  z-index: 8;
  pointer-events: none;
  display: flex;
  gap: 0.45rem;
  opacity: 0.5;
}

#room-loading[hidden] {
  display: none;
}

#room-loading-dots {
  display: inline-flex;
  gap: 0.45rem;
}

#room-loading-dots i {
  width: 5px;
  height: 5px;
  background: #8b95a7;
  animation: room-dot 1.1s ease-in-out infinite;
}

#room-loading-dots i:nth-child(2) { animation-delay: 0.18s; }
#room-loading-dots i:nth-child(3) { animation-delay: 0.36s; }

@keyframes room-dot {
  0%, 70%, 100% { opacity: 0.2; transform: translateY(0); }
  35%           { opacity: 1;   transform: translateY(-3px); }
}

/* ------------------------------------------------------------------ */
/*  TORIS dancer texture-set arrows (bottom-center, flank the back    */
/*  button; only visible inside the TORIS scene)                      */
/* ------------------------------------------------------------------ */

.toris-arrow {
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  flex: none;
  border: none;
  border-radius: 0;
  background-color: #1f4ef8;
  color: #ffffff;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-position: 55% 55%;
  background-size: contain;
  box-shadow: 0 0 14px rgba(0, 120, 255, 0.55);
  transition: opacity 0.3s ease, background-color 0.15s ease, transform 0.15s ease;
}

#ctrls.toris .toris-arrow {
  opacity: 1;
  pointer-events: auto;
}

.toris-arrow:hover {
  background-color: #2f61ff;
  transform: scale(1.06);
}

.toris-arrow:active {
  transform: scale(0.94);
}

/* dot-matrix arrow entrance: every UI arrow blinks 3 times (same off/on
   rhythm as the "THIS IS A PORTFOLIO" LED bubble) before it stays lit */
@keyframes dotarrow-blink {
  0%,   20% { opacity: 1; }
  21%,  30% { opacity: 0; }
  31%,  50% { opacity: 1; }
  51%,  60% { opacity: 0; }
  61%,  80% { opacity: 1; }
  81%,  88% { opacity: 0; }
  89%, 100% { opacity: 1; }
}

.dotarrow-flash {
  animation: dotarrow-blink 1.8s linear both;
}

@media (max-width: 480px) {
  #ctrls,
  #art-nav {
    bottom: max(1rem, calc(var(--safe-b) + 0.5rem));
  }
}

/* ------------------------------------------------------------------ */
/*  Introductory language choice (dot-matrix, same family as the      */
/*  arrow buttons): two flat HARD-SQUARE blue screens (arrow style),   */
/*  stacked and centered — the top screen holds the 5x7 LED badge      */
/*  (IDIOMA / LANGUAGE), the bottom the binary high-res flags.         */
/* ------------------------------------------------------------------ */
#lang-choice {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 7;
  pointer-events: none;
  opacity: 0;
  transform: translateY(10px) scale(0.96);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

#lang-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
}

/* one flat arrow-style LED screen: hard square edges, solid fill, no glow */
.lang-screen {
  background-color: #1f4ef8;
  border: 1px solid #0c1c78;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.lang-badge-screen {
  padding: 0.5rem 0.9rem;
}

.lang-flags-screen {
  padding: 0.6rem 0.8rem;
}

#lang-choice.show {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.lang-badge {
  height: auto;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100% 100%;
  filter: drop-shadow(0 0 12px rgba(0, 140, 255, 0.55));
}

.lang-picker {
  display: flex;
  align-items: center;
  justify-content: center;
  /* the flags + slash are the widest fixed thing on the first screen, so they
     scale off the viewport width: full size on a phone, still comfortable on
     a 320px screen, never clipped on a 280px one */
  gap: clamp(0.9rem, 4.5vw, 1.5rem);
  pointer-events: auto;
}

.lang-flag {
  width: clamp(3.25rem, 19vw, 5rem);
  height: clamp(3.25rem, 19vw, 5rem);
  border: none;
  padding: 0;
  background-color: transparent;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  border-radius: 0;
  cursor: pointer;
  filter: none;
  transition: transform 0.15s ease, background-color 0.15s ease;
}

.lang-flag:hover,
.lang-flag:focus-visible {
  transform: scale(1.08);
  filter: drop-shadow(0 0 14px rgba(255, 255, 255, 0.75));
}

.lang-flag:active {
  transform: scale(0.96);
}

.lang-sep {
  width: clamp(1.5rem, 8vw, 2.25rem);
  height: clamp(1.5rem, 8vw, 2.25rem);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  pointer-events: none;
}

/* ------------------------------------------------------------------ */
/*  ART popup overlay (windows on the laptop desktop)                  */
/* ------------------------------------------------------------------ */

#art-popup-stage {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  overflow: hidden;
}

#art-popup-stage.dim::before {
  content: "";
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  pointer-events: none;
}

.art-popup {
  position: fixed;
  left: 0;
  top: 0;
  width: var(--popup-width, 320px);
  max-width: calc(100vw - 2rem);
  pointer-events: auto;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  z-index: 60;
  background: rgba(10, 10, 13, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.16);
  box-shadow: 0 12px 44px rgba(0, 0, 0, 0.55);
  color: #f8f4ea;
  opacity: 0;
  transform: scale(0.96);
  animation: art-popup-in 0.3s ease-out forwards;
  overflow: hidden;
}

.art-popup.portrait {
  --popup-width: min(340px, calc(100vw - 2rem));
}

.art-popup.landscape {
  --popup-width: min(540px, calc(100vw - 2rem));
}

/* Vertical / small screens: the artwork windows are scaled down to ~65% of
   their desktop size. On a 390px portrait phone the desktop landscape popup
   (440px) had to fall back to `100vw - 2rem` anyway, which made it read as a
   full-screen takeover rather than a window sitting on the laptop desktop.
   Driving the width off vw (instead of a fixed px cap) keeps the windows
   proportional at every phone width, and the 46vh image cap leaves the label
   and some drag room underneath. */
@media (max-width: 720px) {
  .art-popup.landscape {
    --popup-width: min(65vw, 286px);
  }

  .art-popup.portrait {
    --popup-width: min(52vw, 182px);
  }

  .art-popup img {
    max-height: 46vh;
  }

  .art-popup .art-popup-label {
    padding: 0.3rem 0.5rem 0.4rem;
    font-size: 0.62rem;
    letter-spacing: 0.14em;
  }
}

.art-popup.dragging {
  cursor: grabbing;
  z-index: 70;
}

.art-popup img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 60vh;
  object-fit: cover;
  pointer-events: none;
}

.art-popup.portrait img {
  width: auto;
  max-width: 100%;
  margin: 0 auto;
}

.art-popup .art-popup-label {
  display: block;
  padding: 0.4rem 0.65rem 0.55rem;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  background: rgba(0, 0, 0, 0.55);
  pointer-events: none;
}

.art-popup-close {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  width: 1.8rem;
  height: 1.8rem;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.65);
  color: #f8f4ea;
  font-size: 1rem;
  cursor: pointer;
  z-index: 2;
}

/* full-screen artwork modal (image + info), mirroring the portfolio script */
#art-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  /* clear the notch / home indicator on a phone, then keep the 2rem gutter */
  padding-top: max(2rem, calc(var(--safe-t) + 1rem));
  padding-bottom: max(2rem, calc(var(--safe-b) + 1rem));
  padding-left: max(2rem, calc(var(--safe-l) + 1rem));
  padding-right: max(2rem, calc(var(--safe-r) + 1rem));
  background: rgba(0, 0, 0, 0.86);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

#art-modal.open {
  display: flex;
}

.art-modal-window {
  position: relative;
  width: min(1200px, calc(100vw - 4rem));
  max-width: 100%;
  max-height: 90vh;
  display: grid;
  grid-template-columns: minmax(0, 1.7fr) minmax(260px, 0.8fr);
  gap: 1.5rem;
  padding: 1.25rem;
  background: rgba(8, 8, 10, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 18px 80px rgba(0, 0, 0, 0.5);
}

.art-modal-media {
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.03);
  min-height: 360px;
  overflow: hidden;
}

.art-modal-media img {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: min(78vh, calc(100vh - 6rem));
  object-fit: contain;
}

.art-modal-close {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.55);
  color: #f8f4ea;
  font-size: 1rem;
  cursor: pointer;
  z-index: 2;
}

.art-modal-info {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.8rem;
  padding: 0.35rem 0.2rem;
}

.art-modal-info h2 {
  margin: 0;
  font-size: 1.1rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.art-modal-info p {
  margin: 0;
  color: #d7d0c0;
  line-height: 1.7;
}

.art-modal-info .art-modal-hint {
  margin-top: 0.35rem;
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #8e8780;
}

.art-modal-info .art-modal-portfolio {
  margin-top: 0.35rem;
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  color: #c9c1ae;
}

.art-modal-info .art-modal-portfolio a {
  color: #f0e9d6;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* browse arrows for the artwork modal: fixed bottom-center, same style and
   placement as the TORIS scene arrows (blue squares flanking the back button
   position), floating above the modal backdrop so switching works while the
   popup is up and never moves with the window */
#art-nav {
  position: fixed;
  left: 50%;
  /* same safe-area treatment as #ctrls, so the browse arrows clear the home
     indicator on a phone */
  bottom: max(1.4rem, calc(var(--safe-b) + 0.8rem));
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  z-index: 110;
  opacity: 0;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  transition: opacity 0.3s ease;
}

#art-nav.open {
  opacity: 1;
  pointer-events: auto;
}

#art-nav.open .toris-arrow {
  opacity: 1;
  pointer-events: auto;
}

/* Narrow / portrait: the two-column window can't fit, so it stacks. The image
   has to give up most of its height here — with the info block underneath it,
   a 78vh image leaves nothing but scroll, which is the wrong read on a phone. */
@media (max-width: 720px) {
  .art-modal-window {
    grid-template-columns: 1fr;
    gap: 1rem;
    max-height: 90vh;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    width: calc(100vw - 2rem);
    max-width: calc(100vw - 2rem);
    margin: 0 auto;
  }

  .art-modal-media {
    min-height: 0;
  }

  .art-modal-media img {
    /* ~40% of the viewport leaves room for the title, description and the
       portfolio link underneath without scrolling on a 667px-tall phone */
    max-height: min(40vh, 60vw);
  }

  .art-modal-info {
    padding: 0 0.2rem 0.35rem;
  }

  .art-modal-info p {
    line-height: 1.6;
  }

  #art-modal {
    padding: 1rem;
    padding-top: max(1rem, calc(var(--safe-t) + 0.5rem));
    padding-bottom: max(1rem, calc(var(--safe-b) + 0.5rem));
  }
}

@keyframes art-popup-in {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* ------------------------------------------------------------------ */
/*  ABOUT fake browser — single-tab chrome-style window floating over   */
/*  the 3D world (~20% of each axis stays visible around it).           */
/* ------------------------------------------------------------------ */

#about-browser {
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
}

#about-browser.open {
  pointer-events: auto;
}

.ab-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.38);
  opacity: 0;
  transition: opacity 0.28s ease;
}

#about-browser.open .ab-backdrop {
  opacity: 1;
}

.ab-window {
  position: absolute;
  inset: 10%;
  display: flex;
  flex-direction: column;
  background: #16171a;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6);
  opacity: 0;
  transform: translateY(14px) scale(0.95);
  transition: opacity 0.26s ease, transform 0.26s ease, inset 0.3s ease,
    border-radius 0.3s ease;
}

#about-browser.open .ab-window {
  opacity: 1;
  transform: none;
}

/* fullscreen keeps a thin sliver of world visible — still meta */
#about-browser.fs .ab-window {
  inset: 1%;
  border-radius: 8px;
}

.ab-tabstrip {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  padding: 6px 8px 0;
  background: #101113;
  flex: none;
}

/* draggable by the tab strip */
.ab-tabstrip {
  cursor: grab;
  touch-action: none;
  user-select: none;
}

#about-browser.dragging,
#about-browser.dragging .ab-tabstrip {
  cursor: grabbing;
}

/* instant follow while dragging — no easing lag */
#about-browser.dragging .ab-window {
  transition: none;
}

.ab-tab {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 60%;
  min-width: 0;
  padding: 7px 10px;
  background: #2b2d31;
  border-radius: 9px 9px 0 0;
  color: #e8ecf3;
  font-size: 12px;
}

.ab-tab-favicon {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
}

.ab-tab-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ab-spacer {
  flex: 1;
}

.ab-tab-close,
.ab-winbtn {
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  flex: none;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #aab2c0;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.ab-tab-close:hover,
.ab-winbtn.fs:hover,
.ab-winbtn[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.14);
  color: #ffffff;
}

.ab-winbtn.close:hover {
  background: #e81123;
  color: #ffffff;
}

.ab-toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: #232529;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  flex: none;
}

.ab-navbtn {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  flex: none;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #aab2c0;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s ease;
}

.ab-navbtn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.12);
}

.ab-navbtn:disabled {
  opacity: 0.35;
  cursor: default;
}

.ab-urlbar {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px;
  background: rgba(255, 255, 255, 0.09);
  border-radius: 999px;
  color: #cfd6e2;
  font-size: 12px;
}

.ab-lock {
  font-size: 10px;
  flex: none;
}

.ab-url {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  user-select: none;
}

.ab-frame {
  flex: 1;
  width: 100%;
  border: 0;
  background: #ffffff;
}

/* Narrow / portrait: the window is nearly full-bleed, so the tab strip would
   land under the notch unless the inset is lifted past the safe area. max()
   keeps the 4% gutter wherever there are no insets. */
@media (max-width: 720px) {
  .ab-window {
    inset: max(4%, var(--safe-t)) max(4%, var(--safe-r)) max(4%, var(--safe-b)) max(4%, var(--safe-l));
  }

  #about-browser.fs .ab-window {
    inset: 0;
    border-radius: 0;
  }
}

/* ------------------------------------------------------------------ */
/*  TOUCH TARGETS — last in the sheet on purpose.                       */
/*                                                                     */
/*  Every button above is sized for a mouse: 2rem / 2.1rem squares and a */
/*  ~28px mute light. A thumb on a phone misses those often enough to     */
/*  matter, so on a coarse pointer every control in the bottom cluster   */
/*  grows to a ~44px box (iOS HIG / Material both land on 44-48). Keyed  */
/*  off the POINTER rather than the width, so a touch laptop and a tablet */
/*  get the roomy targets while a narrow desktop window keeps the compact */
/*  ones — and, living at the end of the file, these plain-ID rules win  */
/*  over the earlier #ctrls / #art-nav / .toris-arrow declarations by    */
/*  source order without needing !important.                            */
/* ------------------------------------------------------------------ */
@media (pointer: coarse) {
  #back,
  #zoomout,
  .toris-arrow {
    width: 2.75rem;
    height: 2.75rem;
  }

  #ctrls,
  #art-nav {
    gap: 0.75rem;
  }

  /* the mute light keeps its "status light" look (no plate, still dimmed) —
     it just gets a thumb-sized box to hit */
  #mute {
    --mute-size: 2.75rem;
  }

  .art-popup-close,
  .art-modal-close {
    width: 2.5rem;
    height: 2.5rem;
  }
}
