/* ============================================================
   VOXEL — design system
   Palette locked to the brand deck (Mirette Wadee, 2026)
   ============================================================ */

:root {
  /* ---- Brand constants ---- */
  --blue:        #0a6fd6;
  --blue-bright: #3d96f6;
  --blue-deep:   #064e9b;
  --navy:        #14171d;
  --navy-deep:   #0a0c10;
  --black:       #000000;
  --paper:       #f2f5f8;
  --white:       #ffffff;

  /* ---- Channels ----
     Held as bare "r g b" triples so every translucent variant in the
     sheet can be written rgb(var(--x-rgb) / .5), and so the handful of
     blocks that re-declare a channel (the printer stage) restyle
     everything nested inside them at once. */
  /* Both are navy, not neutral. They used to be near-black, which was
     invisible against a near-black page top; on the blue ramp a neutral
     panel reads as a black rectangle pasted onto a blue page. They stay
     darker than the ramp so an inset still reads as recessed. */
  --deep-rgb:    3 12 34;      /* deepest surface: overlays, insets   */
  --surface-rgb: 10 22 51;     /* raised surface: cards, panels       */
  --text-rgb:    238 242 247;  /* foreground                          */
  --accent-rgb:  10 111 214;
  --accent-bright-rgb: 61 150 246;
  --accent-deep-rgb:   6 78 155;

  /* ---- Semantic, derived from the channels ---- */
  --bg:        rgb(var(--deep-rgb));
  --bg-alt:    rgb(var(--surface-rgb));
  --text:      rgb(var(--text-rgb));
  --dim:       rgb(var(--text-rgb) / 0.62);
  --line-dark: rgb(var(--text-rgb) / 0.14);
  --line-light: rgb(var(--surface-rgb) / 0.12);
  --dim-ink:   rgb(var(--surface-rgb) / 0.64);
  --footer-bg: var(--black);
  --shadow:    rgba(0, 0, 0, .45);
  --grain-blend: overlay;
  --grain-opacity: .14;
  --aura-a: .62;
  --aura-b: .34;
  --danger:    #7f1d1d;

  /* ---- The company-profile gradient ----
     Sampled from the PDF, which ramps from its navy floor to the blue glow
     at its brightest. Used as the page ground top-to-bottom, so the
     document opens at the deep end and finishes at the lightest.
     The floor is deep navy, not the PDF's near-black corners: the corners
     read as unlit paper rather than as part of the ramp, and starting there
     made the first screenful colourless. The bright end is unchanged, so
     the ramp covers less distance -- the stops below hold its shape. */
  --grad-1: #05204d;
  --grad-2: #08286a;
  --grad-3: #0c3087;
  --grad-4: #1e61f4;

  /* Accent for text sitting on the page background: the brand blue is
     the button colour, and the brighter tint is what stays legible at
     body size on navy. */
  --accent-on-bg: var(--blue-bright);

  color-scheme: dark;

  /* Type — three faces, each doing one job.
     Anton is a single-weight poster face: enormous impact at headline
     size, illegible below about 1.2rem. It is therefore restricted to
     .display, and Archivo carries every mid-level heading and all body
     copy. Plex Mono still carries every number and label. */
  --font-display: 'Anton', 'Archivo', 'IBM Plex Sans Arabic', system-ui, sans-serif;
  --font-body:    'Archivo', 'IBM Plex Sans Arabic', system-ui, sans-serif;
  --font-mono:    'IBM Plex Mono', ui-monospace, monospace;

  /* Rhythm */
  --gut: clamp(1.25rem, 4vw, 3rem);
  --wrap: 1240px;
  --cut: 14px; /* corner cut, echoes the logo's angular terminals */

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
/* Any author `display` beats the UA rule for [hidden], which silently
   un-hides anything given a display value (.done is display:grid). */
[hidden] { display: none !important; }

/* The markup carries English as its fallback, so an Arabic visitor used to
   watch every string swap after the i18n module ran — deferred, like all
   modules. The pre-paint script in each <head> sets dir immediately and
   adds .lang-pending; this holds the translatable text (not the layout,
   not images, not the 3D stages) invisible until applyI18n() removes it.
   visibility, not display: the boxes keep their size, so nothing reflows
   when the words arrive. The script drops the class after 2s regardless,
   so a module that fails to load still shows English. */
html.lang-pending [data-i18n],
html.lang-pending [data-i18n-html],
html.lang-pending [data-lang-toggle] { visibility: hidden; }

/* ============================================================
   THE PAGE GRADIENT
   Spans the whole document, not the viewport: the top of the page
   is the darkest colour in the profile's gradient and the bottom
   is the lightest, so scrolling walks the ramp once. It sits on
   <body>, whose box is the document; a fixed attachment would
   repeat the whole ramp on every screenful instead.
   html carries the dark end so overscroll matches the top.
   ============================================================ */
:root body {
  background: linear-gradient(180deg,
    var(--grad-1) 0%,
    var(--grad-2) 50%,
    var(--grad-3) 88%,
    var(--grad-4) 100%);
  background-repeat: no-repeat;
}
/* The stops are not evenly spread, and that is the whole trick. Dim text
   is text at 62% over the ground; against the gradient's brightest blue
   that lands at 2.7:1, and it needs ~82% opacity to pass — at which point
   it is not dim any more. So the ramp holds the mid-blue until the content
   ends and climbs to the brightest only across the footer, which is the
   actual bottom of the page. Measured: dim text stays >= 4.9:1 the whole
   way down the content. */

/* The footer would otherwise hide that last stretch behind solid black,
   so the lightest colour would never be seen. It goes transparent and
   lets the ramp finish. Its small headings are dim by default, which does
   not survive out there — they take the full foreground instead. */
/* The printer section is a full-viewport dark island. Left opaque it
   paints a flat band straight through the middle of the ramp. Its canvas
   is already alpha:true, so letting the gradient serve as the backdrop
   removes the seam — and at that depth the ramp is still deep navy, which
   is exactly the ground the molten blue needs to read against. */
:root .printer,
:root .printer__sticky { background: transparent; }

/* The loader covers the first viewport, so it has to be the ramp's first
   colour, not the page token. They used to be the same near-black; now the
   ramp starts at navy and a --bg boot screen would hand over with a flash. */
:root .boot { background: var(--grad-1); }

/* The header is opaque by design — frosted glass is borrowed vocabulary —
   but a flat colour cannot match a ground that changes as you scroll, so
   it read as a dark bar sitting on the blue. It gets the same gradient
   instead, sized to the document and offset by the scroll position, so it
   is painting the exact slice of ramp it is covering. initNav keeps the
   two custom properties in sync. */
:root .nav--stuck {
  background-image: linear-gradient(180deg,
    var(--grad-1) 0%, var(--grad-2) 50%, var(--grad-3) 88%, var(--grad-4) 100%);
  background-repeat: no-repeat;
  background-size: 100% var(--doc-h, 100vh);
  background-position: 0 var(--nav-shift, 0px);
}

:root .footer { background: transparent; }
:root .footer h4,
:root .footer__brand p,
:root .footer__bottom { color: var(--text); }
/* Links already carry the foreground, but the .78 opacity that reads as a
   soft state on black is a legibility problem out on the bright end. */
:root .footer a { opacity: 1; }
/* The end note sits just above the footer, so on shorter pages it lands
   deep enough in the ramp for dim to fail (4.38 on About). Same fix. */
:root .end__row { color: var(--text); }
:root .footer__bottom { border-block-start-color: rgb(var(--text-rgb) / .28); }
:root { background: var(--grad-1); }
img, canvas, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 3px; }

html[dir='rtl'] body { font-family: 'IBM Plex Sans Arabic', var(--font-body); }
html[dir='rtl'] h1, html[dir='rtl'] h2, html[dir='rtl'] h3,
html[dir='rtl'] .display { font-family: 'IBM Plex Sans Arabic', var(--font-display); }
/* Arabic needs its own metrics: the tight Latin leading collides
   ascenders with the descenders of the line above, and negative
   tracking damages the joins between letters. */
