/* MyImagination — one stylesheet, no build step, no framework. */

:root {
  /* Tells the browser to render its own furniture — scrollbars, form controls,
     spinner arrows — to match. Without it those stay light on a dark page. */
  color-scheme: light;

  /* Palette "Malam" (redesign 2026-11-07, Alberd chose direction B with a free
     palette: "warna bebas yang penting bagus dan wajib ada 1 warna logo kita").
     The SITE palette is no longer the drawer: the products are still printed in
     Putih / Hitam / Oranye Pumpkin (#ff9016, pengaturan.json colors - data, not
     CSS), but the site wears the LOGO's orange (#ff5a1f, konsep/logo/buat-logo.py)
     on a night-blue. Cyan survives only where it MEANS "info".
     Three roles are kept strictly apart, because conflating them is what broke
     contrast the last times the palette changed:
       --brand / --sun   are FILLS. Never text on the light page: #ff5a1f on the
                         page is 2.9:1, so text on it is --on-bright (6.0:1).
       --brand-line      focus rings, hover/selected borders, radio fill: the
                         things that need 3:1 against the page on their own.
       --*-ink, --brand-deep   text on the page and on the soft tints. */
  --brand: #ff5a1f;
  --brand-line: #d9400a;
  /* Dark orange of the logo's hue, for links and prices on the light surfaces
     (tools/cek-kontras.py checks it on --bg, --card, --bg-alt and the tint). */
  --brand-deep: #b3360a;
  --brand-soft: #ffede5;
  --brand-ink: #a83208;
  /* The night-blue band (header text sits on --brand; the custom call and the
     footer sit on --deep). Text on it is --on-deep; the logo orange reads on it
     at 4.96:1, so a heading may be orange there. */
  --deep: #18234a;
  --on-deep: #f3f5fb;
  --on-deep-soft: #c3cbe3;
  --deep-line: #3a4a7a;
  --deep-card: #202d5c;
  --cyan-soft: #e0f7fc;
  --cyan-ink: #00758d;
  --sun: #fbbf24;
  --sun-soft: #fff8e1;

  /* Text placed on any bright fill. Deliberately the same in light and dark:
     a bright orange chip is bright in both themes, so what reads on it does
     not change either. */
  --on-bright: #17110d;

  --bg: #f5f6f9;
  --bg-alt: #eaedf3;
  --card: #ffffff;
  --ink: #141a2e;
  --ink-soft: #3d4560;
  --muted: #5f667d;
  --line: #dfe3ec;
  --line-strong: #c4cbda;

  --good: #059669;
  --good-soft: #e7f8f2;
  --good-ink: #047b56;
  --warn: #d12222;
  --warn-soft: #fef1f1;
  --warn-deep: #a81a1a;

  /* Radius carries meaning instead of one value everywhere. A single 16px
     radius on every surface is one of the four things that made this read as
     generated output: nothing had a reason to differ, so nothing did.
       sheet   — panels, cards, tables. Nearly square, because these are
                 documents you read, not chips you press.
       control — buttons and inputs. Rounded enough to invite a tap.
       pill    — only things that are genuinely pill-shaped. */
  /* 2026-11-07: direction B softens the sheet (14) and makes every button a
     pill; inputs keep a smaller round so a field does not read as a button. */
  --radius: 14px;         /* sheet — kept under the old name */
  --radius-lg: 16px;
  --r-control: 10px;
  --r-pill: 999px;
  /* Touch comfort (Alberd 2026-11-07: "kenyamanan dari users"). --chip is the height of every
     small control (viewer buttons, swatches, quantity steps, small buttons); --tap the height of a
     full button. A mouse pointer gets a tighter size, a finger gets 44 px (WCAG 2.5.5, Apple HIG).
     The bars that hold room for controls before they exist compute it from these, so changing a
     size here cannot reopen a layout shift. --chip-gap keeps two targets apart (8 px). */
  --chip: 32px;
  --tap: 42px;
  --chip-gap: 8px;

  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* The one webfont (Alberd 2026-11-07 lifted "Webfont 0" for it): headings,
     product names, prices. Body text stays on the system stack. */
  --head: "Tilt Warp", var(--sans);

  /* Surfaces are separated by a rule, not by a glow. A soft shadow under every
     card is the other half of the generated look, and it also flattens
     hierarchy: when everything floats, nothing does. Shadow is now reserved
     for things that genuinely sit above the page — a toast, a sticky bar. */
  --shadow: none;
  --shadow-lift: none;
  --shadow-float: 0 2px 6px rgba(20, 26, 46, .08), 0 16px 36px -16px rgba(20, 26, 46, .30);

  --step--1: .875rem; --step-0: 1rem;    --step-1: 1.125rem;
  --step-2: 1.375rem; --step-3: 1.75rem; --step-4: 2.25rem; --step-5: 3rem;

  --sp-1: .25rem; --sp-2: .5rem;  --sp-3: .75rem; --sp-4: 1rem;
  --sp-5: 1.5rem; --sp-6: 2rem;   --sp-7: 3rem;   --sp-8: 4rem;

  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-spring: cubic-bezier(.34, 1.56, .64, 1);
  --dur-fast: 140ms; --dur-base: 240ms; --dur-slow: 420ms;
}

@media (pointer: coarse) {
  :root { --chip: 44px; --tap: 48px; }
}

/* Everything non-essential stops when the reader asks for it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- dark theme ----------
   The reader chooses; the system only supplies the default.
   `data-theme` is written by the inline script in every page's <head>, before
   the first paint, so an explicit choice always wins over the OS setting.

   ⚠️ The dark tokens are written TWICE below and the two copies MUST stay
   identical. There is no build step to keep them in sync, so
   `tools/cek-kontras.py` compares them and fails if they drift. Edit one, edit
   the other, then run the script.

   Copy 1 — the reader picked dark explicitly. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --brand: #ff5a1f;
  --brand-line: #ff6a33;
  --brand-deep: #ff8a5c;
  --brand-soft: #3a1d14;
  --cyan-soft: #0d2b33;
  --sun: #fcd34d;
  --sun-soft: #33280d;
  --deep: #0a0f20;
  --on-deep: #f3f5fb;
  --on-deep-soft: #c3cbe3;
  --deep-line: #2a3560;
  --deep-card: #151c36;

  /* On dark, the soft tints are dark, so the bright colours ARE the readable
     text here (tools/cek-kontras.py). The ink tokens invert accordingly; only
     --on-bright stays put, because the fills it sits on stay bright in both
     themes. */
  --brand-ink: #ff8a5c;
  --cyan-ink: #22d3ee;
  --good-ink: #34d399;

  --bg: #10152a;
  --bg-alt: #141a31;
  --card: #171e38;
  --ink: #eef1f8;
  --ink-soft: #c5cbe0;
  --muted: #9aa2bb;
  --line: #262e4a;
  --line-strong: #3b4566;

  --good: #34d399;
  --good-soft: #0d2b22;
  --warn: #f87171;
  --warn-soft: #2e1414;
  --shadow-float: 0 2px 6px rgba(0, 0, 0, .45), 0 20px 44px -18px rgba(0, 0, 0, .85);
}

/* Copy 2 — no explicit choice recorded, so the OS decides. `:not([data-theme])`
   matters: without it this would override a reader who deliberately asked for
   light while their phone is in dark mode. It also covers the no-JavaScript
   case, where the attribute is never written at all. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;

    --brand: #ff5a1f;
    --brand-line: #ff6a33;
    --brand-deep: #ff8a5c;
    --brand-soft: #3a1d14;
    --cyan-soft: #0d2b33;
    --sun: #fcd34d;
    --sun-soft: #33280d;
    --deep: #0a0f20;
    --on-deep: #f3f5fb;
    --on-deep-soft: #c3cbe3;
    --deep-line: #2a3560;
    --deep-card: #151c36;

    --brand-ink: #ff8a5c;
    --cyan-ink: #22d3ee;
    --good-ink: #34d399;

    --bg: #10152a;
    --bg-alt: #141a31;
    --card: #171e38;
    --ink: #eef1f8;
    --ink-soft: #c5cbe0;
    --muted: #9aa2bb;
    --line: #262e4a;
    --line-strong: #3b4566;

    --good: #34d399;
    --good-soft: #0d2b22;
    --warn: #f87171;
    --warn-soft: #2e1414;
    --shadow-float: 0 2px 6px rgba(0, 0, 0, .45), 0 20px 44px -18px rgba(0, 0, 0, .85);
  }
}

/* ---------- theme switch ---------- */

.theme-switch {
  display: inline-flex; align-items: center; gap: 2px;
  border: 1px solid var(--line-strong); border-radius: 999px;
  padding: 2px; background: var(--card); margin-left: var(--sp-2);
}
.theme-switch button {
  border: 0; background: none; cursor: pointer; color: var(--muted);
  font: inherit; font-size: 13px; line-height: 1;
  padding: 6px 9px; border-radius: 999px;
  display: inline-grid; place-items: center;
  transition: background var(--dur-fast), color var(--dur-fast);
}
@media (hover: hover) and (pointer: fine) {
  .theme-switch button:hover { color: var(--ink); }
}
.theme-switch button[aria-pressed="true"] {
  background: var(--brand); color: var(--on-bright);
}
.theme-switch svg { width: 15px; height: 15px; display: block; }
/* In the orange header: the band's ink, the chosen theme in night-blue. */
header.site .theme-switch { background: none; border-color: rgba(23, 17, 13, .4); }
header.site .theme-switch button { color: var(--on-bright); }
header.site .theme-switch button[aria-pressed="true"] { background: var(--deep); color: var(--on-deep); }
/* tema.js draws the switch after first paint (a deferred module). Until then its room is
   held at the same end of the header row, so the header does not grow and push every page
   down when it lands (CLS 0,08-0,12 on a 375 px phone, 2026-10-31). Sizes = the switch's:
   two 33 x 27 buttons (36 x 36 on touch) + gap 2 + padding 2 + border 1. */
header.site .wrap:not(:has(> .theme-switch))::after {
  content: ''; flex: none; margin-left: var(--sp-2); width: 74px; height: 33px;
}
@media (pointer: coarse) {
  header.site .wrap:not(:has(> .theme-switch))::after { width: 80px; height: 42px; }
}

* { box-sizing: border-box; }

/* ---------- phone hygiene (AUDIT_FRONTEND P1-8) ----------
   Hover rules sit in (hover: hover) and (pointer: fine): on a phone a tapped
   button otherwise keeps its hover colour until the next tap elsewhere. The
   grey tap flash is replaced by each control's own :active state. */
