/* ---------------------------------------------------------------------------
   Catalog-only styles for the public web store.

   Reuses the custom properties declared in pages/base.html's inline :root (the
   app's "Confident Blue" palette, transcribed from
   mobile/lib/core/theme/app_colors.dart) — nothing is redefined here, so the
   dark-scheme overrides in base.html apply to these rules automatically.

   Kept as a separate static file rather than appended to that inline <style>:
   it is fetched once and cached (hashed + compressed by WhiteNoise in prod)
   instead of being re-sent inline on every product view, and the legal pages
   never load it at all. Self-hosted, no imports, no third-party requests.

   Everything here works with JavaScript off: the gallery is a CSS scroll-snap
   strip, the filters and the picker are links, the plan switcher is a `:checked`
   rule, the search box is a GET form. The few rules scoped under `.gal-live` /
   `.chips-live` / `.calc-live` / `.is-swapping` style controls catalog.js adds
   on top of that — never anything the page needs in order to function. Each of
   those blocks is marked "catalog.js only"; the test that a control is a real
   link is in pages.tests.ProgressiveEnhancementTests, not here.
   --------------------------------------------------------------------------- */

.ctitle { font-size: clamp(26px, 4vw, 38px); margin: 26px 0 14px; }

/* breadcrumbs */
.crumbs { font-size: 14px; color: var(--muted); margin: 18px 0 0; }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--accent); }
.crumb-sep { padding: 0 2px; }

/* search — one field plus a button, on one row down to 360px */
.csearch { display: flex; gap: 10px; margin: 6px 0 16px; }
.csearch input {
  flex: 1 1 auto; min-width: 0; font: inherit; color: var(--text);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-field); padding: 11px 15px;
}
.csearch input:focus-visible { outline: 3px solid var(--ring); outline-offset: 1px; border-color: var(--accent); }
.csearch .btn { padding: 11px 20px; box-shadow: none; }

/* chips — the app's BrandChip; scroll sideways rather than wrap on a phone */
/* Exists only to position the arrows catalog.js adds, for the same reason as
   `.gal-frame`: the row itself is the scroll container. */
.chiprow { position: relative; }
.chips {
  display: flex; gap: 8px; margin: 0 0 12px;
  overflow-x: auto; padding-bottom: 4px; scrollbar-width: thin;
  /* A chip scrolled to by Tab lands clear of the edge (and of the fade). */
  scroll-padding-inline: 24px;
}
.chip {
  flex: 0 0 auto; text-decoration: none; white-space: nowrap; user-select: none;
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-chip); padding: 7px 14px; font-size: 14px; font-weight: 600;
}
.chip:hover { border-color: var(--accent); text-decoration: none; }
.chip.on { background: var(--accent-tint); border-color: var(--accent); color: var(--accent); }

/* --- catalog.js only: the scrollbar goes and the arrows + edge fades replace
   it. Deliberately *not* unconditional, unlike the gallery's: the gallery has a
   thumbnail row that navigates it with no script at all, and a chip row has no
   such second control — so with the script blocked the scrollbar stays, ugly
   and working, rather than leaving a mouse no way to reach the last category. */
.chips-live {
  padding-bottom: 0; scrollbar-width: none;
  --fade-s: 0px; --fade-e: 0px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-s),
    #000 calc(100% - var(--fade-e)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-s),
    #000 calc(100% - var(--fade-e)), transparent 100%);
}
.chips-live::-webkit-scrollbar { display: none; }
/* Set only while there is something to scroll to on that side, so a row that
   fits is not faded at either end. */
.chips-live.more-s { --fade-s: 34px; }
.chips-live.more-e { --fade-e: 34px; }
/* `.chip-nav` itself is styled with the gallery's arrows, below — it shares
   their base rule, which would otherwise re-set any size given up here. */