html[dir='rtl'] .display,
html[dir='rtl'] h1, html[dir='rtl'] h2, html[dir='rtl'] h3 {
  line-height: 1.32;
  letter-spacing: 0;
  font-stretch: normal;
  /* Anton covers no Arabic, and casing is meaningless in the script */
  font-weight: 700;
  text-transform: none;
}
html[dir='rtl'] .hero h1 .accent { font-stretch: normal; -webkit-text-stroke-width: 1px; }

/* ---------- primitives ---------- */
.wrap { width: min(100% - (var(--gut) * 2), var(--wrap)); margin-inline: auto; }

.display {
  font-family: var(--font-display);
  font-weight: 400;          /* Anton ships a single weight */
  font-stretch: normal;      /* and no width axis to vary */
  letter-spacing: 0.005em;   /* condensed caps need a hair of air */
  line-height: 0.88;
  text-transform: uppercase;
}
/* Deliberately steep: the jump from headline to body is what carries
   hierarchy, so the page needs no decoration to feel structured. */
h1.display { font-size: clamp(50px, 8.2vw, 7rem); }
h2.display { font-size: clamp(35px, 5.4vw, 4rem); }
h3 { font-family: var(--font-body); font-weight: 700; font-stretch: 105%;
     letter-spacing: -0.02em; line-height: 1.1; }

/* A catalogue index mark, not a decorated pill. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  display: block;
}
.lede { font-size: clamp(1.05rem, 1.35vw, 1.24rem); line-height: 1.5; color: var(--dim); max-width: 54ch; }

/* angular cut — the logo's signature terminal */
.cut {
}

/* ---------- buttons ----------
   Square and flat. The bevel was applied to every surface on the page,
   which turns a brand detail into wallpaper; it is now used once, large,
   on the hero stage. No hover lift either — a translateY on every
   element is the tell that motion is standing in for hierarchy. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  padding: 0.9rem 1.5rem;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border: 1px solid transparent;
  transition: background .16s linear, color .16s linear, border-color .16s linear;
}
.btn--primary { background: var(--blue); color: #fff; border-color: var(--blue); }
.btn--primary:hover { background: transparent; color: var(--accent-on-bg); border-color: var(--accent-on-bg); }
.btn--ghost { border-color: var(--line-dark); color: var(--text); }
.btn--ghost:hover { border-color: var(--text); background: var(--text); color: var(--bg); }
.btn--onlight { border-color: var(--line-light); color: var(--text); }
.btn--onlight:hover { border-color: var(--text); }
.btn--lg { padding: 1.05rem 1.9rem; font-size: 0.86rem; }
.btn--block { width: 100%; }
.btn[disabled] { opacity: .38; pointer-events: none; }
.btn__arrow { transition: transform .16s linear; }
.btn:hover .btn__arrow { transform: translateX(4px); }
html[dir='rtl'] .btn__arrow { transform: scaleX(-1); }
html[dir='rtl'] .btn:hover .btn__arrow { transform: scaleX(-1) translateX(4px); }

/* ---------- logo ---------- */
.logo { display: inline-flex; align-items: center; gap: 0.65rem; }
.logo__mark {
  width: 30px; aspect-ratio: 4 / 3;
  background: currentColor;
  -webkit-mask: url('../img/voxel-mark.png') center / contain no-repeat;
          mask: url('../img/voxel-mark.png') center / contain no-repeat;
  flex: none;
}
.logo__word {
  width: 104px; aspect-ratio: 1600 / 405;
  background: currentColor;
  -webkit-mask: url('../img/voxel-wordmark.png') center / contain no-repeat;
          mask: url('../img/voxel-wordmark.png') center / contain no-repeat;
}

/* ============================================================
   NAV
   ============================================================ */
.nav {
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: 100;
  transition: background .3s var(--ease), border-color .3s var(--ease), backdrop-filter .3s;
  border-block-end: 1px solid transparent;
}
/* Opaque, with a hairline. Frosted-glass headers are the same borrowed
   vocabulary as the gradient blob. */
.nav--stuck {
  background: var(--bg);
  border-block-end-color: var(--line-dark);
}
.nav__in {
  display: flex; align-items: center; gap: 2rem;
  height: 74px;
}
.nav__links { display: flex; align-items: center; gap: 1.9rem; margin-inline-start: auto; }
.nav__link {
  font-family: var(--font-mono);
  font-size: 0.78rem; font-weight: 400; letter-spacing: 0.02em;
  color: var(--dim);
  position: relative; padding-block: 4px;
  transition: color .16s linear;
}
.nav__link::after {
  content: ''; position: absolute; inset-block-end: 0; inset-inline-start: 0;
  width: 0; height: 1px; background: currentColor;
  transition: width .18s linear;
}
.nav__link:hover, .nav__link[aria-current='page'] { color: var(--text); }
.nav__link:hover::after, .nav__link[aria-current='page']::after { width: 100%; }
.nav__actions { display: flex; align-items: center; gap: 0.85rem; }

/* Header booking CTA. Lives in .nav__actions rather than .nav__links so it
   stays on screen at every width instead of disappearing behind the
   burger — a booking button hidden in a menu is a booking button nobody
   presses. Filled blue because it is the one action in the header. */
.nav__book {
  font-family: var(--font-mono); font-weight: 500; font-size: 0.75rem;
  letter-spacing: 0.06em;
  padding: 0.5rem 0.9rem;
  background: var(--blue);
  color: #fff;
  border: 1px solid var(--blue);
  white-space: nowrap;
  transition: background .16s linear, border-color .16s linear;
}
.nav__book:hover, .nav__book:focus-visible { background: var(--blue-bright); border-color: var(--blue-bright); }
.nav__book:focus-visible { outline: 2px solid var(--accent-on-bg); outline-offset: 2px; }

.lang {
  font-family: var(--font-mono); font-weight: 500; font-size: 0.75rem;
  letter-spacing: 0.06em;
  padding: 0.5rem 0.8rem;
  border: 1px solid var(--line-dark);
  color: var(--dim);
  transition: color .2s, border-color .2s;
}
.lang:hover { color: var(--text); border-color: var(--blue); }


.nav__burger { display: none; width: 40px; height: 40px; position: relative; }
.nav__burger span {
  position: absolute; inset-inline: 9px; height: 2px; background: var(--text);
  transition: transform .28s var(--ease), opacity .2s;
}
.nav__burger span:nth-child(1) { top: 14px; }
.nav__burger span:nth-child(2) { top: 20px; }
.nav__burger span:nth-child(3) { top: 26px; }
body.menu-open .nav__burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
body.menu-open .nav__burger span:nth-child(2) { opacity: 0; }
body.menu-open .nav__burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 900px) {
  .nav__in { height: 62px; gap: 0.6rem; }
  .logo__word { width: 88px; }
  /* Exactly one auto margin, on the group — putting it on the burger
     too would push a gap between the two and strand the ع button. */
  .nav__actions { order: 2; margin-inline-start: auto; gap: 0.5rem; }
  .nav__burger { display: block; order: 3; width: 34px; height: 34px; }
  .nav__burger span { inset-inline: 6px; }
  .nav__burger span:nth-child(1) { top: 11px; }
  .nav__burger span:nth-child(2) { top: 16px; }
  .nav__burger span:nth-child(3) { top: 21px; }
  .lang { padding: 0.35rem 0.55rem; font-size: 0.74rem; }
  .nav__book { padding: 0.4rem 0.6rem; font-size: 0.7rem; }
  .nav__links {
    position: fixed; inset: 62px 0 auto 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--bg-alt);
    border-block-end: 1px solid var(--line-dark);
    padding: 0.5rem var(--gut) 1.5rem;
    transform: translateY(-120%);
    /* -120% is 120% of the panel's own height, but it starts 62px down, so
       a couple of pixels of it still land inside the viewport. Against the
       old near-black ground that was invisible; on the navy ramp it is a
       dark strip across the top, and it was swallowing clicks up there the
       whole time. visibility takes it out of paint and hit-testing, and is
       transitioned so it stays on screen for the closing slide. */
    visibility: hidden;
    transition: transform .38s var(--ease), visibility .38s;
    margin: 0;
  }
  body.menu-open .nav__links { transform: translateY(0); visibility: visible; }
  .nav__link { padding: 0.95rem 0; border-block-end: 1px solid var(--line-dark); font-size: 1.05rem; }
  .nav__link::after { display: none; }
}
/* On a phone the header CTA wraps and crowds the burger — the hero
   and the sticky WhatsApp button already cover the same action. */