html { -webkit-tap-highlight-color: transparent; }
::placeholder { color: var(--muted); opacity: 1; }
@media (pointer: coarse) {
  /* iOS zooms the whole page into any field under 16 px. !important because
     pages style their own fields (.cari, inline <style>) with higher
     specificity, and none of them may go below this on a touch screen. */
  :where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea) {
    font-size: max(16px, 1em) !important;
  }
  /* Touch targets: 44 px (--chip), not the 26 px a mouse pointer gets away with. */
  .qty > button:not(.btn-hapus) { min-width: var(--chip); min-height: var(--chip); }
  .btn-hapus { min-height: var(--chip); }
  /* Icon only since its name floats instead of widening it (2026-10-31). */
  .btn-hapus { min-width: var(--chip); justify-content: center; }
  .theme-switch button { min-width: var(--chip); min-height: var(--chip); }
  /* A tap on a <summary> or a lone link is a tap on a 44 px row, not on 23 px of text. */
  summary { padding-block: 11px; }
  /* Product names on cards and lone links: a 44 px tall target without moving the text
     (the padding is given back as negative margin). */
  .card h3 a.plain-link, .card h2.h3 a.plain-link, .kepala-bagian a {
    display: inline-block; padding-block: 11px; margin-block: -11px;
  }
  /* Under the material select: its own 44 px row, 8 px below the field. */
  a.bahan-cocok { display: inline-block; padding-block: 11px; margin-top: var(--chip-gap); }
  footer.site .kaki-tautan { gap: 0 22px; }
  footer.site .kaki-tautan a { padding-block: 11px; }
}
html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
  /* Kaki halaman menempel di dasar layar. Di halaman yang isinya lebih pendek
     dari layar (keranjang kosong, Custom sebelum berkas dijatuhkan, Saran,
     Testimoni) ia dulu mengambang di tengah, dengan pita kosong di bawahnya.
     Bagian TERAKHIR yang mengisi sisa tinggi; halaman yang lebih panjang dari
     layar tidak berubah satu piksel pun. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
/* Isi halaman ada di <main> sejak 2026-10-23 (landmark + tautan lompat). Ia yang mengisi sisa
   tinggi, dan di dalamnya section terakhir, persis seperti dulu saat section anak langsung body. */
body > main { flex-grow: 1; display: flex; flex-direction: column; }
body > main > section:last-of-type { flex-grow: 1; }
/* Halaman admin (lokal, tanpa <main>) memakai style.css yang sama. */
body > section:last-of-type { flex-grow: 1; }
/* Lompatan ke #konten tidak perlu menggulir: isinya mulai tepat di bawah header yang menempel. */
#konten { scroll-margin-top: 50vh; }

/* ---------- typography ----------
   One webfont since 2026-11-07, for headings only (Alberd lifted "Webfont 0",
   MYIMAGINATION_UI.md §9): Tilt Warp, OFL, cut down to one 14 KB file by
   tools/huruf-situs.py. Body text stays on the system stack: data costs money
   here and that stack renders instantly.
   - `font-display: optional` + the <link rel="preload"> in every page's head:
     the font is used when it is there at first paint and never swapped in
     afterwards, so headings cannot reflow the page (CLS stays 0). On a slow
     first visit the heading shows in the system face; the next page has it.
   - The face is one weight. Declaring it for 100-900 maps every weight the
     stylesheet asks for (h1 800, .card h3 700 ...) onto it, so the browser never
     fakes a bold by smearing the outline. */
@font-face {
  font-family: "Tilt Warp";
  src: url("../fon/tiltwarp.woff") format("woff");
  font-weight: 100 900;
  font-style: normal;
  font-display: optional;
}
h1, h2, h1.sec, h2.sec, .brand, .price, .card h3:where(:not(.h4)), .card h2.h3, .judul {
  font-family: var(--head); font-synthesis: none; letter-spacing: -.005em;
}
/* What follows is the shaping the system stack does not do on its own. */

/* Promised in MYIMAGINATION_UI.md §1.3 but never actually shipped until now.
   Without it a heading regularly strands one word on its own last line, which
   reads as a mistake rather than a line break. */
h1, h2, h3, h4, .lead-big, .sec-sub {
  text-wrap: balance;
}

/* Tingkat judul mengikuti kerangka halaman (satu h1, tanpa loncatan), tampilannya tidak.
   `.h3` / `.h4` = "tampil seperti h3 / h4": nilai bawaan peramban untuk tag itu, lewat :where()
   supaya kekhususannya nol dan semua aturan lain (kelas, gaya sebaris) tetap menang. */
:where(.h3) { font-size: 1.17em; margin-block: 1em; }
:where(.h4) { font-size: 1em; margin-block: 1.33em; }