/* the product grid — 2 columns at 360px, filling out to 6 on a wide screen */
.pgrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 14px; margin: 18px 0 8px;
}
.pcard {
  display: flex; flex-direction: column; gap: 4px; text-decoration: none;
  color: var(--text); background: var(--surface); border: 1px solid transparent;
  border-radius: var(--r-card); padding: 12px; box-shadow: var(--shadow);
  transition: border-color .15s, transform .15s;
}
@media (prefers-color-scheme: dark) { .pcard { border-color: var(--border); } }
.pcard:hover { border-color: var(--accent); transform: translateY(-2px); text-decoration: none; }
.pcard-img {
  position: relative; display: block; aspect-ratio: 1 / 1; margin-bottom: 6px;
  background: var(--surface-2); border-radius: var(--r-field); overflow: hidden;
}
.pcard-img img { width: 100%; height: 100%; object-fit: contain; display: block; }
.pcard-ph { display: block; width: 100%; height: 100%; background: var(--surface-2); }
.pcard-badge {
  position: absolute; top: 8px; left: 8px; background: var(--accent);
  color: var(--accent-contrast); font-size: 11px; font-weight: 800;
  padding: 3px 8px; border-radius: var(--r-chip);
}
.pcard-badge.inline { position: static; display: inline-block; }
.pcard-name { font-weight: 700; font-size: 15px; line-height: 1.3; }
.pcard-attrs { color: var(--muted); font-size: 13px; }
.pcard-price { font-weight: 800; font-size: 17px; margin-top: 4px; }
.pcard-options { color: var(--muted); font-size: 12px; }

/* empty state + pagination */
.cempty {
  background: var(--surface); border-radius: var(--r-card); box-shadow: var(--shadow);
  padding: 40px 24px; margin: 18px 0; text-align: center;
}
.pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 26px 0 8px; }
.pager .btn { padding: 10px 18px; }

.cabout { max-width: var(--measure); margin: 40px 0 0; }

/* the app-promo banner on page 1 */
.promo {
  display: grid; grid-template-columns: 1.15fr .85fr; gap: 24px; align-items: center;
  background: var(--surface); border-radius: var(--r-card); box-shadow: var(--shadow);
  padding: 24px 26px; margin: 0 0 22px;
}
.promo-title { margin: 0 0 6px; font-size: 22px; }
.promo .lead { margin: 0; }
.promo-art { min-width: 0; }
.promo .hero-svg, .promo-art svg { display: block; width: 100%; height: auto; max-width: 340px; margin-inline: auto; }

/* product detail — the media column is capped rather than proportional: at
   1.05fr of a 1040px page the square gallery grew to ~500px tall and pushed the
   price and the picker below the fold. The app frames its gallery as a 230px
   card for the same reason (the offer is the hero, not the photo). */
.pdetail {
  display: grid; grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  gap: 30px; align-items: start; margin: 12px 0 8px;
}
/* Both columns need `min-width: 0`, and the media one is the load-bearing case:
   a grid item's automatic minimum size is its min-content width, and the
   gallery's is the widest photo, so on a phone (where the grid collapses to one
   column) the media column blew out to ~1650px and took the whole page sideways
   with it. The scroll strip's own `overflow-x` does not save it — the overflow
   is the *item*, one level up. */
.pdetail-info, .pdetail-media { min-width: 0; }
.ptitle { font-size: clamp(24px, 3.4vw, 34px); margin: 0 0 8px; }
.pprice { font-size: 30px; font-weight: 800; letter-spacing: -.02em; margin: 14px 0 2px; }
.pmonthly { color: var(--accent); font-weight: 700; margin: 0; }
.buyheading { font-size: 17px; margin: 26px 0 0; }

/* gallery — scroll-snap strip, thumbnails are same-page anchors */
.gal { min-width: 0; }
/* Exists only so the arrows can be positioned over the strip: the strip itself
   is the scroll container, so an absolutely-positioned child would scroll away
   with the photos. */
.gal-frame { position: relative; }
/* The surface is on the *slide*, not on the strip: with one background behind
   all of them the photos read as one long canvas that happens to scroll, rather
   than as separate pictures (owner, 2026-08-08).

   Both scrollbars go with it, and the vertical one is not cosmetic — where the
   platform draws classic (non-overlay) bars, the horizontal one eats ~15px of
   the 320px box, the full-height slide then overflows what is left, and the
   strip grows a *second*, vertical scrollbar. The thumbnails below are the
   script-free way to navigate, so nothing is lost by hiding them. */