@media (max-width: 560px) { .nav__actions .btn { display: none; } }

/* ============================================================
   SECTIONS
   ============================================================ */
.section { padding-block: clamp(4rem, 8vw, 7rem); position: relative; }
/* Alternating surfaces are gone. A single ground with hairline rules
   between sections reads as one continuous document rather than a
   stack of unrelated marketing panels. */
.section--light, .section--navy { background: transparent; }
.section + .section { border-block-start: 1px solid var(--line-dark); }

/* Label left, argument right — the spec-sheet split. This asymmetry is
   what stops every section opening with the same centred stack. */
.section__head {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 7fr);
  gap: clamp(1rem, 4vw, 3rem);
  margin-block-end: clamp(2.5rem, 5vw, 4rem);
  align-items: start;
}
.section__head .eyebrow { padding-block-start: 0.55rem; }
.section__head > :not(.eyebrow) { grid-column: 2; }
.section__head .lede { margin-block-start: 1.1rem; }
@media (max-width: 780px) {
  .section__head { grid-template-columns: 1fr; gap: 0.9rem; }
  .section__head > :not(.eyebrow) { grid-column: 1; }
}

/* No gradient blobs and no noise overlay. Depth comes from hairline
   rules, real spacing and type scale — the ambient-glow backdrop is
   the single most recognisable generated-template signature. */
.aura { position: relative; }
.grain { display: none; }

/* ============================================================
   HERO
   ============================================================ */
.hero {
  position: relative;
  padding-block: clamp(6rem, 13vw, 9.5rem) clamp(3rem, 6vw, 5rem);
  overflow: hidden;
}
/* Type-led: the headline takes the larger share and the render sits
   beside it as evidence, rather than the two splitting the page evenly. */
.hero__in {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: end;
}
.hero__copy { display: grid; gap: 1.5rem; justify-items: start; }
.hero h1 > span { display: block; }
/* Anton has neither a weight nor a width axis, so the old light-and-narrow
   contrast is impossible here — the second clause is separated by an
   outline instead, which is the poster-face way of doing it and keeps the
   accent colour free for actions. */
.hero h1 .accent {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--dim);
  text-stroke: 1.5px var(--dim);
}
/* Fallback: without text-stroke support the clause would vanish entirely. */
@supports not (-webkit-text-stroke: 1px black) {
  .hero h1 .accent { color: var(--dim); -webkit-text-stroke: 0; }
}
.hero__cta { display: flex; flex-wrap: wrap; gap: 0.9rem; }

.hero__stage {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--line-dark);
  /* Opaque, not tinted: the stage has to stay darker than the ramp
     behind it or a white or clear filament lit against it disappears. */
  background:
    radial-gradient(ellipse at 50% 104%, rgb(var(--text-rgb) / .07), transparent 60%),
    rgb(var(--surface-rgb));
  /* The one place the logo's angular terminal is used — once, large. */
  --cut: 26px;
  clip-path: polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut));
}
.hero__stage canvas { width: 100%; height: 100%; }
.hero__badge {
  position: absolute; inset-block-end: 16px; inset-inline-start: 18px;
  display: flex; align-items: center; gap: 0.5rem;
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--dim);
}
.hero__badge .dot { width: 6px; height: 6px; background: var(--blue); }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

@media (max-width: 900px) {
  .hero__in { grid-template-columns: 1fr; }
  /* Copy leads on a phone too. Putting the render first pushed the
     headline below the fold, which undercuts a type-led page. */
  .hero__stage { aspect-ratio: 4 / 3; }
}

/* ---------- marquee strip ---------- */
.strip { display: none; } /* ticker of adjectives — pure filler */
.strip--unused {
  border-block: 1px solid var(--line-dark);
  background: var(--bg-alt);
  overflow: hidden;
  padding-block: 1.05rem;
}
.strip__track {
  display: flex; gap: 3.5rem; width: max-content;
  animation: slide 34s linear infinite;
  font-family: var(--font-body); font-weight: 700; font-size: 0.82rem;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--dim);
}
.strip__track span { display: flex; align-items: center; gap: 3.5rem; }
.strip__track em {
  font-style: normal; white-space: nowrap;
  display: flex; align-items: center; gap: 3.5rem;
}
.strip__track em::after { content: '◆'; color: var(--blue); font-size: 0.6rem; }
@keyframes slide { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .strip__track { animation: none; } }

/* ============================================================
   CARDS
   ============================================================ */
/* ---------- cells ----------
   Not floating cards: each item hangs off a hairline rule, the way a
   printed catalogue sets a column. No box, no shadow, no lift. */
.cards { display: grid; gap: 0; }
.cards--3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.cards--4 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }

.card {
  position: relative;
  padding: 1.5rem 2rem 2.5rem 0;
  border-block-start: 1px solid var(--text);
  display: grid; gap: 0.7rem; align-content: start;
  background: none;
}
.card h3 { font-size: 1.18rem; }
.card p { color: var(--dim); font-size: 0.95rem; max-width: 40ch; }
.section--light .card { background: none; border-block-start-color: var(--text); }

/* An index number, not a decorative watermark. */
.card__num {
  font-family: var(--font-mono);
  font-size: 0.72rem; font-weight: 500; letter-spacing: 0.1em;
  color: var(--dim);
  margin-block-end: 0.2rem;
}

/* ---------- spec table ----------
   The materials list is data, so it is set as data: tabular figures,
   monospace, hairline rows. */
.spec { width: 100%; border-collapse: collapse; }
.spec thead th {
  font-family: var(--font-mono); font-size: 0.68rem; font-weight: 500;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim);
  text-align: start; padding: 0 1rem 0.8rem 0;
  border-block-end: 1px solid var(--text);
  white-space: nowrap;
}
.spec tbody td {
  padding: 1.15rem 1rem 1.15rem 0;
  border-block-end: 1px solid var(--line-dark);
  vertical-align: baseline;
  font-size: 0.95rem;
}
.spec tbody tr:last-child td { border-block-end: none; }
.spec__name {
  font-family: var(--font-body); font-weight: 700; font-stretch: 105%;
  font-size: 1.05rem; letter-spacing: -0.01em; white-space: nowrap;
}
.spec__use { color: var(--dim); }
.spec__dots { display: flex; gap: 5px; }
.spec__dots i { width: 11px; height: 11px; display: block; outline: 1px solid var(--line-dark); }
@media (max-width: 720px) {
  .spec, .spec tbody, .spec tr, .spec td { display: block; width: 100%; }
  .spec thead { display: none; }
  .spec tbody tr { border-block-end: 1px solid var(--line-dark); padding-block: 1.1rem; }
  .spec tbody tr:last-child { border-block-end: none; }
  .spec tbody td { border: none; padding: 0.15rem 0; }
  .spec__name { font-size: 1.15rem; }
}

/* ============================================================
   STUDIO
   ============================================================ */
.studio { padding-block: 6.5rem 4rem; min-height: 100vh; }
.studio__head { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 1.5rem; margin-block-end: 2rem; }

.tabs { display: flex; border: 1px solid var(--line-dark); }
.tab {
  padding: 0.8rem 1.4rem;
  font-family: var(--font-mono); font-weight: 500; font-size: 0.78rem;
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--dim);
  transition: background .16s linear, color .16s linear;
}
.tab[aria-selected='true'] { background: var(--text); color: var(--bg); }

.studio__grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 1.5rem; align-items: start; }
@media (max-width: 1000px) { .studio__grid { grid-template-columns: 1fr; } }