/* Terbaca pembaca layar, tidak terlihat. */
.hanya-pembaca {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* Tautan lompat (WCAG 2.4.1): tersembunyi di atas layar, muncul saat difokus papan ketik. */
.lompat {
  position: absolute; left: 12px; top: 12px; z-index: 100;
  padding: 8px 14px; border-radius: var(--r-control);
  background: var(--ink); color: var(--bg); font-weight: 700; text-decoration: none;
  transform: translateY(-200%);
}
.lompat:focus { transform: none; }

/* Body copy gets `pretty` instead: it only rescues the last line, so it does
   not reflow whole paragraphs the way `balance` would. */
p, li, dd, figcaption, .card p, ol.steps p {
  text-wrap: pretty;
}

/* Reading measure. Prose past ~70 characters loses the line it is on; this
   site's audience reads it on a phone held one-handed. Grid and table cells are
   exempt because their width is already decided by the layout. */
.narrow p, .narrow li, .lead-big, p.lead {
  max-width: 68ch;
}

/* Every figure that can change while the reader is looking at it — the cart
   total counts up, quantities step, prices move when material cost changes.
   Proportional digits make those numbers visibly jitter mid-change; tabular
   ones hold their column. */
.price, .cart-count, .qty, table.spec td,
.total-row, .sum-val, .timeline .when, .order-row .code {
  font-variant-numeric: tabular-nums;
}

.wrap { max-width: 1140px; margin: 0 auto; padding: 0 20px; }
.narrow { max-width: 780px; }
a { color: var(--brand-deep); }

/* ---------- header ---------- */

/* The header is the top of the orange band (direction B, 2026-11-07), in both
   themes: the logo's colour is the one thing every page shares. Everything on
   it is --on-bright (6.0:1); focus rings switch to that ink too, because the
   orange --brand-line would vanish on an orange ground. */
header.site {
  background: var(--brand); color: var(--on-bright);
  position: sticky; top: 0; z-index: 50;
}
header.site .wrap { display: flex; align-items: center; gap: 22px; height: 64px; }
header.site :focus-visible { outline-color: var(--on-bright); }
.brand {
  font-size: 21px; letter-spacing: 0;
  text-decoration: none; color: var(--on-bright); white-space: nowrap;
  display: inline-flex; align-items: center; min-height: 44px;
}
/* One colour: the split wordmark (two weights) needs a second weight the
   heading face does not have. */
.brand span { color: inherit; }
/* Satu baris, selalu. Dulu `flex-wrap: wrap` di dalam header setinggi 64 px:
   di layar sempit menunya turun ke baris kedua dan meluber KE LUAR header,
   menimpa isi halaman (25 px di 375 px; 45 px sesudah tautan Misteri masuk).
   Kini menu yang tidak muat bergulir mendatar di tempatnya, dan dua tepinya
   memudar supaya terlihat bahwa masih ada isinya. Di layar lebar tidak ada
   yang berubah: tak ada yang meluber, jadi tak ada yang bergulir. */
nav.main {
  display: flex; gap: 18px; margin-left: auto; align-items: center;
  flex-wrap: nowrap; min-width: 0; overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none; padding-block: 8px;
}
nav.main::-webkit-scrollbar { display: none; }
@media (max-width: 899px) {
  header.site .wrap { gap: 14px; }
  nav.main {
    gap: 16px; padding-inline: 10px;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
            mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
  }
}
nav.main a {
  text-decoration: none; color: var(--on-bright); font-size: 14.5px; font-weight: 600;
  white-space: nowrap; flex: none;
  text-decoration-thickness: 2px; text-underline-offset: 6px;
}
/* The current page and the hovered link are underlined: colour cannot mark
   them, every link on the band is the same ink. */
nav.main a[aria-current="page"] { text-decoration-line: underline; }
@media (hover: hover) and (pointer: fine) {
  nav.main a:hover { text-decoration-line: underline; }
}

/* The cart sits OUTSIDE the scrolling menu, beside it. Inside, it was the
   menu's last item: on a 375 px phone it and the theme switch after it were
   off the screen (x 701), so a buyer who had just added something saw no count
   and no way to the cart without swiping the menu. */
header.site .cart-link {
  text-decoration: none; color: var(--on-bright); font-size: 14.5px; font-weight: 600;
  white-space: nowrap; flex: none;
  text-decoration-thickness: 2px; text-underline-offset: 6px;
}
header.site .cart-link[aria-current="page"] { text-decoration-line: underline; }
@media (hover: hover) and (pointer: fine) {
  header.site .cart-link:hover { text-decoration-line: underline; }
}
/* Phone and tablet: logo, cart, and theme on the first row; the menu gets the whole
   second row to itself and still scrolls if it does not fit. The fixed 64 px
   height is what made a wrapped menu spill over the page before, so it goes.
   Sampai 899 px, bukan 599 (2026-10-23): di 600-899 px menu berbagi baris dengan logo,
   keranjang, dan tombol tema, dan terpotong (768 px: ruang 377 px, isi 422 px). */
@media (max-width: 899px) {
  header.site .wrap {
    flex-wrap: wrap; height: auto; column-gap: 12px; row-gap: 0;
    padding-top: 12px;
  }
  header.site .cart-link { margin-left: auto; }
  nav.main { order: 3; flex: 1 0 100%; margin-left: 0; padding-block: 6px 10px; }
}
/* Ponsel: menu muat tanpa digeser. "Beranda" disembunyikan (logo sudah tautan ke beranda)
   dan "Lacak Pesanan" jadi "Lacak": isi menu 293 px, muat sampai layar 333 px. Di 375 px
   dulu 422 px di ruang 335 px dan "FAQ" seluruhnya di luar layar. Di bawah 333 px menu
   masih bisa digeser dan tepinya memudar. */
@media (max-width: 599px) {
  nav.main .nav-beranda, nav.main .nav-panjang { display: none; }
}

/* ---------- menu burger (ponsel, < 600 px; Alberd 2026-11-07) ----------
   Baris kedua yang bergulir menyembunyikan "FAQ" di luar layar (390 px) dan memakan 40 px tinggi
   tiap halaman. Kini baris pertama = logo, keranjang, tombol menu; menu tegak + tombol tema ada di
   panel di bawahnya, masih di dalam header yang lengket. Tombolnya ditulis di tiap halaman dan
   `html.js` dipasang skrip pemblokir di <head> sebelum cat pertama, jadi header sudah berbentuk
   akhir pada bingkai pertama (tanpa geser). Tanpa JavaScript: tombol tidak tampil, menu lama tetap.
   Buka/tutup: assets/js/menu.js. */
.menu-tombol { display: none; }
@keyframes menu-masuk { from { opacity: 0; transform: translateY(-4px); } }
@media (max-width: 599px) {
  .js header.site .wrap { padding-bottom: 10px; }
  .js .menu-tombol {
    display: inline-grid; place-items: center; order: 2; flex: none;
    width: 44px; height: 44px; margin-right: -10px; padding: 0;
    border: 0; border-radius: var(--r-control); background: none; color: var(--on-bright);
    cursor: pointer; -webkit-tap-highlight-color: transparent;
  }
  .js .menu-tombol svg { width: 24px; height: 24px; display: block; }
  .js .menu-tombol .garis-tutup,
  .js .menu-tombol[aria-expanded="true"] .garis-buka { display: none; }
  .js .menu-tombol[aria-expanded="true"] .garis-tutup { display: inline; }
  .js .menu-tombol:focus-visible { outline: 2px solid var(--on-bright); outline-offset: 0; }
  .js header.site .cart-link { order: 1; }
  /* Tempat tombol tema yang dipegang sebelum tema.js jalan (::after di atas) tidak di baris
     pertama lagi: tombol tema pindah ke panel. */
  .js header.site .wrap::after { content: none; }
  .js header.site nav.main,
  .js header.site .theme-switch { display: none; }
  .js header.site.menu-buka nav.main {
    display: flex; flex-direction: column; align-items: stretch; gap: 0;
    order: 3; flex: 1 0 100%; padding: 8px 0 2px; overflow: visible;
    -webkit-mask-image: none; mask-image: none;
  }
  .js header.site.menu-buka nav.main a {
    padding: 12px 2px; font-size: 16.5px; font-weight: 600; color: var(--on-bright);
    border-top: 1px solid rgba(23, 17, 13, .22);
  }
  .js header.site.menu-buka nav.main a:first-child { border-top: 0; }
  .js header.site.menu-buka nav.main a[aria-current="page"] { text-decoration-line: underline; }
  .js header.site.menu-buka nav.main .nav-beranda { display: block; }
  .js header.site.menu-buka nav.main .nav-panjang { display: inline; }
  .js header.site.menu-buka .theme-switch { display: inline-flex; order: 4; margin: 6px 0 4px; }
  @media (prefers-reduced-motion: no-preference) {
    .js header.site.menu-buka nav.main,
    .js header.site.menu-buka .theme-switch { animation: menu-masuk .18s cubic-bezier(.2, .7, .2, 1); }
  }
}

.cart-link { position: relative; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; }
.cart-count {
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  background: var(--brand); color: var(--on-bright);
  font-size: 12px; font-weight: 800; font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  display: inline-grid; place-items: center;
}
.cart-count[data-empty="true"] { background: var(--line-strong); color: var(--ink-soft); }
/* On the orange header: a filled count is the night-blue (it must stand out
   from the band), an empty one is only an outline in the band's ink. */
header.site .cart-count { background: var(--deep); color: var(--on-deep); }
header.site .cart-count[data-empty="true"] {
  background: none; color: var(--on-bright); box-shadow: inset 0 0 0 1.5px var(--on-bright);
}

/* ---------- hero ---------- */

.hero {
  padding: 60px 0 48px;
  position: relative; overflow: hidden; isolation: isolate;
}

/* The gradient drifts on a pseudo-element moved by transform, not by animating
   background-position. §4.1 of the UI design asked for background-position, but
   §5 forbids animating anything except transform and opacity — the budget wins,
   because background-position repaints the whole area every frame while a
   transform stays on the compositor. */
/* The measuring grid that stood behind the hero (2026-08-07 (m)) is gone too
   (redesign 2026-11-07): detectors and readers both recognise a faint grid
   behind a headline as generator output. The hero has no ornament now; on the
   home page it is the orange band itself (.hero.pita below). */

/* (The pulsing "Lihat layanan" scroll cue under the hero was removed 2026-10-30,
   AUDIT_FRONTEND P1-1.) */

.hero h1 {
  font-size: clamp(31px, 5.2vw, 50px); line-height: 1.04;
  letter-spacing: -.01em; margin: 0 0 16px; font-weight: 800;
}
/* No coloured, underlined phrase inside a heading any more (redesign
   2026-11-07: the "one highlighted phrase" headline is a generator tell). An
   <em> left in a heading reads as plain heading text. */
.hero h1 em { font-style: normal; color: inherit; }
.hero p.lead { font-size: 18px; color: var(--ink-soft); margin: 0 0 26px; }
@media (max-width: 599px) {
  .hero { padding-top: 14px; }
  .hero h1 { margin-bottom: 12px; }
  .hero p.lead { font-size: 16.5px; margin-bottom: 20px; }
}

/* ---------- home page (direction B, 2026-11-07) ----------
   The page is three bands: the orange of the logo (header + hero), the page
   itself (the shelf, the story), and one night-blue band (the custom call)
   before the night-blue footer. Every box that data fills later has its final
   size before the data arrives, so nothing on the first screen moves (CLS 0). */

/* Band 1: the hero continues the header's orange. Its picture sits on the
   band's lower edge, in a night-blue frame whose strip carries the caption. */
.hero.pita { background: var(--brand); color: var(--on-bright); padding: 36px 0 0; }
.hero.pita h1 { font-size: clamp(38px, 6.2vw, 80px); line-height: .98; margin: 0 0 20px; }
.hero.pita p.lead { color: var(--on-bright); font-weight: 500; font-size: 18.5px; max-width: 30em; }
.hero.pita .btn-ghost { background: none; border: 2px solid var(--on-bright); color: var(--on-bright); }
.hero.pita .btn-malam { border-width: 2px; }
@media (hover: hover) and (pointer: fine) {
  .hero.pita .btn-ghost:hover { background: var(--on-bright); color: var(--brand); }
}
.hero.pita .hero-grid { display: grid; gap: 40px; }
.hero-teks { padding-bottom: 56px; }
.hero-benda {
  margin: 0; background: var(--deep); color: var(--on-deep);
  border-radius: 22px 22px 0 0; overflow: hidden;
}
.hero-benda > .hero-media.viewer { aspect-ratio: 5 / 4; border-radius: 0; }
.hero-media[data-state="foto"] { background: var(--deep-card); }
.hero-media .foto-produk, .hero-media .foto-produk img { display: block; width: 100%; height: 100%; }
.hero-media .foto-produk img { object-fit: cover; object-position: 50% 35%; }
/* The strip under the picture: the viewer's buttons (a model) or a label and a
   link (a photo). Same height either way, set before it is filled. */
.hero-benda > .viewer-bar {
  background: var(--deep); border: 0; margin: 0; padding: 12px 18px 0;
  min-height: calc(12px + 2 * var(--chip) + var(--chip-gap)); align-items: center;
}
/* Two rows of chips + the strip's 12 px: the live viewer's bar (hint + buttons), measured 2026-11-07. */
.hero-benda > .viewer-bar[data-tempat]:empty { min-height: calc(12px + 2 * var(--chip) + var(--chip-gap)); }
.hero-benda .label-foto { font-size: 14px; font-weight: 600; color: var(--on-deep-soft); margin-right: auto; }
.hero-benda > .dekor-caption {
  color: var(--on-deep-soft); font-size: 14px; padding: 8px 18px 18px;
  min-height: calc(2lh + 26px);
}
.hero-benda > .dekor-caption a { text-decoration: none; }
.hero-benda > .dekor-caption strong { color: var(--on-deep); }
.hero-benda > .dekor-caption a { color: var(--brand); }
@media (min-width: 860px) {
  .hero.pita .hero-grid { grid-template-columns: 1.08fr 1fr; align-items: end; gap: 48px; }
  .hero-teks { padding-bottom: 72px; align-self: center; padding-top: 24px; }
}
/* Phone and tablet: the picture first, edge to edge, then the heading and both buttons. */
@media (max-width: 859px) {
  .hero.pita { padding-top: 0; }
  .hero.pita .hero-grid { display: flex; flex-direction: column; gap: 0; }
  .hero-benda { order: -1; margin: 0 -20px; border-radius: 0; }
  .hero-benda > .hero-media.viewer { aspect-ratio: 4 / 3; }
  .hero-teks { padding: 22px 0 36px; }
  /* The viewer's three buttons wrap to two rows on a phone; the photo's label and link take
     the same two rows, so the strip is one height whichever the hero turns out to be. */
  .hero-benda > .viewer-bar, .hero-benda > .viewer-bar[data-tempat]:empty {
    min-height: calc(10px + 2 * var(--chip) + var(--chip-gap)); padding: 10px 16px 0;
  }
  .hero-benda .label-foto { flex: 1 0 100%; }
  /* Three lines: the model's caption (two prices) is the longer one. */
  .hero-benda > .dekor-caption { padding: 6px 16px 14px; min-height: calc(3lh + 20px); }
}
@media (max-width: 599px) {
  .hero.pita h1 { font-size: clamp(34px, 10.4vw, 46px); margin-bottom: 14px; }
  .hero.pita p.lead { font-size: 16.5px; margin-bottom: 20px; }
}

/* Band 2: the shelf. One large tile and two smaller ones, never three equal cards. */
.kepala-bagian { display: flex; justify-content: space-between; align-items: baseline; gap: 8px 24px; flex-wrap: wrap; margin-bottom: 24px; }
.kepala-bagian h2 { margin: 0; }
.kepala-bagian a { font-weight: 650; }
.rak { display: grid; gap: 22px; }
.rak-item {
  display: flex; flex-direction: column; min-width: 0;
  background: var(--card); border: 1px solid var(--line); border-radius: 20px;
  overflow: hidden; text-decoration: none; color: inherit;
  transition: border-color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  a.rak-item:hover { border-color: var(--brand-line); }
}
.rak-gambar { height: 230px; background: var(--bg-alt); }
.rak-gambar picture, .rak-gambar img { display: block; width: 100%; height: 100%; }
.rak-gambar img { object-fit: cover; }
.rak-model img { object-fit: contain; }
.rak-meta {
  display: grid; grid-template-columns: 1fr auto; gap: 4px 14px; align-content: start;
  padding: 14px 18px 18px; min-height: 96px;
}
.rak-meta h3 { margin: 0; font-family: var(--head); font-weight: 400; font-size: 21px; line-height: 1.12; }
.rak-meta .price { font-size: 19px; white-space: nowrap; }
.rak-meta .price small { font-family: var(--sans); font-size: 13px; font-weight: 500; color: var(--muted); }
.rak-meta p { grid-column: 1 / -1; margin: 0; color: var(--muted); font-size: 14.5px; }
@media (min-width: 860px) {
  .rak { grid-template-columns: 1.35fr 1fr; grid-template-rows: auto auto; }
  .rak-besar { grid-row: span 2; }
  .rak-besar .rak-gambar { flex: 1; height: auto; min-height: 380px; }
  .rak-item:not(.rak-besar) .rak-gambar { height: 200px; }
}

/* Band 3: night-blue, the custom call. The box is a link to the Custom page
   that looks like the drop zone waiting there, not a drop zone of its own. */
.ajak-custom .wrap { display: grid; gap: 28px; align-items: center; }
.ajak-custom h2.sec { font-size: clamp(30px, 4.4vw, 52px); line-height: 1; }
.ajak-custom p { font-size: 17.5px; max-width: 30em; margin: 14px 0 0; }
.kotak-unggah {
  display: grid; justify-items: center; gap: 6px; text-align: center;
  padding: 38px 22px; border: 2px dashed var(--deep-line); border-radius: 22px;
  color: var(--on-deep); text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-out);
}
section.malam a.kotak-unggah { color: var(--on-deep); }
@media (hover: hover) and (pointer: fine) {
  .kotak-unggah:hover { border-color: var(--brand); }
}
.kotak-unggah .judul { font-size: 24px; }
.kotak-unggah .kecil { font-size: 14.5px; color: var(--on-deep-soft); }
.kotak-unggah .tarif {
  margin-top: 14px; padding: 8px 16px; border-radius: var(--r-pill);
  background: var(--brand); color: var(--on-bright); font-weight: 700; font-size: 15px;
  font-variant-numeric: tabular-nums; text-wrap: balance;
}
.kotak-unggah .tarif > span { white-space: nowrap; }
@media (min-width: 860px) {
  .ajak-custom .wrap { grid-template-columns: 1fr 1fr; gap: 56px; }
}