.gal-strip {
  display: flex; gap: 12px; overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory; height: 320px; scrollbar-width: none;
}
.gal-strip::-webkit-scrollbar { display: none; }
.gal-strip:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
.gal-slide {
  flex: 0 0 100%; scroll-snap-align: center; margin: 0;
  height: 100%; display: grid; place-items: center; overflow: hidden;
  background: var(--surface-2); border-radius: var(--r-card);
}
/* The padding is inside the box (the global border-box), so it is the app's
   `EdgeInsets.all(md)` around a `BoxFit.contain` photo. */
.gal-slide img { width: 100%; height: 100%; object-fit: contain; display: block; padding: 14px; }
.gal-thumbs { display: flex; gap: 8px; margin-top: 10px; overflow-x: auto; scrollbar-width: thin; }
.gal-thumb {
  flex: 0 0 auto; border: 1px solid var(--border); border-radius: var(--r-field);
  overflow: hidden; line-height: 0;
}
.gal-thumb:hover { border-color: var(--accent); }
.gal-thumb.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.gal-thumb img { width: 62px; height: 62px; object-fit: contain; background: var(--surface); }

/* --- catalog.js only: the arrows, the zoom cursor and the lightbox. Scoped
   under .gal-live so nothing here can imply an affordance the page does not
   actually have when the script is blocked. */
.gal-live .gal-slide img { cursor: zoom-in; }
/* The active thumbnail is scrolled into view by the script, so this row loses
   its scrollbar too — but only once that is true. */
.gal-live .gal-thumbs { scrollbar-width: none; }
.gal-live .gal-thumbs::-webkit-scrollbar { display: none; }
.gal-nav, .lb-nav, .lb-close, .chip-nav {
  position: absolute; z-index: 2; width: 40px; height: 40px; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--surface); color: var(--text);
  box-shadow: var(--shadow); transition: opacity .15s, background .15s;
}
.gal-nav { top: 50%; transform: translateY(-50%); }
.gal-nav.prev { left: 10px; }
.gal-nav.next { right: 10px; }
/* The chip row's pair: smaller than the gallery's and sitting on the row's own
   edge — a chip is ~36px tall, so a 40px circle would stand taller than the
   thing it scrolls. Placed here, after the shared base, because the two rules
   have equal specificity and the later one wins. */
.chip-nav { top: 50%; transform: translateY(-50%); width: 32px; height: 32px; }
.chip-nav.prev { left: -6px; }
.chip-nav.next { right: -6px; }
.gal-nav:hover, .lb-nav:hover, .lb-close:hover, .chip-nav:hover { background: var(--accent-tint); }
/* Also how an arrow disappears when there is nothing on that side — including a
   chip row that fits, where both are disabled from the start. */
.gal-nav:disabled, .chip-nav:disabled { opacity: 0; pointer-events: none; }
/* On a touch screen the swipe is the gesture and an arrow just covers the
   product (or two chips); the thumbnails below stay the direct way to jump. */
@media (hover: none) and (pointer: coarse) { .gal-nav, .chip-nav { display: none; } }
/* A chevron drawn in CSS — no icon font, no SVG request. */
.gal-nav::before, .lb-nav::before, .chip-nav::before {
  content: ""; width: 9px; height: 9px;
  border: 2px solid currentColor; border-width: 2px 2px 0 0;
}
.gal-nav.prev::before, .lb-nav.prev::before, .chip-nav.prev::before { transform: rotate(-135deg); margin-left: 3px; }
.gal-nav.next::before, .lb-nav.next::before, .chip-nav.next::before { transform: rotate(45deg); margin-right: 3px; }
.lb-close::before, .lb-close::after {
  content: ""; position: absolute; width: 15px; height: 2px; background: currentColor;
}
.lb-close::before { transform: rotate(45deg); }
.lb-close::after { transform: rotate(-45deg); }

.lightbox {
  border: none; padding: 0; background: none; max-width: 100vw; max-height: 100vh;
  width: 100%; height: 100%; overflow: hidden;
}
.lightbox::backdrop { background: rgba(4, 10, 28, .88); }
.lightbox img {
  display: block; width: 100%; height: 100%; object-fit: contain; padding: 56px 16px;
}
.lb-close { top: 14px; right: 14px; }
.lb-nav { top: 50%; transform: translateY(-50%); }
.lb-nav.prev { left: 14px; }
.lb-nav.next { right: 14px; }