.viewer {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--line-dark);
  /* A faint floor bounce stages the object; it is photographic, not
     decorative, so it stays — just far weaker than the old glow. */
  background:
    radial-gradient(ellipse at 50% 112%, rgb(var(--text-rgb) / .07), transparent 58%),
    var(--bg-alt);
  overflow: hidden;
  position: sticky; top: 92px;
}
/* relative, never static — the tools, readouts and drop zone are
   absolutely positioned and need .viewer to stay their containing block */
@media (max-width: 1000px) { .viewer { position: relative; top: auto; aspect-ratio: 4 / 3; } }
.viewer canvas { width: 100%; height: 100%; touch-action: none; }

.viewer__drop {
  position: absolute; inset: 0; z-index: 3;
  display: grid; place-content: center; justify-items: center; gap: 1rem;
  text-align: center; padding: 2rem;
  background: rgb(var(--deep-rgb) / .9);
  transition: opacity .25s, visibility .25s;
}
.viewer__drop[hidden] { display: none; }
.viewer.is-dragging .viewer__drop { background: rgb(var(--accent-rgb) / .22); }
.viewer__drop svg { width: 54px; height: 54px; stroke: var(--accent-on-bg); }
.viewer__drop h3 { font-size: 1.3rem; }
.viewer__drop p { color: var(--dim); font-size: 0.9rem; max-width: 34ch; }
.viewer__hint {
  position: absolute; inset-block-end: 12px; inset-inline-start: 14px; z-index: 2;
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--dim); pointer-events: none;
}
.viewer__dims {
  position: absolute; inset-block-start: 12px; inset-inline-start: 14px; z-index: 2;
  display: grid; gap: 2px;
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--dim);
  font-variant-numeric: tabular-nums; pointer-events: none;
}
.viewer__dims b { color: var(--text); font-weight: 600; }
.viewer__name {
  font-family: var(--font-body); font-size: 0.82rem; font-weight: 600; color: var(--text);
  max-width: 26ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  margin-block-end: 2px;
}
.viewer__tools {
  position: absolute; inset-block-start: 12px; inset-inline-end: 12px; z-index: 4;
  display: flex; gap: 6px;
}
.viewer__tool {
  width: 34px; height: 34px; display: grid; place-content: center;
  border: 1px solid var(--line-dark); background: rgb(var(--deep-rgb) / .7);
  color: var(--dim); font-size: 0.9rem;
  transition: color .2s, border-color .2s;
}
.viewer__tool:hover { color: var(--text); border-color: var(--blue); }
.viewer__tool[aria-pressed='true'] { color: var(--accent-on-bg); border-color: var(--blue); }

.hint--touch { display: none; }
@media (pointer: coarse) {
  .hint--mouse { display: none; }
  .hint--touch { display: inline; }
  /* A full-width square canvas that swallows every swipe is a scroll
     dead-end. Handing vertical pans back to the page leaves horizontal
     drags for the orbit controls — which is the axis the model spins on. */
  .viewer canvas { touch-action: pan-y; }
}

/* Nothing loaded yet: the orbit hint and the rotate/wireframe/reset
   tools have nothing to act on, and they overlap the drop panel. */
.viewer:has([data-drop]:not([hidden])) .viewer__tools,
.viewer:has([data-drop]:not([hidden])) .viewer__hint { display: none; }

/* At the TOP of the viewer: at the bottom it sat below the fold on a
   laptop. One line per refused file; a click closes it. */
.viewer__err {
  position: absolute; inset-block-start: 0; inset-inline: 0; z-index: 7;
  padding: 0.85rem 2.4rem 0.85rem 1.1rem;
  background: var(--danger); color: #fff; font-size: 0.88rem;
  white-space: pre-line; cursor: pointer;
}
:where([dir="rtl"]) .viewer__err { padding: 0.85rem 1.1rem 0.85rem 2.4rem; }
.viewer__err::after {
  content: "×"; position: absolute; inset-block-start: 0.55rem; inset-inline-end: 0.9rem;
  font-size: 1.3rem; line-height: 1;
}
/* the row number in the price breakdown, kept apart from the material name
   ("01 PLA", not "PLA01" once Arabic reorders the line) */
.quote__n { margin-inline-end: 0.5em; }
.field__hint { color: var(--dim); font-size: 0.75rem; line-height: 1.4; }
/* a part bigger than the bed, a rush that cannot be one: said, not blocked */
.field__warn, .quote__warn {
  display: block; margin-block-start: 0.4rem; padding: 0.55rem 0.75rem;
  border-inline-start: 3px solid #f2c53d; background: rgb(242 197 61 / .1);
  color: var(--text); font-size: 0.8rem; line-height: 1.45;
}
.field__warn[hidden] { display: none; }
.form__err {
  margin: 0.35rem 0 0; padding: 0.6rem 0.8rem;
  border: 1px solid var(--danger); color: #fff; background: rgb(from var(--danger) r g b / .18);
  font-size: 0.85rem; line-height: 1.5;
}
.form__err[hidden] { display: none; }
[aria-invalid="true"] { border-color: var(--danger) !important; }
.viewer__err[hidden] { display: none; }

/* Reading a file. Opaque enough to say "not ready", and the spinner is a
   transform animation so the compositor keeps it turning while the main
   thread is busy parsing — a frozen spinner would say "crashed". */
.viewer__loading {
  position: absolute; inset: 0; z-index: 6;
  display: grid; place-content: center; justify-items: center; gap: 1rem;
  padding: 2rem; text-align: center;
  background: rgb(var(--deep-rgb) / .86);
  font-family: var(--font-mono); font-size: 0.8rem; color: var(--text);
}
.viewer__loading p { max-width: 34ch; overflow-wrap: anywhere; }
.viewer__spin {
  inline-size: 34px; block-size: 34px; border-radius: 50%;
  border: 3px solid rgb(var(--text-rgb) / .18); border-block-start-color: var(--accent-on-bg);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* options panel */
.panel { display: grid; gap: 1.25rem; }
.field { display: grid; gap: 0.55rem; }
.field__label {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--dim);
}
.field__val { color: var(--text); letter-spacing: 0; font-variant-numeric: tabular-nums; }
/* The totals label gets extra tracking. As an inline style it outranked
   every stylesheet rule including the RTL reset, so it stayed spaced-out
   in Arabic; as a class it obeys the cascade like everything else. */
.field__label--wide { letter-spacing: 0.14em; }

.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.chip {
  padding: 0.55rem 0.9rem;
  border: 1px solid var(--line-dark);
  font-family: var(--font-mono); font-size: 0.78rem; color: var(--dim);
  transition: border-color .16s linear, color .16s linear, background .16s linear;
  display: flex; align-items: center; gap: 0.45rem;
}
.chip:hover { border-color: var(--text); color: var(--text); }
.chip[aria-pressed='true'] { border-color: var(--text); background: var(--text); color: var(--bg); }
.chip small { color: var(--dim); font-size: 0.76rem; }
/* --dim is the foreground at 62%, which is near-white — and a pressed chip
   fills with that same near-white. The secondary half of every selected
   chip (the price, the layer height) was invisible: 1.0:1. It takes the
   ink colour on that ground instead. */
.chip[aria-pressed='true'] small { color: rgb(var(--deep-rgb) / .72); }

/* The swatch is a colour AND a name. It used to be a 38px button with no
   rule for either child, so the colour block collapsed to nothing and the
   name overflowed the box — a colour picker showing no colours. The
   chip carries the colour, the label sits under it, and the item is wide
   enough for the longest name in either language ("Voxel Blue",
   "أزرق فوكسل"), which is what was tearing out of the old box. */
.swatches { display: flex; flex-wrap: wrap; gap: 0.7rem; }
.swatch {
  inline-size: 66px;
  display: grid; gap: 0.35rem; justify-items: center;
  padding: 0; background: none; border: 0;
}
.swatch > span {
  inline-size: 38px; block-size: 38px; display: block;
  border: 2px solid transparent; outline: 1px solid var(--line-dark);
  transition: transform .2s var(--ease), border-color .2s, outline-color .2s;
}
.swatch > small {
  font-size: 0.64rem; line-height: 1.3; text-align: center; color: var(--dim);
  transition: color .2s;
}
.swatch:hover > span { outline-color: var(--text); transform: translateY(-1px); }
.swatch[aria-pressed='true'] > span { border-color: var(--accent-on-bg); outline-color: var(--accent-on-bg); }
.swatch[aria-pressed='true'] > small { color: var(--text); }