/* Band 4: who does the work, and the four steps as plain words (no numbered
   circles: the <dl> says it in order). */
.cerita .wrap { display: grid; gap: 22px; }
.cerita h2.sec { font-size: clamp(28px, 3.6vw, 44px); line-height: 1.04; margin: 0; }
.cerita p { font-size: 17.5px; color: var(--ink-soft); margin: 0 0 18px; max-width: 34em; }
.cara { margin: 0 0 18px; display: grid; gap: 10px; }
.cara > div { display: grid; grid-template-columns: 5.2em 1fr; gap: 14px; align-items: baseline; }
.cara dt { font-family: var(--head); font-size: 18px; }
.cara dd { margin: 0; color: var(--ink-soft); }
.testimoni-beranda { grid-column: 1 / -1; }
.testimoni-beranda[hidden] { display: none; }
@media (min-width: 860px) {
  .cerita .wrap { grid-template-columns: 1fr 1fr; gap: 56px; }
}

/* A plain small line above a heading, in sentence case: tracked capitals with
   a rule under them were template chrome (AUDIT_FRONTEND §2). */
.eyebrow {
  display: inline-block; font-size: 14.5px; font-weight: 600;
  color: var(--ink-soft); background: none;
  padding: 0; border: 0; margin-bottom: 12px;
}

/* ---------- buttons ---------- */

/* One button system (rombak UI 2026-11-07). Three ranks, nothing else:
     .btn-primary  the one action of the screen: orange fill, ink text;
     .btn-ghost    the second action: an outline that is visible on every ground;
     .btn-malam    the primary where the ground is already orange.
   Every button is a pill of --tap height (48 px under a finger), centred content, an icon or a
   count beside the word through `gap`. Pressing shrinks it a little (feedback a finger feels
   without a hover state); focus draws a ring OUTSIDE the pill, so the shape stays a pill. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); padding: 0 22px; border-radius: var(--r-pill);
  font-weight: 700; font-size: 15px; line-height: 1.2; text-decoration: none; text-align: center;
  border: 2px solid transparent; cursor: pointer; font-family: inherit;
  transition: transform .12s ease, filter .12s ease, background-color .12s ease, border-color .12s ease;
  -webkit-user-select: none; user-select: none; touch-action: manipulation;
}
.btn:active { transform: scale(.97); }
.btn:focus-visible { outline: 3px solid var(--brand-line); outline-offset: 3px; border-radius: var(--r-pill); }
.btn-primary {
  background: var(--brand); color: var(--on-bright);
}
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { filter: brightness(1.07); }
}
.btn-ghost { border-color: var(--line-strong); color: var(--ink); background: var(--card); }
@media (hover: hover) and (pointer: fine) {
  .btn-ghost:hover { border-color: var(--ink); }
}
/* Disabled reads as disabled (faded, no press), and a busy button (aria-busy, set by the
   page while it waits on the network) shows a turning ring and cannot be pressed twice. */
.btn:disabled, .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; transform: none; }
.btn[aria-busy="true"] { pointer-events: none; }
.btn[aria-busy="true"]::before {
  content: ''; width: 14px; height: 14px; flex: none; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: putar .7s linear infinite;
}
@keyframes putar { to { transform: rotate(360deg); } }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; }
/* Phone: a row that wraps gives each button its row's width, so two calls to
   action stacked one under the other are one width (204 and 211 px before),
   and filters share one row in equal cells instead of breaking 2 + 1. */
@media (max-width: 599px) {
  .btn-row > .btn:not(.btn-sm) { flex: 1 1 auto; text-align: center; }
  .btn-row.filter-row { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; }
  .btn-row.filter-row > .btn { padding: 0 6px; font-size: 14px; }
}
.btn-sm { min-height: max(36px, var(--chip)); padding: 0 16px; font-size: 14px; border-radius: var(--r-pill); }
/* Night-blue button: the primary action where the ground is already orange
   (the home page band, a toast on orange). */
.btn-malam { background: var(--deep); color: var(--on-deep); border-color: var(--deep); }
@media (hover: hover) and (pointer: fine) {
  .btn-malam:hover { filter: brightness(1.25); }
}
.btn-block { display: flex; width: 100%; }

/* ---------- sections ---------- */

/* Section rhythm used to be one number for every section, which is why the page
   scrolled like a form. Space now follows what the section is doing: an opening
   breathes, a specification sits tight, a closing note sits tighter still. */
section { padding: var(--sp-8) 0 var(--sp-7); }
section.alt { background: var(--bg-alt); }
section.tight { padding: var(--sp-6) 0 var(--sp-5); }   /* dense, reference-like */
section.roomy { padding: 5.5rem 0 var(--sp-8); }        /* openings */
section + section.alt { border-top: 1px solid var(--line); }
/* The night-blue band: one per page at most (the home page's custom call),
   plus the footer. Orange headings and links read on it (4.9:1). */
section.malam { background: var(--deep); color: var(--on-deep); border-top: 0; }
section.malam h2, section.malam h1 { color: var(--brand); }
section.malam p, section.malam .sec-sub { color: var(--on-deep-soft); }
section.malam a:not(.btn) { color: var(--brand); }
section.malam + section, section + section.malam { border-top: 0; }
h1.sec, h2.sec { font-size: clamp(25px, 3.4vw, 34px); letter-spacing: -.005em; margin: 0 0 8px; font-weight: 800; line-height: 1.08; }
p.sec-sub { color: var(--ink-soft); margin: 0 0 30px; max-width: 64ch; }

/* ---------- cards & grid ---------- */

.grid { display: grid; gap: 22px; }
.grid-2, .grid-3 { grid-template-columns: 1fr; }
@media (min-width: 700px) { .grid-2, .grid-3 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1000px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }

.card {
  background: var(--card); border: 1px solid var(--line-strong);
  border-radius: var(--radius); overflow: hidden;
  display: flex; flex-direction: column;
}

/* For blocks that are information, not choices. A card says "pick me"; a
   specification, a policy clause and a numbered step say no such thing, and
   boxing them was what turned every section into the same row of tiles.
   These get a rule and the page's own background instead. */
.sheet {
  background: none; border: 0; border-top: 2px solid var(--ink);
  border-radius: 0; padding: var(--sp-4) 0 0; margin: var(--sp-5) 0;
}
.sheet > * + * { margin-top: var(--sp-3); }
.sheet .card-body { padding: 0; }
.card-body { padding: 18px; display: flex; flex-direction: column; flex: 1; }
.card h3:where(:not(.h4)), .card h2.h3 { margin: 0 0 6px; font-size: 19px; letter-spacing: 0; font-weight: 700; line-height: 1.15; }
.card p { margin: 0 0 14px; color: var(--ink-soft); font-size: 15px; flex: 1; }
/* ---------- catalogue card layout ----------
   The card had tags, a heading, a summary, a loose line of facts, a colour
   picker, a price and two buttons all stacked at the same rhythm, so nothing
   grouped and nothing led. These give it three bands: what it is, what it
   measures, what it costs. */