/* The in-flight state of a <main> swap. The delay is what keeps a fast swap
   from flashing — most land well inside it and are never seen at all. */
.is-swapping { cursor: progress; }
.is-swapping main { opacity: .55; transition: opacity .15s ease .12s; }
main:focus:not(:focus-visible) { outline: none; }

/* variant picker — the app's labelled chip rows, as links */
.vheading { font-size: 17px; margin: 24px 0 0; }
.vpick { display: grid; gap: 12px; margin: 18px 0 4px; }
.vlabel {
  display: block; font-size: 13px; font-weight: 800; color: var(--muted);
  margin-bottom: 7px;
}
.vchips { display: flex; flex-wrap: wrap; gap: 7px; }
.vchips .chip { font-size: 13px; padding: 6px 13px; }

/* the full configuration list, collapsed under the picker */
.vall { margin: 34px 0 0; }
.vall summary {
  cursor: pointer; width: fit-content; color: var(--accent);
  font-size: 15px; font-weight: 700; padding: 4px 0;
}
.vall summary:hover { text-decoration: underline; }
.vall[open] summary { margin-bottom: 12px; }

/* attribute + plan tables */
.tablewrap { overflow-x: auto; }
.ptable {
  width: 100%; border-collapse: collapse; background: var(--surface);
  border-radius: var(--r-card); overflow: hidden; box-shadow: var(--shadow);
}
.ptable th, .ptable td { text-align: left; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.ptable th { font-size: 13px; color: var(--muted); font-weight: 700; }
.ptable tbody tr:last-child td { border-bottom: none; }
.ptable .num { text-align: right; font-variant-numeric: tabular-nums; }
.ptable td.num { font-weight: 700; }
.small { font-size: 13px; }

/* used units — the app's radio-style _OptionTile: a mark, the price as the
   title, the specs under it, the whole tile tappable. */
/* A used-unit list is often ten rows long, and each tile is short — one column
   of full-page-width rows left most of the page empty. Fills sideways first,
   collapsing to a single column on a phone. */
.units {
  list-style: none; padding: 0; margin: 12px 0 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: 10px;
}
.unit-item { margin: 0; scroll-margin-top: 90px; }
.unit {
  display: flex; gap: 14px; align-items: flex-start; text-decoration: none;
  color: var(--text); background: var(--surface); border: 1px solid transparent;
  border-radius: var(--r-card); padding: 14px 18px; box-shadow: var(--shadow);
}
@media (prefers-color-scheme: dark) { .unit { border-color: var(--border); } }
.unit:hover { border-color: var(--accent); text-decoration: none; }
.unit.on { border-color: var(--accent); background: var(--accent-tint); }
.unit-mark {
  flex: 0 0 auto; width: 20px; height: 20px; margin-top: 4px; border-radius: 50%;
  border: 2px solid var(--muted); display: grid; place-items: center;
}
.unit-mark::after {
  content: ""; width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent); transform: scale(0);
}
.unit.on .unit-mark { border-color: var(--accent); }
.unit.on .unit-mark::after { transform: scale(1); }
.unit-main { display: grid; gap: 2px; min-width: 0; }
.unit-price { font-weight: 800; font-size: 18px; }
.unit-attrs { font-weight: 600; }
.unit-meta, .unit-note { color: var(--muted); font-size: 14px; }

/* installment panel — a piece of the header band moved into the page, the way
   the app's InstallmentCalculator is (design handoff, screen 04). The plan
   switcher is a radio group: `:checked + label + .calc-panel` reveals the
   matching panel, and flexbox `order` lifts every label into one row above it,
   so the whole thing works with no JavaScript. catalog.js then swaps the chip
   row for a `<select>` over those same radios (`.calc-live`) — the app's
   PlanPickerField, which a chip row per tariff is not: thirteen of them wrap to
   two rows and read as filters rather than as one choice. */
.calc {
  background: var(--band); color: var(--on-band);
  border-radius: var(--r-card); padding: 20px 22px;
  /* Contains the absolutely-positioned radios below. */
  position: relative;
}
.calc-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
/* Visually hidden but still focusable — `display: none` would take the plan
   chips out of the keyboard's reach entirely. */