/* An object in several colours gets one button per colour above the
   swatches — the swatches change whichever is pressed. Each shows the
   colour it prints in now, so the row doubles as the object's palette. */
.colourtab { padding: 0.45rem 0.75rem; }
.colourtab__dot {
  flex: none; inline-size: 12px; block-size: 12px; border-radius: 50%;
  background: var(--dot); box-shadow: 0 0 0 1px rgb(var(--text-rgb) / .45);
}
.colourtab[aria-pressed='true'] .colourtab__dot { box-shadow: 0 0 0 1px rgb(var(--deep-rgb) / .55); }
.field__hint { font-size: 0.74rem; line-height: 1.45; color: var(--dim); }

input[type='range'] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 3px; background: var(--line-dark); cursor: pointer;
}
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; background: var(--blue); border: 3px solid var(--bg);
  cursor: pointer; transition: transform .15s;
}
input[type='range']::-webkit-slider-thumb:hover { transform: scale(1.15); }
input[type='range']::-moz-range-thumb {
  width: 18px; height: 18px; background: var(--blue); border: 3px solid var(--bg);
  border-radius: 0; cursor: pointer;
}
/* Size and quantity wait for a model; say so rather than ignore the drag. */
input[type='range']:disabled, .stepper:has(:disabled) { opacity: .4; cursor: not-allowed; }
input[type='range']:disabled::-webkit-slider-thumb { cursor: not-allowed; }
.stepper button:disabled { pointer-events: none; }

.input, select.input, textarea.input {
  width: 100%;
  padding: 0.8rem 1rem;
  background: rgb(var(--deep-rgb) / .6);
  border: 1px solid var(--line-dark);
  color: var(--text);
  transition: border-color .2s;
}
.input:focus { border-color: var(--blue); outline: none; }
.input::placeholder { color: rgb(var(--text-rgb) / .35); }
textarea.input { resize: vertical; min-height: 84px; }
select.input { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--dim) 50%), linear-gradient(135deg, var(--dim) 50%, transparent 50%); background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%; background-size: 5px 5px; background-repeat: no-repeat; }
html[dir='rtl'] select.input { background-position: 13px 50%, 18px 50%; }

.stepper { display: flex; border: 1px solid var(--line-dark); width: max-content; }
.stepper button { width: 42px; height: 42px; color: var(--dim); font-size: 1.1rem; transition: color .2s, background .2s; }
.stepper button:hover { color: var(--text); background: rgb(var(--accent-rgb) / .16); }
.stepper input { width: 58px; text-align: center; background: none; border: none; border-inline: 1px solid var(--line-dark); font-family: var(--font-body); font-weight: 700; }
.stepper input::-webkit-outer-spin-button, .stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* quote card */
.quote {
  border: 1px solid rgb(var(--accent-rgb) / .45);
  background: linear-gradient(160deg, rgb(var(--accent-rgb) / .16), rgb(var(--surface-rgb) / .7));
  padding: 1.5rem;
  display: grid; gap: 1rem;
}
.quote__total { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.quote__total .n { font-family: var(--font-body); font-weight: 800; font-stretch: 112%;
  font-size: clamp(2.1rem, 5vw, 2.9rem); line-height: 1; letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums; }
.quote__total .n small { font-size: 0.9rem; color: var(--dim); font-weight: 500; margin-inline-start: .35rem; }
.quote__rows { display: grid; gap: 0.45rem; font-family: var(--font-mono); font-size: 0.78rem;
  font-variant-numeric: tabular-nums; }
.quote__row { display: flex; justify-content: space-between; gap: 1rem; color: var(--dim); }
.quote__row b { color: var(--text); font-weight: 500; }
.quote__row--sep { border-block-start: 1px solid var(--line-dark); padding-block-start: 0.45rem; margin-block-start: 0.25rem; }
.quote__note { font-size: 0.78rem; color: var(--dim); line-height: 1.5; }
.quote.is-stale { opacity: .5; }

/* ============================================================
   FILES IN THIS ORDER
   ------------------------------------------------------------
   An order is a list of parts, and the colour lives on the part.
   So the colour has to be visible on every row without opening
   anything — the dot is the primary information here, not
   decoration: it is how you check at a glance that you have not
   left two files on the same filament by accident.
   ============================================================ */
.parts { gap: 0.6rem; }
.parts__list { list-style: none; display: grid; gap: 0.4rem; margin: 0; padding: 0; }

.part { display: flex; align-items: stretch; gap: 0.35rem; }
.part__pick {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.6rem 0.7rem; text-align: start;
  border: 1px solid var(--line-dark);
  background: rgb(var(--deep-rgb) / .5);
  transition: border-color .2s, background .2s;
}
.part__pick:hover { border-color: rgb(var(--text-rgb) / .3); }
.part.is-active .part__pick { border-color: var(--accent-on-bg); background: rgb(var(--accent-rgb) / .1); }

.part__no {
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--dim);
  font-variant-numeric: tabular-nums; flex: none;
}
/* Ringed rather than bare: half the palette is near-black or near-white,
   and either one disappears against some part of the page gradient. */
.part__dot {
  flex: none; width: 16px; height: 16px; border-radius: 50%;
  background: var(--dot);
  box-shadow: 0 0 0 1px rgb(var(--text-rgb) / .45);
}
.part__body { min-width: 0; display: grid; gap: 0.1rem; }
/* A painted part shows its other colours after its own, smaller: the row
   leads with the main colour; all of them are changed in the Colour field. */
.part__dot--paint { width: 10px; height: 10px; align-self: center; }
.part__dot { position: relative; }
/* The file's own colour, as a badge on the stocked one — shown only when
   the nearest filament we stock visibly is not what the designer used. */
.part__was {
  position: absolute; inset-block-end: -3px; inset-inline-end: -4px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--dot); box-shadow: 0 0 0 1.5px rgb(var(--deep-rgb));
}
.quote__dot--paint { width: 7px; height: 7px; margin-inline-start: -0.25rem; }
.part__name {
  font-size: 0.85rem; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.part__meta { font-family: var(--font-mono); font-size: 0.68rem; color: var(--dim); }

.part__x {
  flex: none; width: 34px;
  border: 1px solid var(--line-dark); color: var(--dim);
  font-size: 1.1rem; line-height: 1;
  transition: border-color .2s, color .2s;
}
.part__x:hover { border-color: var(--danger); color: var(--text); }

.parts__add { margin-block-start: 0.2rem; }
/* A body row is not removable — the shells come from the mesh, not from
   the customer — so it has no × and the whole row is the button. */
/* One block per FILE. A file is not a row — there is nothing to set on a
   file — but it has to be visibly its own thing, or a one-object file
   dropped after a nine-object one reads as a tenth object of the file
   above. That is exactly what happened. Hence a real container with a
   gap between groups, not just a heading. */
.parts__list { gap: 0.85rem; }
.filegroup { display: grid; gap: 0.4rem; }
/* Only a split file gets a rule and an indent: a single-object file IS
   its row, and boxing one row would be decoration without information. */
.filegroup--split {
  border-inline-start: 2px solid var(--line-dark);
  padding-inline-start: 0.7rem;
}
.filegroup__list { list-style: none; display: grid; gap: 0.4rem; margin: 0; padding: 0; }
.filegroup__head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  font-family: var(--font-mono); font-size: 0.66rem;
  text-transform: uppercase; letter-spacing: 0.07em; color: var(--dim);
}
.filegroup__name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-transform: none; letter-spacing: 0; color: var(--text);
}
.filegroup__n { flex: none; margin-inline-start: auto; }
.filegroup__head { align-items: center; }
.filegroup__x {
  flex: none; inline-size: 26px; block-size: 26px; display: grid; place-content: center;
  border: 1px solid var(--line-dark); color: var(--dim); font-size: 0.95rem; line-height: 1;
  transition: border-color .2s, color .2s;
}
.filegroup__x:hover { border-color: var(--danger); color: var(--text); }
/* A 42-object file put the material and colour controls 2,000px below the
   list, so every colour change was a long scroll down and back. The list
   scrolls inside itself past a screenful; the controls stay close. */