.kartu-kepala { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.card-body > .kartu-kepala + h3, .card-body > .kartu-kepala + h2.h3 { margin-top: 10px; }

.kartu-fakta {
  display: flex; gap: var(--sp-5); flex-wrap: wrap;
  margin: 0 0 var(--sp-4); padding: var(--sp-3) 0;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.kartu-fakta div { display: flex; flex-direction: column; gap: 1px; }
.kartu-fakta dt {
  font-size: 12.5px; color: var(--muted); font-weight: 600;
}
.kartu-fakta dd {
  margin: 0; font-size: 14px;
  font-variant-numeric: tabular-nums; color: var(--ink);
}

/* .accent-top / .accent-cyan / .accent-violet: the 3px coloured strip on top of
   cards was template texture (AUDIT_FRONTEND P1-3) and is gone; the classes are
   left in the markup as harmless hooks. */

/* A small plain label, not an uppercase chip (AUDIT_FRONTEND P1-3). It takes
   the ink tokens, not the fills. .cyan / .violet were decoration only (which
   service line a card belongs to) and now read as the neutral grey label. */
.tag {
  display: inline-block; font-size: 12.5px; font-weight: 650;
  letter-spacing: 0; text-transform: none;
  padding: 2px 8px; border-radius: 6px;
  background: var(--brand-soft); color: var(--brand-ink);
}
.tag.cyan, .tag.violet { background: var(--bg-alt); color: var(--ink-soft); }
.tag.good { background: var(--good-soft); color: var(--good-ink); }
.tag.grey { background: var(--bg-alt); color: var(--ink-soft); }

/* ---------- 3D viewer ---------- */

.viewer {
  position: relative; width: 100%; aspect-ratio: 4 / 3;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--line);
  /* pan-y, bukan none: usapan tegak menggulir halaman, usapan mendatar memutar
     benda (kanvasnya diberi nilai yang sama di viewer3d.js). */
  cursor: grab; touch-action: pan-y; overflow: hidden;
}
.viewer:active { cursor: grabbing; }
.viewer canvas { display: block; width: 100%; height: 100%; }
/* Tanpa menu "simpan gambar" tekan-lama (iOS) dan tanpa seret gambar keluar; pasangannya
   tolakSimpan() di potret.js. Kosmetik: alat pengembang tetap bisa mengambilnya (2026-11-01). */
.viewer img, .viewer canvas {
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; -webkit-user-drag: none;
}
/* Kartu katalog: gambar diam model yang sama (tools/potret-produk.mjs, latar transparan, jadi
   latar .viewer di atas tetap yang terlihat), penampil 3D baru dibuat saat diketuk. Ia tidak
   ber-z-index: kilau pemuatan (::after) tetap tampil di atasnya selama model dimuat. */
.viewer[data-state="potret"] { cursor: pointer; }
.viewer-potret {
  position: absolute; inset: 0; display: block; width: 100%; height: 100%;
  padding: 0; border: 0; background: none; cursor: pointer;
  transition: opacity var(--dur-base) var(--ease-out);
}
.viewer-potret img { display: block; width: 100%; height: 100%; object-fit: contain; }
.viewer-potret.pergi { opacity: 0; pointer-events: none; }
.viewer-bar .viewer-putar::before { content: '↻'; margin-right: 5px; }

/* Fade the model in rather than letting it pop, and show a breathing skeleton
   while the context is still being built and the file parsed. */
.viewer .viewer-canvas { opacity: 0; transition: opacity var(--dur-base) var(--ease-out); }
.viewer[data-state="ready"] .viewer-canvas { opacity: 1; }
.viewer[data-state="loading"]::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg,
    transparent 20%, color-mix(in srgb, var(--card) 70%, transparent) 46%, transparent 72%);
  background-size: 220% 100%;
  animation: sheen 1.5s linear infinite;
}
@keyframes sheen { to { background-position: -220% 0; } }

/* ---------- keluarga lampu foto: satu kartu, satu halaman (2026-11-03) ---------- */

/* Kartu katalog: gambar diam tiap bingkai berderet, digeser dengan jari (scroll-snap), tombol
   panah, titik, atau tombol panah papan ketik saat jalurnya terfokus. Gambar saja, tanpa three.js. */
.geser { position: relative; border-bottom: 1px solid var(--line); background: #120e0b; }
.geser-jalur {
  display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth;
  scrollbar-width: none; overscroll-behavior-x: contain; touch-action: pan-x pan-y;
}
.geser-jalur::-webkit-scrollbar { display: none; }
.geser-jalur:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.geser-slide {
  position: relative; flex: 0 0 100%; scroll-snap-align: center; scroll-snap-stop: always;
  aspect-ratio: 4 / 3; display: block; color: #f7f1eb; text-decoration: none;
}
.geser-slide img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  -webkit-touch-callout: none; user-select: none; -webkit-user-drag: none;
}
.geser-slide .geser-kosong {
  display: grid; place-items: center; width: 100%; height: 100%; font-weight: 700;
}
.geser-label {
  position: absolute; left: 10px; top: 10px; padding: 3px 9px; border-radius: var(--r-pill);
  background: rgba(14, 11, 9, .72); color: #f7f1eb; font-size: 12.5px; font-weight: 700;
}
.geser-gambaran {
  position: absolute; right: 10px; top: 10px; padding: 3px 8px; border-radius: var(--r-pill);
  background: rgba(14, 11, 9, .72); color: #e9dfd6; font-size: 11.5px; pointer-events: none;
}
.geser-panah {
  position: absolute; top: 50%; translate: 0 -50%;
  width: max(36px, var(--chip)); height: max(36px, var(--chip)); border-radius: 50%; border: 1px solid rgba(247, 241, 235, .35);
  background: rgba(14, 11, 9, .62); color: #f7f1eb; font-size: 20px; line-height: 1;
  display: grid; place-items: center; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.geser-panah:hover { background: rgba(14, 11, 9, .85); }
.geser-panah:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.geser-panah[disabled] { opacity: 0; pointer-events: none; }
.geser-panah.kiri { left: 8px; }
.geser-panah.kanan { right: 8px; }
.geser-titik {
  position: absolute; left: 0; right: 0; bottom: 2px; display: flex; justify-content: center; gap: 0;
}
/* Titik 8 px di dalam tombol 24 px: sasaran sentuh yang cukup, tanda yang tetap kecil. */
.geser-titik button {
  /* The dot is 8 px; the button around it is a full target (--chip), dots side by side. */
  width: var(--chip); height: var(--chip); padding: 0; border: 0; background: none; cursor: pointer;
  display: grid; place-items: center;
}
.geser-titik button::before {
  content: ''; width: 8px; height: 8px; border-radius: 50%;
  background: rgba(247, 241, 235, .45); transition: background var(--dur-fast) var(--ease-out);
}
.geser-titik button[aria-current="true"]::before { background: var(--brand); }
.geser-titik button:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; border-radius: 50%; }
@media (prefers-reduced-motion: reduce) { .geser-jalur { scroll-behavior: auto; } }

/* Halaman produk: tombol bingkai, dan gambaran lampu menyala di bawah penampil. */
.pilih-bingkai { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 12px; }
.contoh-nyala { margin: 16px 0 0; }
.contoh-nyala img {
  display: block; width: 100%; height: auto; border-radius: var(--radius-lg); background: #120e0b;
  -webkit-touch-callout: none; user-select: none; -webkit-user-drag: none;
}
.contoh-nyala figcaption { margin-top: 8px; font-size: 13px; color: var(--muted); }
.contoh-nyala figcaption .tag { margin-right: 4px; }
/* A real photo of the printed object on the product page (rombak 2026-11-07). */
.foto-cetak { margin: 16px 0 0; }
.foto-cetak img { display: block; width: 100%; height: auto; border-radius: var(--radius); background: var(--bg-alt); }
.foto-cetak figcaption { margin-top: 8px; font-size: 13px; color: var(--muted); }
.foto-cetak figcaption .tag { margin-right: 4px; }

/* ---------- viewer bar ----------
   Nothing here sits on top of the canvas. Text over a rotating 3D scene is
   WCAG failure F83 -- the contrast behind each letter changes as the model
   turns, so it cannot be guaranteed at all. These read on a flat surface
   underneath the image instead, where the contrast is a known number. */
.viewer-bar {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-2) 0 0;
  opacity: 0; animation: fadeIn var(--dur-slow) var(--ease-out) 120ms forwards;
}
@keyframes fadeIn { to { opacity: 1; } }
/* A page may render the bar EMPTY before its model or still is there (product page, home
   hero): it holds the two rows decorateViewer fills, so the text under the viewer does not
   jump when they land (CLS, 2026-10-31). */
.viewer-bar[data-tempat]:empty { min-height: calc(var(--sp-2) + 2 * var(--chip) + var(--chip-gap)); }
/* A photo lamp's live bar has a fifth button ("Nyalakan lampu") and takes three rows on a phone
   (140 px at 412 on a touch screen, measured 2026-11-07); the empty bar before it holds two (94). Holding the three
   rows from the start keeps the buy box under it from jumping 46 px when the 3D viewer is ready
   (Lighthouse CLS 0,017 on the lamp page). */
@media (max-width: 599px) {
  .viewer[data-nyala] + .viewer-bar,
  .viewer[data-nyala] + .viewer-bar[data-tempat]:empty {
    min-height: calc(var(--sp-2) + 3 * var(--chip) + 2 * var(--chip-gap)); align-content: start;
  }
}

.viewer-bar .viewer-hint {
  font-size: 12px; color: var(--muted);
  display: inline-flex; align-items: center; gap: 5px;
  min-height: var(--chip);   /* one button's height: the hint reads level with the first one */
}
.viewer-bar .viewer-hint::before {
  content: '↻'; font-size: 13px; line-height: 1;
}
.viewer-bar .viewer-fallback {
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  color: var(--warn); margin-left: auto;
}

/* Scale reference toggle — answers "how big is it?" better than a number. */
/* Only the first control gets pushed right; the rest sit beside it, or every
   button would claim the free space and they would spread across the bar. */
.viewer-bar .viewer-scale + .viewer-scale { margin-left: 0; }
.viewer-bar .viewer-scale {
  margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--chip);
  font: 700 12px/1 var(--sans); letter-spacing: .01em;
  color: var(--ink); cursor: pointer;
  background: var(--card);
  /* 12 px type and a 1 px edge: the two columns of a multi-part model need every pixel of a
     335 px bar (see the 420 px rule below); a wider chip would wrap them to four rows. */
  border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  padding: 0 12px; touch-action: manipulation;
  transition: border-color var(--dur-fast), color var(--dur-fast), transform var(--dur-fast);
}
@media (hover: hover) and (pointer: fine) {
  .viewer-bar .viewer-scale:hover { border-color: var(--ink); }
}
.viewer-bar .viewer-scale:active { transform: scale(.96); }
.viewer-bar .viewer-scale[aria-pressed="true"] {
  background: var(--brand); border-color: var(--brand); color: var(--on-bright);
}

/* Terpasang / Tata letak cetak — two buttons that share one answer, so they
   are drawn as one control: joined edges, and the group (not each button) is
   what gets pushed to the right. Colours are the buttons' own, unchanged. */