.calc-radio { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.calc-chip {
  order: 1; cursor: pointer; white-space: nowrap; user-select: none;
  background: var(--band-fill); color: var(--on-band-muted);
  border-radius: var(--r-chip); padding: 7px 14px; font-size: 14px; font-weight: 700;
}
.calc-chip:hover { color: var(--on-band); }
.calc-radio:checked + .calc-chip { background: var(--on-band); color: var(--band); }
.calc-radio:focus-visible + .calc-chip { outline: 3px solid rgba(255, 255, 255, .65); outline-offset: 2px; }
.calc-panel { order: 2; width: 100%; display: none; }
.calc-radio:checked + .calc-chip + .calc-panel { display: block; }

/* --- catalog.js only: the dropdown that replaces the chip row. The field is
   the app's, drawn on the panel (a translucent white fill, white bold text, a
   chevron); the option list is the platform's own and keeps the ambient theme,
   which is exactly what the app does too — its picker sheet is its own surface,
   not part of the blue panel. */
.calc-live .calc-chip { display: none; }
.calc-select {
  font: inherit; font-size: 15px; font-weight: 700; line-height: 1.2;
  width: min(100%, 340px); margin-bottom: 4px; cursor: pointer;
  color: var(--on-band); background-color: var(--band-fill);
  border: none; border-radius: var(--r-field); padding: 12px 40px 12px 14px;
  appearance: none;
  /* The same chevron as the gallery arrows, in the one place a pseudo-element
     can't reach: a <select> has no rendered children of its own. */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 20px center, right 14px center;
  background-size: 7px 7px; background-repeat: no-repeat;
}
.calc-select:focus-visible { outline: 3px solid rgba(255, 255, 255, .65); outline-offset: 2px; }
/* Windows and Linux draw the option list in the select's own colors, which here
   would be white on a translucent white — so it is given the page's surface. */
.calc-select option { color: var(--text); background: var(--surface); }
.calc-head {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  align-items: baseline; gap: 8px 16px; margin: 16px 0 12px;
}
.calc-monthly {
  font-size: 26px; font-weight: 800; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.calc-monthly.full { font-size: 19px; font-weight: 700; }
.calc-total { font-size: 13px; color: var(--on-band-muted); font-variant-numeric: tabular-nums; }
.calc-sched {
  list-style: none; margin: 0; padding: 8px 14px;
  background: rgba(255, 255, 255, .12); border-radius: var(--r-panel);
}
.calc-sched li { display: flex; gap: 12px; align-items: baseline; margin: 0; padding: 5px 0; font-size: 13px; }
/* Wide enough for a two-digit instalment: the whole schedule is printed, so a
   12-month plan ends on "12" and a 16px column clipped it. */
.calc-sched .n {
  flex: 0 0 auto; width: 21px; color: var(--on-band-muted);
  font-variant-numeric: tabular-nums;
}
.calc-sched .d { flex: 1 1 auto; font-weight: 600; }
.calc-sched .a { flex: 0 0 auto; font-weight: 800; font-variant-numeric: tabular-nums; }

@media (max-width: 900px) {
  .pdetail { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  .gal-strip { height: 260px; }
}
@media (max-width: 720px) {
  .promo { grid-template-columns: 1fr; }
  .promo-art { order: -1; }
  .promo .hero-svg, .promo-art svg { max-width: 260px; }
}
@media (max-width: 640px) {
  /* Stacked rows: each cell carries its column label from data-label. */
  .ptable, .ptable thead, .ptable tbody, .ptable tr, .ptable td { display: block; width: 100%; }
  .ptable thead { display: none; }
  .ptable tr { border-bottom: 1px solid var(--border); padding: 6px 0; }
  .ptable tbody tr:last-child { border-bottom: none; }
  .ptable td { display: flex; justify-content: space-between; gap: 16px; border: none; padding: 6px 16px; }
  .ptable td::before { content: attr(data-label); color: var(--muted); font-size: 13px; }
  .ptable .num { text-align: right; }
  .calc { padding: 18px 16px; }
  .calc-monthly { font-size: 23px; }
}
@media (prefers-reduced-motion: reduce) {
  .pcard, .gal-nav, .lb-nav, .lb-close, .chip-nav, .is-swapping main { transition: none; }
  /* catalog.js reads the same query and stops passing `behavior: "smooth"`. */
  .gal-strip { scroll-behavior: auto; }
}