.parts__list { max-block-size: min(58vh, 34rem); overflow-y: auto; padding-inline-end: 2px; }
/* A file needing three filaments shows three dots, so the row says how
   many colours it takes before you open anything. */
.part__dots { display: flex; flex: none; gap: 3px; }

/* The AMS meter. Present from the first file, not sprung at checkout —
   somebody choosing a fifth colour should see the cost of it as they do
   it, not after they have committed to the order. */
.slots {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line-dark);
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--dim);
}
.slots__dots { display: flex; gap: 3px; }
.slots__dots .quote__dot { margin-inline-end: 0; }
.slots__label { flex: 1; min-width: 0; }
.slots__n { color: var(--text); font-variant-numeric: tabular-nums; }
.slots.is-over { border-color: rgb(var(--accent-bright-rgb) / .7); }
.slots.is-over .slots__n { color: var(--accent-on-bg); }
.parts__note--warn {
  border-inline-start: 2px solid var(--accent-on-bg);
  padding-inline-start: 0.6rem; color: var(--text);
}
.sheet__bodies {
  display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem;
  padding-inline-start: 1.1rem; font-size: 0.78rem; color: var(--dim);
}
.sheet__bodies span { display: flex; align-items: center; }
.parts__note, .parts__scope {
  font-size: 0.75rem; line-height: 1.55; color: var(--dim);
}
/* The 3MF line is advice about the selected file rather than about the
   order, so it reads as an aside on the note above it. */
.parts__note--3mf {
  border-inline-start: 2px solid rgb(var(--accent-bright-rgb) / .55);
  padding-inline-start: 0.6rem;
  color: var(--text);
}
.parts__editing {
  font-family: var(--font-mono); font-size: 0.7rem; color: var(--dim);
  text-transform: uppercase; letter-spacing: 0.08em;
  padding-block-start: 0.4rem;
}
.parts__editing b { color: var(--text); letter-spacing: 0; text-transform: none; }

/* The same dot in the price breakdown and the order sheet, so a row
   there reads as the same thing as a row in the list above. */
.quote__dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  background: var(--dot); margin-inline-end: 0.45rem;
  box-shadow: 0 0 0 1px rgb(var(--text-rgb) / .4);
}
.quote__row--part > span { display: flex; align-items: center; min-width: 0; }
.quote__row--part b[dir='ltr'] { color: var(--dim); margin-inline-end: 0.4rem; }

/* Dots in their own column: a part with four colours used to wrap its
   dots onto a line of their own, above its name. */
.sheet__part {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start;
  column-gap: 0.55rem; font-size: 0.85rem;
}
.sheet__dots { display: flex; align-items: center; padding-block-start: 0.35rem; }
.sheet__what { display: flex; flex-wrap: wrap; column-gap: 0.45rem; min-width: 0; }
.sheet__what > span { overflow-wrap: anywhere; }

/* Name + note for the selected part. Grid rather than the usual .field
   stack because the two labels and two controls alternate. */
.part-meta { gap: 0.4rem; }
.part-meta textarea.input { resize: vertical; min-height: 3.4rem; line-height: 1.5; }
/* A written note is worth flagging on the row — otherwise it is invisible
   until you click back into that part. */
.part__hasnote { font-style: normal; color: var(--accent-on-bg); margin-inline-start: 0.3rem; }
.part__hasnote svg { display: inline-block; width: 11px; height: 11px; vertical-align: -1px;
  fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }

/* Rename on the row itself. The pencil is there for touch and for anyone
   who does not know F2 or the second click; it shows on the selected row
   and on hover so fifty rows are not fifty pencils. */
.part__ren {
  flex: none; width: 34px; display: grid; place-content: center;
  border: 1px solid var(--line-dark); color: var(--dim);
  opacity: 0; transition: opacity .15s, border-color .2s, color .2s;
}
.part__ren svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; }
.part:hover .part__ren, .part.is-active .part__ren, .part:focus-within .part__ren { opacity: 1; }
.part__ren:hover, .part__ren:focus-visible { border-color: var(--text); color: var(--text); }
.part.is-active .part__name { cursor: text; }
.part.is-renaming .part__pick { border-color: var(--accent-on-bg); background: rgb(var(--accent-rgb) / .1); cursor: default; }
/* the box takes the row's width, not just the old name's */
.part.is-renaming .part__body { flex: 1; }
.part__rename {
  width: 100%; min-width: 0; font: inherit; font-size: 0.85rem; color: var(--text);
  background: rgb(var(--deep-rgb)); border: 1px solid var(--accent-on-bg);
  padding: 0.1rem 0.4rem; outline: none;
}
.part__rename::selection { background: rgb(var(--accent-rgb) / .55); }

/* The right-click menu on a row. */
.ctxmenu {
  position: fixed; z-index: 300; min-width: 180px; padding: 0.3rem;
  display: grid; background: var(--bg-alt); border: 1px solid var(--line-dark);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .5);
}
.ctxmenu button {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1.6rem;
  padding: 0.55rem 0.7rem; text-align: start; font-size: 0.85rem; color: var(--text);
}
.ctxmenu button:hover, .ctxmenu button:focus-visible { background: rgb(var(--accent-rgb) / .28); outline: none; }
.ctxmenu kbd { font-family: var(--font-mono); font-size: 0.68rem; color: var(--dim); }
@media (pointer: coarse) { .ctxmenu kbd { display: none; } }
.sheet__note {
  padding-inline-start: 1.1rem; font-size: 0.8rem; color: var(--dim);
  line-height: 1.5; white-space: pre-wrap;
}

/* ============================================================
   PRESETS
   ============================================================ */
.presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.9rem; }
.preset {
  border: 1px solid var(--line-dark);
  background: rgb(var(--deep-rgb) / .5);
  padding: 0.9rem;
  display: grid; gap: 0.5rem; text-align: start;
  transition: border-color .2s, transform .2s var(--ease);
}
.preset:hover { border-color: var(--text); }
.preset[aria-pressed='true'] { border-color: var(--text); background: rgb(var(--text-rgb) / .06); }
.preset__thumb {
  aspect-ratio: 1; width: 100%;
  background: rgb(var(--text-rgb) / .04);
  /* flex, not grid: a centred grid area is content-sized, so a
     percentage width on the SVG would resolve against nothing */
  display: flex; align-items: center; justify-content: center;
}
.preset__thumb svg { width: 62%; height: 62%; stroke: var(--text); fill: none; stroke-width: 2.4; }
.preset__name { font-family: var(--font-body); font-weight: 700; font-size: 0.92rem; letter-spacing: -0.01em; }
.preset__meta { font-family: var(--font-mono); font-size: 0.7rem; color: var(--dim); }

/* ============================================================
   ORDER SHEET
   ============================================================ */
.sheet {
  position: fixed; inset: 0; z-index: 210;
  background: rgb(var(--deep-rgb) / .94); backdrop-filter: blur(10px);
  display: grid; place-items: center; padding: var(--gut);
  opacity: 0; visibility: hidden; transition: opacity .28s, visibility .28s;
  overflow-y: auto;
}
.sheet.is-open { opacity: 1; visibility: visible; }
.sheet__box {
  width: min(100%, 560px);
  background: var(--bg-alt); border: 1px solid var(--line-dark);
  padding: clamp(1.5rem, 4vw, 2.4rem);
  display: grid; gap: 1.15rem;
  transform: translateY(16px); transition: transform .32s var(--ease);
}
.sheet.is-open .sheet__box { transform: none; }
.sheet__row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; }
@media (max-width: 520px) { .sheet__row { grid-template-columns: 1fr; } }
.sheet__summary {
  padding: 0.9rem 1.1rem; border: 1px solid var(--line-dark);
  background: rgb(var(--deep-rgb) / .5); font-size: 0.86rem; display: grid; gap: 0.3rem;
}
.sheet__summary b { color: var(--accent-on-bg); }