/* Two columns. Left: the hint, and the Terpasang / Tata letak pair under it.
   Right: "Lihat di atas plate" over "Bandingkan ukuran", both the width of the
   wider, so they line up edge to edge on every product (they used to wrap
   apart on a model with the pair) and every card's bar is two rows high. */
.viewer-bar .viewer-kiri {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--chip-gap);
}
.viewer-bar .viewer-kiri .viewer-susunan { margin-left: 0; }
.viewer-bar .viewer-kiri .viewer-scale { white-space: nowrap; }
.viewer-bar .viewer-kontrol {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--chip-gap);
  margin-left: auto;
}
.viewer-bar .viewer-kontrol .viewer-scale { white-space: nowrap; }
.viewer-bar .viewer-kontrol > .viewer-scale { margin-left: 0; text-align: center; }
/* At a 335 px bar the two columns of a multi-part model needed 337 px, so the right one
   wrapped under the left and the bar grew from two rows to four (94 -> 178 px on the product
   page, the same on catalogue cards after a tap). Two pixels less padding per side fits. */
@media (max-width: 420px) {
  .viewer-bar .viewer-scale { padding-inline: 8px; }
}
.viewer-bar .viewer-susunan { display: inline-flex; margin-left: auto; }
.viewer-bar .viewer-susunan .viewer-scale { margin-left: 0; }
.viewer-bar .viewer-susunan .viewer-scale:first-child {
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}
.viewer-bar .viewer-susunan .viewer-scale:last-child {
  border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px;
}
.viewer-bar .viewer-susunan .viewer-scale:focus-visible { position: relative; z-index: 1; }
.viewer-bar .viewer-susunan + .viewer-scale { margin-left: 0; }

.viewer.tall { aspect-ratio: 1 / 1; }
.viewer.wide { aspect-ratio: 16 / 10; }

/* ---------- price ---------- */

.price-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-top: auto; padding-top: 14px; border-top: 1px solid var(--line);
}
/* Prices in the heading face (2026-11-07), not monospace: mono for small data labels was template
   chrome (AUDIT_FRONTEND §2). Tabular figures are kept (see the rule near the top). */
.price { font-family: var(--head); font-size: 23px; font-weight: 800; letter-spacing: 0; color: var(--ink); }
.price-note { font-size: 12.5px; color: var(--muted); }

details.breakdown { margin-top: 12px; }
details.breakdown summary { cursor: pointer; font-size: 13px; color: var(--muted); font-weight: 600; }
details.breakdown table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13.5px; }
details.breakdown td { padding: 5px 2px; border-bottom: 1px solid var(--line); }
details.breakdown td:last-child { text-align: right; font-family: var(--mono); }
details.breakdown tr:last-child td { border-bottom: none; font-weight: 700; }

/* ---------- notices ---------- */

/* One calm note style: a tinted panel, no thick left rule (AUDIT_FRONTEND
   P1-3 — the striped left border was template texture). The tint carries the
   meaning: orange = note, red = problem, green = done, cyan = info. */
.notice {
  border: 0; background: var(--brand-soft); border-radius: var(--radius-lg);
  padding: 14px 17px; font-size: 14.5px; margin: 16px 0;
}
.notice.danger { background: var(--warn-soft); }
.notice.good { background: var(--good-soft); }
.notice.cyan { background: var(--cyan-soft); }
.notice strong { color: var(--brand-deep); }
.notice.danger strong { color: var(--warn); }
.notice.good strong { color: var(--good-ink); }
.notice.cyan strong { color: var(--cyan-ink); }

.banner-draft {
  background: var(--warn-deep);
  color: #fff; padding: 9px 20px; font-size: 13.5px;
  text-align: center; font-weight: 700;
}

/* ---------- forms ---------- */

.field { margin-bottom: 15px; }
.field label { display: block; font-size: 14px; font-weight: 700; margin-bottom: 5px; }
.field .help { display: block; font-size: 13px; color: var(--muted); font-weight: 400; margin-top: 2px; }
/* Keterangan yang panjangnya berbeda-beda ditaruh DI BAWAH kolomnya, bukan di
   dalam labelnya: label sebaris membuat kolom-kolom yang bersebelahan mulai di
   garis yang sama, berapa pun panjang keterangan masing-masing. */
.field :is(input, select, textarea, output) + .help { margin-top: 6px; }
.field input, .field select, .field textarea {
  width: 100%; min-height: var(--tap); padding: 10px 14px; font: inherit; font-size: 16px;
  border: 1.5px solid var(--line-strong); border-radius: var(--r-control);
  background: var(--card); color: var(--ink);
}
.field textarea { min-height: 96px; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: 3px solid color-mix(in srgb, var(--brand-line) 35%, transparent); outline-offset: 0;
  border-color: var(--brand-line);
}
.field input[type="number"] { font-family: var(--mono); text-align: right; }
.field textarea { min-height: 90px; resize: vertical; }
/* Galat SATU kolom, ditulis tepat di bawah kolomnya (nama dan nomor HP di
   keranjang). Kosong berarti tidak ada galat, dan tidak memakan tempat. Warnanya
   dijaga tools/cek-kontras.py: --warn di atas --card. */
.field .galat { display: block; font-size: 13px; font-weight: 600; color: var(--warn); margin-top: 5px; }
.field .galat:empty { display: none; }
.field input.salah { border-color: var(--warn); }

/* ---------- dropzone ---------- */

.dropzone {
  border: 2px dashed var(--line-strong); border-radius: var(--radius);
  padding: 42px 22px; text-align: center; background: var(--bg-alt);
  cursor: pointer; transition: border-color .15s ease, background .15s ease;
}
.dropzone.over { border-color: var(--brand-line); background: var(--brand-soft); }
@media (hover: hover) and (pointer: fine) {
  .dropzone:hover { border-color: var(--brand-line); background: var(--brand-soft); }
}
.dropzone .big { font-size: 17px; font-weight: 700; margin-bottom: 6px; }
.dropzone .small { font-size: 13.5px; color: var(--muted); }

/* ---------- spec table ---------- */

table.spec { width: 100%; border-collapse: collapse; font-size: 14.5px; }
table.spec th, table.spec td { padding: 9px 6px; border-bottom: 1px solid var(--line); text-align: right; }
table.spec th:first-child, table.spec td:first-child { text-align: left; }
table.spec th { font-size: 13px; color: var(--muted); font-weight: 600; }
/* Baris yang nilainya bisa lebih lebar dari kolomnya (tiga ukuran benda di
   Custom): satu sel selebar tabel, label di kiri dan nilai di kanan selagi
   muat sebaris. Kalau tidak muat, nilainya turun UTUH ke baris berikutnya dan
   tetap rata kanan - tidak patah di tengah "× 74.0 mm", dan tidak lagi memeras
   kolom label sampai label baris lain ikut patah. */
table.spec .spec-lebar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0 12px; }
table.spec .spec-lebar > :last-child { margin-left: auto; text-align: right; }
td.num, .num { font-family: var(--mono); }
tr.total td { border-top: 2px solid var(--ink); border-bottom: none; font-weight: 800; padding-top: 12px; }

/* ---------- cart ---------- */

.cart-item {
  display: grid; grid-template-columns: 72px 1fr auto; gap: 14px;
  align-items: center; padding: 15px 0; border-bottom: 1px solid var(--line);
}
.cart-item .thumb {
  width: 72px; height: 72px; border-radius: var(--radius);
  display: grid; place-items: center; font-size: 22px; font-weight: 800;
  color: var(--on-bright);
}
/* Gambar baris keranjang (2026-11-03, Alberd: "saya mau foto barangnya terlihat di list
   keranjangnya"): gambar mini bendanya dari tools/potret-produk.mjs, di latar kartu. Warna
   pilihan baris multiwarna dan set bersisi tampil sebagai bintik di pojoknya; gambar itu sendiri
   dalam warna bawaan. Tanpa gambar yang cocok baris tetap memakai kotak huruf di atas. */
.cart-item .thumb-foto {
  position: relative; overflow: hidden;
  /* Latar terang di KEDUA tema: gambar mini dirender dengan cahaya tema terang, dan bagian
     hitam tenggelam di kotak gelap (diukur 2026-11-03 di tema gelap). */
  background: #f4efe9; border: 1px solid var(--line);
}
.cart-item .thumb-foto img {
  width: 100%; height: 100%; object-fit: contain; display: block;
  -webkit-user-drag: none; user-select: none; -webkit-touch-callout: none;
}
.cart-item .thumb-warna {
  position: absolute; left: 4px; bottom: 4px; display: flex; gap: 3px;
}
.cart-item .thumb-warna span {
  width: 10px; height: 10px; border-radius: 50%;
  box-shadow: 0 0 0 1px var(--card), 0 0 0 2px var(--line-strong);
}
.cart-item h4, .cart-item h2 { margin: 0 0 3px; font-size: 15.5px; }
.cart-item .meta { font-size: 13px; color: var(--muted); }
/* Nama berkas unggahan bisa 95 karakter tanpa spasi. Tanpa ini lebar
   min-content kolom rincian = seluruh nama, dan kolom 1fr serta kisi
   halaman di atasnya ikut melebar: uji rusak 2026-10-23 membuat halaman 553 px
   di layar 360 px. `anywhere` (bukan break-word) juga mengecilkan min-content,
   jadi kisinya tidak melebar sejak awal. */
.cart-item > div { min-width: 0; }
.cart-item h2, .cart-item .meta, #namaFile { overflow-wrap: anywhere; }
/* Kolom harga. Angkanya tidak boleh patah: "Rp" di satu baris dan "81.000" di
   baris berikutnya terbaca sebagai dua hal. */
.cart-item .harga { text-align: right; }
.cart-item .harga .num { white-space: nowrap; }
/* Di layar sempit tiga kolom tidak muat — pada 375 px kolom harga tinggal 53 px
   dan itulah yang mematahkan angkanya. Harga turun ke barisnya sendiri di bawah
   rincian, sejajar dengan nama barangnya. */
@media (max-width: 520px) {
  .cart-item { grid-template-columns: 56px 1fr; align-items: start; }
  .cart-item .thumb { width: 56px; height: 56px; font-size: 19px; }
  .cart-item .harga {
    grid-column: 2; text-align: left;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px;
  }
  .cart-item > .btn-hapus { grid-column: 2; justify-self: start; }
}
.qty { display: inline-flex; align-items: center; gap: 8px; margin-top: 6px; }
/* Hanya anak LANGSUNG, dan bukan tombol hapus. Tombol hapus duduk di dalam .qty
   juga, dan dulu kedua aturan di bawah bocor ke sana: lebarnya terkunci 26 px
   dan labelnya dipaksa minimal 22 px (min-width mengalahkan max-width: 0), jadi
   yang tampil adalah "Hap" yang terpotong dan meluber keluar dari tombolnya. */