.done { display: grid; gap: 1rem; justify-items: center; text-align: center; padding: 1rem 0; }
/* ============================================================
   BANDS + FOOTER
   ============================================================ */
.band {
  border-block-start: 1px solid var(--text);
  background: none;
  padding: clamp(1.8rem, 4vw, 2.6rem) 0 0;
  display: grid; grid-template-columns: 1.4fr auto; gap: 2rem; align-items: center;
}
@media (max-width: 820px) { .band { grid-template-columns: 1fr; } }
.band h2 { margin-block-end: 0.6rem; }

/* Full-bleed like the spec table: across the page the rule is that
   section heads are indented and their content runs the full measure. */
.faq { display: grid; gap: 0; border-block-start: 1px solid var(--text); }
.faq__q {
  width: 100%; text-align: start;
  padding: 1.4rem 0; border-block-end: 1px solid var(--line-dark);
  display: flex; justify-content: space-between; align-items: center; gap: 1.5rem;
  font-family: var(--font-body); font-weight: 700; font-size: 1.15rem;
  letter-spacing: -0.015em; transition: color .16s linear;
}
.faq__q:hover { color: var(--dim); }
.faq__q i { font-style: normal; font-family: var(--font-mono); color: var(--dim); font-size: 1.1rem; transition: transform .22s linear; flex: none; }
.faq__q[aria-expanded='true'] i { transform: rotate(45deg); }
.faq__a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .32s var(--ease); border-block-end: 1px solid var(--line-dark); }
.faq__a[data-open='true'] { grid-template-rows: 1fr; }
.faq__a > div { overflow: hidden; }
.faq__a p { color: var(--dim); padding-block: 0 1.4rem; max-width: 62ch; }

.footer { background: var(--footer-bg); padding-block: clamp(3rem, 7vw, 5rem) 2rem; }
.footer__grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 2.5rem; margin-block-end: 3rem; }
@media (max-width: 760px) { .footer__grid { grid-template-columns: 1fr 1fr; } .footer__brand { grid-column: 1 / -1; } }
.footer__brand p { color: var(--dim); font-size: 0.92rem; max-width: 38ch; margin-block-start: 1rem; }
.footer h4 { font-family: var(--font-mono); font-size: 0.68rem; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--dim); margin-block-end: 1.1rem; }
.footer ul { list-style: none; padding: 0; display: grid; gap: 0.65rem; }
.footer a { color: var(--text); font-size: 0.92rem; opacity: .78; transition: opacity .16s linear; }
.footer a:hover { opacity: 1; }
/* On a touchscreen the footer links get a finger-sized height; the gap
   shrinks by the same amount, so the list looks as it did. */
@media (pointer: coarse) {
  .footer ul { gap: 0; }
  .footer ul a { display: inline-block; padding-block: 0.33rem; }
}
.footer__bottom {
  border-block-start: 1px solid rgb(var(--text-rgb) / .12); padding-block-start: 1.6rem;
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem;
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--dim);
}