.qty > button:not(.btn-hapus) {
  width: var(--chip); height: var(--chip); border-radius: 50%; border: 1.5px solid var(--line-strong);
  background: var(--card); color: var(--ink); cursor: pointer; font-size: 15px; line-height: 1;
}
@media (hover: hover) and (pointer: fine) {
  .qty > button:not(.btn-hapus):hover { border-color: var(--brand-line); color: var(--brand-ink); }
}
.qty > span { font-family: var(--mono); min-width: 22px; text-align: center; font-weight: 700; }

/* Removing a row collapses it instead of making it vanish, so the eye can
   follow which line left. max-height is a layout property and §5 normally
   forbids animating those — allowed here as a deliberate exception: it runs
   once, on one element, on an explicit user action, and there is no
   compositor-only way to collapse a list row to zero height. */
.cart-item {
  overflow: hidden; max-height: 320px;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              max-height var(--dur-base) var(--ease-out),
              padding var(--dur-base) var(--ease-out);
}
.cart-item.removing {
  opacity: 0; transform: translateX(-18px);
  max-height: 0; padding-top: 0; padding-bottom: 0;
  border-bottom-color: transparent;
}

/* Totals tick to their new value rather than snapping, so a change in the
   summary is noticed when the eye is somewhere else on the page. */
.counting { font-variant-numeric: tabular-nums; }
/* Delete: an icon button that states its intent on hover, and turns red only
   when it is actually about to be pressed. The name floats ABOVE the icon
   (beside it, it covered the cart's "+" button) instead of widening the
   button: until 2026-10-31 it grew from 0 to 60px,
   which in the order book widened the actions column, made the whole card
   73px taller, slid the button out from under the pointer, shrank it back,
   and so on — the button shook (Alberd). §5: only opacity and transform move. */
.btn-hapus {
  position: relative;
  display: inline-flex; align-items: center;
  border: 1px solid var(--line-strong); background: var(--card); color: var(--muted);
  border-radius: var(--r-control); padding: 5px 10px; cursor: pointer;
  font: 600 12.5px/1 var(--sans);
  transition: border-color var(--dur-fast) ease, background-color var(--dur-fast) ease,
              color var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.btn-hapus svg { width: 14px; height: 14px; flex: none; }
.btn-hapus .label {
  position: absolute; right: 0; bottom: calc(100% + 6px); z-index: 2;
  padding: 5px 8px; border: 1px solid var(--warn); border-radius: var(--r-control);
  background: var(--card); color: var(--warn); white-space: nowrap; pointer-events: none;
  opacity: 0; transform: translateY(3px);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.btn-hapus:focus-visible .label { opacity: 1; transform: none; }
@media (hover: hover) and (pointer: fine) {
  .btn-hapus:hover {
    border-color: var(--warn); background: var(--warn-soft); color: var(--warn);
  }
  .btn-hapus:hover .label { opacity: 1; transform: none; }
}
.btn-hapus:active { transform: scale(.96); }

/* ---------- shipping picker ---------- */

.ship-option {
  display: flex; gap: 12px; align-items: flex-start;
  border: 1.5px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; margin-bottom: 10px; cursor: pointer;
  background: var(--card); transition: border-color .14s ease, background .14s ease;
}
@media (hover: hover) and (pointer: fine) {
  .ship-option:hover { border-color: var(--brand-line); }
}
.ship-option.selected { border-color: var(--brand-line); background: var(--brand-soft); }
/* Yang diredupkan hanya yang tidak bisa dipilih: nama, radio, tarif. Baris
   keterangannya TIDAK — pada pilihan yang belum tersedia, di situlah tertulis
   apa yang harus dilakukan supaya ia tersedia ("Pilih titik lokasi di peta…").
   Dulu seluruh labelnya opacity .55, dan kalimat itu jatuh ke kontras 2,05 : 1
   di tema terang: petunjuk yang paling perlu dibaca justru yang paling pudar. */
.ship-option.disabled { cursor: not-allowed; background: var(--bg-alt); }
.ship-option.disabled .name,
.ship-option.disabled .fee,
.ship-option.disabled input { opacity: .55; }
.ship-option input { margin-top: 3px; accent-color: var(--brand-line); flex: none; }
.ship-option .name { font-weight: 700; font-size: 14.5px; }
.ship-option .sub { font-size: 13px; color: var(--muted); margin-top: 2px; }
.ship-option .fee { margin-left: auto; font-family: var(--mono); font-weight: 800; white-space: nowrap; }
.ship-group-title {
  font-size: 13.5px; font-weight: 700;
  color: var(--muted); margin: 16px 0 8px;
}

/* ---------- map ---------- */

#peta { height: 300px; border-radius: var(--radius); border: 1px solid var(--line-strong); z-index: 1; }
.map-readout {
  display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
  font-size: 13px; color: var(--muted); margin-top: 10px;
}
.map-readout .coord { font-family: var(--mono); color: var(--ink); }

/* ---------- order timeline ---------- */

.timeline { list-style: none; padding: 0; margin: 0; }
.timeline li {
  position: relative; padding: 0 0 20px 40px;
  border-left: 2px solid var(--line); margin-left: 13px;
}
.timeline li:last-child { border-left-color: transparent; padding-bottom: 0; }
.timeline li::before {
  content: ''; position: absolute; left: -9px; top: 2px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--card); border: 2px solid var(--line-strong);
}
.timeline li.done { border-left-color: var(--good); }
.timeline li.done::before { background: var(--good); border-color: var(--good); }
.timeline li.now::before {
  background: var(--brand); border-color: var(--brand);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--brand) 25%, transparent);
}
.timeline li.cancelled::before { background: var(--warn); border-color: var(--warn); }
.timeline h4, .timeline h3.h4 { margin: 0 0 3px; font-size: 15.5px; }
.timeline li.now h4, .timeline li.now h3.h4 { color: var(--brand-deep); }
.timeline li:not(.done):not(.now) h4, .timeline li:not(.done):not(.now) h3.h4 { color: var(--muted); font-weight: 500; }
.timeline p { margin: 0; font-size: 14px; color: var(--ink-soft); }
.timeline .when { font-size: 12px; color: var(--muted); font-family: var(--mono); }

.code-badge {
  display: inline-block; font-family: var(--mono); font-size: 17px; font-weight: 800;
  letter-spacing: .04em; padding: 8px 14px; border-radius: var(--radius);
  background: var(--brand-soft); color: var(--brand-deep); border: 1px dashed var(--brand-line);
}

/* ---------- admin table ---------- */

.order-row {
  display: grid; gap: 12px; align-items: center;
  grid-template-columns: 1fr; padding: 14px 0; border-bottom: 1px solid var(--line);
}
@media (min-width: 800px) { .order-row { grid-template-columns: 190px 1fr 180px auto; } }
.order-row .code { font-family: var(--mono); font-weight: 800; font-size: 14px; }
.order-row select { padding: 7px 9px; border-radius: var(--r-control); border: 1px solid var(--line-strong);
  background: var(--bg); color: var(--ink); font: inherit; font-size: 13.5px; width: 100%; }
.pill {
  display: inline-block; font-size: 12.5px; font-weight: 700;
  padding: 3px 10px; border-radius: 999px;
}
.pill.wait { background: var(--sun-soft); color: #a16207; }
.pill.work { background: var(--brand-soft); color: var(--brand-deep); }
.pill.ship { background: var(--cyan-soft); color: var(--cyan-ink); }
.pill.done { background: var(--good-soft); color: var(--good-ink); }
.pill.dead { background: var(--warn-soft); color: var(--warn); }

/* ---------- colour picker ---------- */

.color-choice { display: flex; gap: 8px; margin: 12px 0 0; }
.color-choice button {
  flex: 1; border: 1.5px solid var(--line); background: var(--card); color: var(--ink-soft);
  border-radius: var(--radius); padding: 9px 6px; cursor: pointer;
  font: 700 12.5px/1.35 var(--sans); text-align: center; transition: all .14s ease;
}
.color-choice button .sub { display: block; font-weight: 500; font-size: 11.5px; color: var(--muted); margin-top: 2px; }
@media (hover: hover) and (pointer: fine) {
  .color-choice button:hover { border-color: var(--brand-line); }
}
.color-choice button[aria-pressed="true"] {
  border-color: var(--brand-line); background: var(--brand-soft); color: var(--brand-deep);
}
.color-choice button[aria-pressed="true"] .sub { color: var(--brand-deep); }
.swatch-row { display: inline-flex; gap: 3px; vertical-align: -2px; margin-right: 5px; }
.swatch { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }

/* ---------- steps ---------- */

/* No numbered circles on a line (redesign 2026-11-07: the round numbered timeline is a
   generator tell). The list stays an <ol>, so a screen reader still counts the steps; on
   screen each step is a heading in the heading face over a rule. */
ol.steps { list-style: none; padding: 0; margin: 0; }
ol.steps li {
  position: relative; padding: 16px 0 18px; border-top: 1px solid var(--line-strong);
}
ol.steps li:last-child { padding-bottom: 0; }
ol.steps h4, ol.steps h3.h4 { margin: 0 0 6px; font-size: 19px; font-family: var(--head); font-weight: 400; letter-spacing: 0; }
ol.steps p { margin: 0; color: var(--ink-soft); font-size: 15px; }

/* ---------- footer ---------- */

/* The footer closes the page on the night-blue band. Its links are a plain wrapping row
   (no middle dots between them since 2026-11-07, AUDIT_FRONTEND P1-3). */
footer.site {
  background: var(--deep); color: var(--on-deep-soft);
  padding: 40px 0 48px; font-size: 14.5px;
}
section:not(.alt):not(.malam) + footer.site, main + footer.site { margin-top: 0; }
footer.site .wrap { display: flex; gap: 20px 40px; flex-wrap: wrap; justify-content: space-between; }
footer.site strong { font-family: var(--head); font-weight: 400; font-size: 21px; color: var(--brand); }
footer.site a { color: var(--on-deep); text-decoration-color: rgba(243, 245, 251, .4); text-underline-offset: 3px; }
footer.site .kaki-tautan { display: flex; flex-wrap: wrap; gap: 10px 22px; max-width: 640px; align-content: start; }
@media (hover: hover) and (pointer: fine) {
  footer.site a:hover { color: var(--brand); text-decoration-color: currentColor; }
}

/* ---------- procedural decoration ---------- */

.dekor {
  position: relative; width: 100%; aspect-ratio: 4 / 3;
  background:
    linear-gradient(165deg, var(--bg-alt), var(--card));
  border-radius: var(--radius); overflow: hidden;
}
.dekor canvas { display: block; }
.dekor.tall { aspect-ratio: 1 / 1; }
.dekor.small { aspect-ratio: 1 / 1; max-width: 190px; margin: 0 auto; }
.dekor.strip { aspect-ratio: 16 / 7; }
.dekor[data-state="ready"] canvas { animation: fadeIn var(--dur-slow) var(--ease-out) both; }
/* Sits BELOW the decoration, never on it — same F83 reason as the viewer bar.
   A gradient scrim over a moving canvas hides the failure, it does not fix it. */
.dekor-caption {
  display: block;
  padding: var(--sp-3) 18px var(--sp-4);
  font-size: 12.5px; line-height: 1.5; color: var(--muted);
}

/* ---------- card interaction ---------- */

.card { transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .card:hover { border-color: var(--brand-line); }
}

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--brand-line); outline-offset: 2px; border-radius: 6px;
}