/* whatsapp float */
.wa {
  position: fixed; inset-block-end: 22px; inset-inline-end: 22px; z-index: 90;
  width: 54px; height: 54px; border-radius: 50%;
  background: #25d366; display: grid; place-content: center;
  box-shadow: 0 10px 30px var(--shadow);
  transition: transform .25s var(--ease);
}
.wa:hover { filter: brightness(1.08); }
.wa { transition: transform .25s var(--ease), opacity .2s linear, visibility .2s; }
body.wa-away .wa { opacity: 0; visibility: hidden; transform: translateY(14px); }
/* The last footer line ran under the button; give the page room past it. */
body:has(> .wa) .footer { padding-block-end: calc(2rem + 64px); }
.wa svg { width: 28px; height: 28px; fill: #fff; }

/* ---------- utils ---------- */
/* Deliberately no scroll-reveal. Fade-and-rise on every block is one of
   the loudest generated-template signals, and it delays the content for
   no benefit. Class kept so the markup stays valid. */
.reveal { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
.sr { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }


/* ============================================================
   ARABIC: NO TRACKING, ANYWHERE
   ------------------------------------------------------------
   Arabic letters join into words. Any positive tracking pulls
   those joins apart, so a label like ابدأ طلبك renders as loose
   disconnected glyphs — the script's equivalent of s p a c e d
   out text, except it also breaks the letterforms themselves.

   The site puts 0.06–0.2em of tracking on every mono label:
   eyebrows, nav, buttons, meta rows, spec tables, the footer
   headings, the totals bar. Fixing them one by one would mean
   ~40 overrides and would miss the next one added, so RTL drops
   tracking wholesale. Latin fragments inside Arabic pages (STL,
   PLA, 2026) lose their tracking too, which costs nothing.

   Scoped under `body` purely so it outranks the single-class
   rules it needs to beat regardless of source order.
   ============================================================ */
html[dir='rtl'] body * { letter-spacing: normal; }

/* Email addresses and URLs are Latin strings: they read left-to-right even
   on an Arabic page, and typing one into an RTL field puts the cursor and
   the dots in the wrong places. Browsers already do this for type="tel",
   which is why the phone field looked right and the email field did not. */
html[dir='rtl'] input[type='email'],
html[dir='rtl'] input[type='url'] { direction: ltr; text-align: start; }

/* IBM Plex Mono carries no Arabic, so every mono label — nav, buttons,
   eyebrows, spec tables, the totals bar — fell through to the generic
   `monospace` family, which renders Arabic as ISOLATED letterforms: the
   word is spelled correctly but the letters never join, which is why
   ابدأ طلبك looked broken apart. Measured: joined width / isolated width
   was exactly 1.000 through the old stack, versus 0.797 through a real
   Arabic face.

   Plex Mono stays first so Latin and every digit keep the monospace
   look the whole design rests on; the Arabic face is inserted only as
   the fallback that actually shapes the script. */
html[dir='rtl'] {
  --font-mono: 'IBM Plex Mono', 'IBM Plex Sans Arabic', ui-monospace, monospace;
}

/* ============================================================
   PHONE
   Everything below is layout that only makes sense on a small,
   one-thumb screen — not just shrunken desktop.
   ============================================================ */
@media (max-width: 640px) {
  :root { --cut: 10px; }

  /* --- type: the desktop clamps bottom out too large at 375px --- */
  h1.display { font-size: clamp(34px, 9.5vw, 2.9rem); }
  h2.display { font-size: clamp(27px, 7vw, 2.2rem); }
  .lede { font-size: 0.98rem; }
  .eyebrow { font-size: 0.66rem; letter-spacing: 0.18em; }

  /* --- hero: 3D first, then the pitch --- */
  .hero { min-height: 0; padding-block: 6rem 3rem; }
  .hero__in { gap: 2rem; }
  .hero__stage { aspect-ratio: 1 / 1; }
  .hero__cta { width: 100%; gap: 0.7rem; }
  .hero__cta .btn { flex: 1 1 100%; }
  .hero__badge {
    font-size: 0.58rem; letter-spacing: 0.1em;
    inset-inline-start: 14px; inset-inline-end: 14px;
    inset-block-end: 12px;
  }

  .section { padding-block: 3.5rem; }
  .section__head { margin-block-end: 2rem; }
  .card { padding: 1.5rem 1.35rem; }
  .stat { padding: 1.35rem 1.1rem; }
  .band { padding: 1.75rem 1.4rem; gap: 1.4rem; }
  .band .btn { width: 100%; }

  /* --- studio --- */
  .studio { padding-block-start: 5.25rem; }
  .studio__head { gap: 1.1rem; }
  .tabs { width: 100%; }
  .tab {
    flex: 1; padding: 0.75rem 0.4rem;
    font-size: 0.84rem; white-space: nowrap; text-align: center;
  }
  /* The drop panel already carries a "Choose a file" button — the one
     in the options column is a duplicate with no drag affordance. */
  [data-pane='upload'] { display: none; }

  .viewer { aspect-ratio: 1 / 1; }
  .viewer__drop { padding: 1.25rem; gap: 0.7rem; }
  .viewer__drop svg { width: 40px; height: 40px; }
  .viewer__drop h3 { font-size: 1.05rem; }
  .viewer__drop p { font-size: 0.84rem; }
  /* Under the tool row: side by side they collide at phone width once the
     size runs to three digits a side (and a fourth tool, isolate, appears). */
  .viewer__dims { font-size: 0.68rem; inset-block-start: 54px; }

  .presets { grid-template-columns: repeat(2, 1fr); }
  .chip { padding: 0.55rem 0.8rem; font-size: 0.84rem; }
  .swatch { inline-size: 58px; }
  .swatch > span { inline-size: 34px; block-size: 34px; }

  /* Room for the sticky total bar so it never covers the last field */
  .panel { padding-block-end: 4.5rem; }

  /* --- order sheet: full-bleed sheet reads better than a floating card --- */
  /* overflow hidden on the overlay so only the sheet body scrolls —
     otherwise the two compete and the sheet sits past the fold.
     dvh keeps it honest when a phone's URL bar collapses. */
  /* Pinned to the bottom edge rather than grid-aligned: with a capped
     height the auto row sizes off the uncapped content and pushes the
     sheet past the fold. dvh keeps it right when the URL bar collapses. */
  .sheet { display: block; padding: 0; overflow: hidden; }
  .sheet__box {
    position: absolute; inset-inline: 0; inset-block-end: 0;
    width: 100%;
    max-height: 90vh; max-height: 90dvh;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    clip-path: none; border-inline: none; border-block-end: none;
  }

  .footer__grid { gap: 1.75rem; }

  /* The floating WhatsApp button sits over the last row of content */
  .wa { width: 48px; height: 48px; inset-block-end: 16px; inset-inline-end: 16px; }
  .wa svg { width: 24px; height: 24px; }
}

/* ============================================================
   Sticky total — phone only.
   On a narrow screen the price sits far below the controls you are
   changing, so it gets pinned where your thumb already is.
   ============================================================ */
.totalbar { display: none; }

@media (max-width: 640px) {
  .totalbar {
    position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 80;
    display: flex; align-items: center; gap: 0.9rem;
    padding: 0.7rem 1rem calc(0.7rem + env(safe-area-inset-bottom));
    background: rgb(var(--deep-rgb) / .93);
    backdrop-filter: blur(16px) saturate(140%);
    border-block-start: 1px solid var(--line-dark);
    transform: translateY(110%);
    transition: transform .3s var(--ease);
  }
  .totalbar.is-on { transform: none; }
  .totalbar__n {
    font-family: var(--font-body); font-weight: 800; font-stretch: 112%;
    font-size: 1.35rem; line-height: 1.1; letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
  }
  .totalbar__n small { display: block; font-size: 0.64rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--dim); font-weight: 500; }
  .totalbar .btn { margin-inline-start: auto; padding-block: 0.8rem; }
  /* Keep the floating WhatsApp button clear of the bar */
  body:has(.totalbar.is-on) .wa { inset-block-end: 78px; }
}

/* ============================================================
   THE PRINT — scroll-driven centrepiece
   A tall track with a sticky viewport inside it. Scroll distance
   is the build; the object, the nozzle and the readout are all
   driven from the same progress value.
   ============================================================ */
.printer {
  position: relative;
  /* The track IS the build time. readScroll() divides by (height - 100vh),
     the sticky viewport's share, so the scrolling that actually builds the
     model is this value minus 100vh: 240vh here = 140vh of build, half the
     380vh (280vh of build) it was. */
  height: 240vh;
  border-block-start: 1px solid var(--line-dark);
  background: var(--bg);
}
.printer__sticky {
  position: sticky; top: 0;
  height: 100vh; height: 100dvh;
  overflow: hidden;
}
/* Its own deeper navy: the molten blue and the layer shadows need a
   ground darker than the ramp at that depth to read against. */
.printer__sticky {
  --deep-rgb: 2 10 31;
  --surface-rgb: 20 23 29;
  --text-rgb: 238 242 247;
  /* --bg and --bg-alt must be restated as well, not just the channels:
     they were substituted at :root, so a channel override alone changes
     nothing for anything that reads the derived token. */
  --bg: rgb(var(--deep-rgb));
  --bg-alt: rgb(var(--surface-rgb));
  --text: rgb(var(--text-rgb));
  --dim: rgb(var(--text-rgb) / 0.6);
  --line-dark: rgb(var(--text-rgb) / 0.14);
  background: var(--bg);
  color: var(--text);
}
.printer__stage { position: absolute; inset: 0; }
.printer__stage canvas { width: 100%; height: 100%; }

.printer__ui {
  position: relative; z-index: 2;
  height: 100%;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding-block: clamp(5rem, 11vh, 7rem) clamp(2rem, 5vh, 3.5rem);
  pointer-events: none;
}
.printer__head { display: grid; gap: 0.9rem; max-width: 20ch; }
.printer__h2 { font-size: clamp(1.9rem, 3.6vw, 3.1rem); }

/* Captions sit low-left, out of the object's way, and cross-fade as
   the build passes each third. */
/* The captions are absolutely stacked so they cross-fade in place;
   the container therefore has no natural height and would collide with
   the readout below it. Reserve the space explicitly. */
.printer__captions { position: relative; align-self: end; max-width: 34ch; min-height: 9.5rem; }
.printer__caption {
  position: absolute; inset-inline-start: 0; inset-block-end: 0;
  display: grid; gap: 0.5rem;
  opacity: 0; transform: translateY(8px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.printer__caption.is-on { opacity: 1; transform: none; }
.printer__caption h3 { font-size: 1.45rem; }
.printer__caption p { color: var(--dim); font-size: 0.95rem; }
.printer__step {
  font-family: var(--font-mono); font-size: 0.7rem;
  letter-spacing: 0.1em; color: var(--dim);
}

/* The readout — the detail that makes it feel like a machine. */
.printer__hud {
  display: flex; gap: clamp(1.5rem, 4vw, 3rem);
  align-self: end; justify-self: end;
  margin: 0; text-align: end;
}
.printer__hud div { display: grid; gap: 0.3rem; }
/* Bottom-corner, like the WhatsApp button — keep the readout clear of it. */
@media (min-width: 781px) { .printer__hud { margin-inline-end: 64px; } }
.printer__hud dt {
  font-family: var(--font-mono); font-size: 0.64rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--dim);
}
.printer__hud dd {
  margin: 0;
  font-family: var(--font-mono); font-size: clamp(1rem, 1.6vw, 1.3rem);
  font-variant-numeric: tabular-nums; color: var(--text);
}

@media (max-width: 780px) {
  .printer { height: 320vh; }
  .printer__ui { padding-block: 5rem 2rem; position: relative; isolation: isolate; }
  .printer__head { max-width: 14ch; }
  .printer__captions { max-width: none; min-height: 8.5rem; }
  .printer__caption h3 { font-size: 1.15rem; }
  .printer__caption p { font-size: 0.88rem; }
  .printer__hud { justify-self: start; text-align: start; margin-block-start: 1.25rem; }
  /* A scrim keeps the copy legible where it crosses the object. */
  .printer__ui::after {
    content: ''; position: absolute; inset-inline: 0; inset-block-end: 0;
    height: 46%; z-index: -1; pointer-events: none;
    background: linear-gradient(to top, rgb(var(--deep-rgb) / .92), transparent);
  }
}

/* Without scroll-linked motion the track is pointless — collapse it
   to a single screen showing the finished object. */
@media (prefers-reduced-motion: reduce) {
  .printer { height: auto; }
  .printer__sticky { position: relative; height: 88vh; }
  .printer__caption { position: relative; opacity: 1; transform: none; margin-block-start: 1.25rem; }
  .printer__captions { display: grid; }
}