/* Filtering hides cards instead of rebuilding the grid. Rewriting innerHTML
   destroyed and recreated every WebGL context, which flickered and churned
   contexts for no reason. */
.card.filtered-out { display: none; }
/* No `content-visibility: auto` here, deliberately. It skips layout for the
   subtree, which collapses the viewer's aspect-ratio box to a few pixels and
   leaves the IntersectionObserver that builds the WebGL context with nothing
   meaningful to observe — the viewers never load. Lazy context creation is the
   bigger win of the two, so this one goes. */
#katalog > .card { animation: cardIn var(--dur-base) var(--ease-out) both; }
@keyframes cardIn {
  from { opacity: 0; transform: translateY(8px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* ---------- skeleton ---------- */

.skeleton {
  border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--card); overflow: hidden;
}
.skeleton .sk-viewer { aspect-ratio: 4 / 3; background: var(--bg-alt); }
.skeleton .sk-body { padding: var(--sp-4); }
.skeleton .sk-line {
  height: 12px; border-radius: 6px; background: var(--bg-alt); margin-bottom: 10px;
}
.skeleton .sk-line.short { width: 45%; }
.skeleton .sk-line.mid { width: 75%; }
.skeleton { position: relative; }
.skeleton::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg,
    transparent 20%, color-mix(in srgb, var(--line) 55%, transparent) 46%, transparent 72%);
  background-size: 220% 100%;
  animation: sheen 1.5s linear infinite;
}

/* ---------- toast ---------- */

#toasts {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 200; display: flex; flex-direction: column; gap: 8px;
  align-items: center; pointer-events: none; width: min(92vw, 420px);
}
.toast {
  pointer-events: auto; width: 100%;
  background: var(--ink); color: var(--bg);
  border-radius: var(--radius); padding: 12px 16px;
  font-size: 14px; font-weight: 600;
  box-shadow: var(--shadow-float);
  display: flex; align-items: center; gap: 10px;
  animation: toastIn var(--dur-base) var(--ease-spring) both;
}
.toast.leaving { animation: toastOut var(--dur-fast) var(--ease-out) both; }
.toast a { color: inherit; text-decoration: underline; margin-left: auto; white-space: nowrap; }
.toast.good { background: var(--good); color: #04241a; }
@keyframes toastIn  { from { opacity: 0; transform: translateY(16px) scale(.96); } }
@keyframes toastOut { to   { opacity: 0; transform: translateY(8px) scale(.98); } }

/* ---------- cart badge pulse ---------- */

.cart-count { transition: transform var(--dur-fast) var(--ease-spring); }
.cart-count.bump { animation: bump 420ms var(--ease-spring); }
@keyframes bump {
  0% { transform: scale(1); }
  38% { transform: scale(1.45); }
  100% { transform: scale(1); }
}

/* ---------- sticky buy bar (mobile) ---------- */

.buybar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  background: color-mix(in srgb, var(--card) 94%, transparent);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 12px;
  transform: translateY(110%);
  transition: transform var(--dur-base) var(--ease-out);
}
.buybar.show { transform: none; }
/* Two equal halves, "+ Keranjang" then "Beli sekarang": the order Indonesian marketplace
   apps use, so the thumb finds the primary action where it expects it. */
.buybar .btn { flex: 1 1 0; padding-inline: 12px; }
@media (min-width: 900px) { .buybar { display: none; } }

/* ---------- utility ---------- */

.mono { font-family: var(--mono); }
.muted { color: var(--muted); }
.center { text-align: center; }

/* Utility classes replacing the inline styles that were repeated most often
   across pages. Each one existed 5+ times as a style="" attribute. */
.txt-sm { font-size: 13.5px; }
.txt-xs { font-size: 12.5px; }
.txt-body { color: var(--ink-soft); font-size: 15px; }
.flush { margin: 0; }
.mt-0 { margin-top: 0; }
.mt-2 { margin-top: var(--sp-2); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.mt-6 { margin-top: var(--sp-6); }
.mb-2 { margin-bottom: var(--sp-2); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); }
.plain-link { text-decoration: none; color: inherit; }
.grow { flex: 1; }
.stack > * + * { margin-top: 14px; }
.hidden { display: none !important; }
.spinner {
  width: 18px; height: 18px; border-radius: 50%; display: inline-block;
  border: 2px solid var(--line-strong); border-top-color: var(--brand-line);
  animation: spin .7s linear infinite; vertical-align: -3px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- lampu foto: coba foto Anda dulu (2026-11-03) ---------- */
/* Bingkai = bidang gambar panel (104 x 78 mm). Cincin = 1 cm di panel, ukuran SEBENARNYA
   (WAJAH_MM / lebar gambar); sasaran sentuhnya diperbesar lewat ::before, lingkarannya tidak. */
.coba-bingkai {
  position: relative; width: 100%; aspect-ratio: 104 / 78; overflow: hidden; margin-top: 12px;
  border-radius: var(--radius); background: #120e0b; touch-action: none; cursor: grab;
}
.coba-bingkai:active { cursor: grabbing; }
.coba-bingkai:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.coba-kanvas { display: block; width: 100%; height: 100%; }
.coba-cincin {
  position: absolute; aspect-ratio: 1; translate: -50% -50%; padding: 0; border-radius: 50%;
  border: 2px solid #22c55e; background: none; box-shadow: 0 0 0 1px rgba(0, 0, 0, .65);
  cursor: move; touch-action: none;
}
.coba-cincin::before { content: ''; position: absolute; inset: -14px; border-radius: 50%; }
.coba-cincin:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
.coba-zum { display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 14px; }
.coba-zum input { flex: 1; accent-color: var(--brand); }
.coba-petunjuk { font-size: 13.5px; color: var(--ink-soft); margin: 8px 0 14px; }
.coba-sub { margin: 4px 0 8px; display: flex; align-items: center; gap: 8px; }
.coba-nyala {
  display: block; width: 100%; height: auto; border-radius: var(--radius); background: #120e0b;
  -webkit-touch-callout: none; user-select: none;
}
.coba-catatan { font-size: 13px; margin: 8px 0; }
.coba-izin { display: flex; gap: 8px; align-items: flex-start; font-size: 13.5px; margin: 14px 0 10px; }
.coba-izin input { margin-top: 3px; accent-color: var(--brand); }
.coba-setuju { width: 100%; }
.coba-galat { color: var(--warn); font-size: 14px; margin: 8px 0 0; }

/* ---------- ulasan (v2, 2026-11-06): testimoni, halaman barang, kotak ulasan di Lacak ---------- */
.ulasan { margin: 0; padding: 0; list-style: none; }
.ulasan li { padding: var(--sp-5) 0; border-top: 1px solid var(--line); }
.ulasan blockquote { margin: 8px 0 10px; font-size: 16px; color: var(--ink); }
.ulasan .siapa { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.ulasan .balasan {
  margin-top: 12px; padding-left: 14px; border-left: 2px solid var(--line-strong);
  font-size: 14.5px; color: var(--ink-soft);
}
.bintang { color: var(--brand-ink); letter-spacing: .12em; font-size: 15px; }
.bintang .bintang-kosong { color: var(--muted); opacity: .45; }
.ulasan-ringkas { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; margin: 0 0 6px; }
.ulasan-media { display: flex; gap: 8px; flex-wrap: wrap; margin: 4px 0 10px; }
.ulasan-media img, .ulasan-media video {
  width: 112px; height: 112px; object-fit: cover; border-radius: var(--radius); background: var(--bg-alt); display: block;
}
.ulasan-media video { width: 168px; }

/* Pemilih bintang: radio sungguhan (bisa dipakai papan ketik dan pembaca layar), digambar sebagai bintang. */
.bintang-pilih { display: inline-flex; gap: 2px; }
.bintang-satu { cursor: pointer; font-size: 34px; line-height: 1; color: var(--muted); position: relative; padding: 2px; }
.bintang-satu input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.bintang-satu span { display: inline-block; transition: transform .15s ease-out, color .15s; }
.bintang-satu.nyala { color: var(--brand-ink); }
.bintang-satu.nyala span { transform: scale(1.08); }
.bintang-satu:has(input:focus-visible) { outline: 2px solid var(--brand-line); outline-offset: 2px; border-radius: var(--radius); }

/* Sesudah mengirim ulasan: bintang yang masuk satu per satu, lalu ucapan terima kasih. */
.terima-ulasan { margin-top: 24px; padding: 18px; border-radius: var(--radius-lg); background: var(--good-soft); }
.bintang-terkirim { font-size: 26px; color: var(--brand-ink); letter-spacing: .1em; animation: bintang-masuk .6s ease-out both; }
.terima-ulasan .ucapan {
  margin: 10px 0 0; font-size: 16px; color: var(--ink);
  animation: ucapan-masuk .5s .35s ease-out both;
}
@keyframes bintang-masuk { from { opacity: 0; transform: translateY(6px) scale(.9); } to { opacity: 1; transform: none; } }
@keyframes ucapan-masuk { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .bintang-terkirim, .terima-ulasan .ucapan, .bintang-satu span { animation: none !important; transition: none !important; }
}
