/* ============================================================
   The web configurator — shared by Marine and Automotive.

   Full-bleed canvas with glass over it, in the studio's idiom: black,
   Inter, purple only on the thing you're acting on. Tokens come from
   design-system.css; this is the page's own vocabulary.
   ============================================================ */

:root {
  /* ── The theme ──
     Every colour the configurator draws with, so a page can be a different
     colour by naming these again. `marine.html` does exactly that — see
     marine/css/marine.css — and nothing below this block knows which theme it
     is painting. They were literals scattered through the file until then,
     which is why a light marine page was a search-and-replace rather than a
     stylesheet. */
  --bo-text:       #fff;
  /* The one solid disc on a translucent control: a switch's knob, a slider's
     thumb. Not `--bo-text`, because it is a surface and not a letter. */
  --bo-knob:       #fff;
  /* The page behind the catalog screen. */
  --bo-picker-bg:  radial-gradient(120% 80% at 50% 0%, rgba(8, 14, 24, 0.86), rgba(2, 4, 8, 0.96));
  /* The app bar, which sits over the scene and has to stay legible on it. */
  --bo-chrome:     rgba(6, 8, 12, 0.72);
  /* A card, and the picture on it before the image loads. */
  --bo-card:       rgba(255, 255, 255, 0.03);
  /* A card that is a card rather than a surface in a list: the "no catalog"
     panel, which is the one thing on screen when there is nothing else. */
  --bo-panel:      rgba(12, 14, 18, 0.9);
  /* An opaque sheet that lifts off the page: a dialog, a menu's sheet, the
     marker panel. Heavier than `--bo-glass`, which is a translucent strip. */
  --bo-sheet:      rgba(12, 14, 19, 0.96);
  /* What covers the whole scene while something is happening to it: the load
     overlay, the failure screen, a modal's backdrop. */
  --bo-scrim:      rgba(3, 6, 10, 0.82);
  --bo-scrim-solid: rgba(3, 6, 10, 0.94);
  --bo-backdrop:   rgba(3, 5, 8, 0.62);
  /* A toast, which floats over the scene with no surface behind it. */
  --bo-toast:      rgba(14, 14, 18, 0.94);
  /* A field you type into: recessed on a dark theme, which is a darker well,
     and recessed on a light one, which is not — so this inverts rather than
     being restated at a different alpha. */
  --bo-inset:      rgba(0, 0, 0, 0.35);
  /* The page behind everything, for the instant before the scene paints. */
  --bo-page:       #05080c;

  /* The ink, as channels, so every tint mixed from it in this file flips with
     the theme instead of being restated. Every `rgba(var(--bo-ink-rgb), …)`
     below is a fill the author chose an alpha for — a hover, a raised card, a
     switch's track — and the alphas are right for both themes. It is the
     colour they are mixed FROM that changes. */
  --bo-ink-rgb:    255, 255, 255;
  /* Danger, as a fill rather than a line. */
  --bo-bad-fill:   rgba(255, 107, 107, 0.16);
  --bo-bad-line:   rgba(255, 107, 107, 0.45);
  /* The refusal text on a control a rule has greyed out. */
  --bo-bad-text:   #FFB3B3;
  --bo-art:        #0b1017;
  --bo-art-veil:   linear-gradient(to bottom, rgba(6, 10, 18, 0), rgba(6, 10, 18, 0.55));
  /* Interactive: the colour a thing turns when it is chosen or being used. */
  --bo-accent:     var(--purple-mid);
  --bo-accent-hi:  var(--purple-hi);
  --bo-accent-soft: rgba(123, 47, 255, 0.22);
  --bo-accent-fill: rgba(123, 47, 255, 0.09);
  --bo-accent-line: rgba(178, 100, 255, 0.7);
  --bo-accent-edge: rgba(178, 100, 255, 0.5);
  --bo-accent-glow: 0 14px 30px -12px rgba(88, 24, 180, 0.55);
  /* The halo a spatial marker wears on the model, and the ghost of it in the
     marker panel's own border. Stronger than `--bo-accent-fill`: this one has
     to read against a photograph rather than against a card. */
  --bo-accent-halo: rgba(123, 47, 255, 0.45);
  --bo-accent-hair: rgba(123, 47, 255, 0.3);

  /* Status. Three states the configurator has to be able to say out loud —
     a session is live, a rule refused a choice, something is stale — and they
     are themed because amber on white is not a colour anybody can read. */
  --bo-ok:         #5FD9A6;
  --bo-ok-soft:    rgba(95, 217, 166, 0.13);
  --bo-ok-line:    rgba(95, 217, 166, 0.55);
  --bo-warn:       #FFC466;
  --bo-warn-line:  rgba(255, 196, 102, 0.5);
  --bo-warn-soft:  rgba(255, 196, 102, 0.14);
  --bo-bad:        #FF6B6B;
  --bo-bad-soft:   #FF9B9B;

  --bo-glass:      rgba(10, 12, 16, 0.66);
  --bo-glass-hi:   rgba(var(--bo-ink-rgb), 0.07);
  --bo-line:       rgba(var(--bo-ink-rgb), 0.12);
  --bo-line-hi:    rgba(var(--bo-ink-rgb), 0.32);
  --bo-dim:        rgba(var(--bo-ink-rgb), 0.56);
  --bo-faint:      rgba(var(--bo-ink-rgb), 0.34);
  --bo-radius:     14px;
  --bo-radius-sm:  9px;
  --bo-panel-w:    360px;
  /* One measure for everything stacked on the catalog screen — the
     Saved-configurations row, the joinable sessions, and the rail-plus-grid
     below them. They were 1080px, 880px and 1320px, so the buttons sat 120px
     inside the right-hand edge of the cards they belong to and the session
     list was narrower than both. Wide enough for two cards at their new size
     plus the rail. */
  --picker-measure: 1680px;
  --bo-mono:       ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;

  /* The app's own navigation bar, under the studio frame. Two bars stacked,
     and --is-chrome-h is the sum — which it was, then wasn't while the frame
     folded and reserved nothing, and is again now that the frame is
     permanently out and 46px (common/docs/025). Kept as the sum through both
     turns and that is why neither one touched this file: it is the right
     question ("how much chrome is over the scene") whatever the frame is
     doing. */
  --is-appbar-h:   54px;
  --is-chrome-h:   calc(var(--is-topbar-h) + var(--is-appbar-h));
}

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

html, body { height: 100%; }

body {
  background: var(--bo-page);
  color: var(--bo-text);
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 14px;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

button, input { font-family: inherit; font-size: inherit; color: inherit; }

/* ── Canvas ─────────────────────────────────────────────── */

#scene {
  position: fixed;
  inset: var(--is-chrome-h) 0 0 0;
  width: 100%;
  height: calc(100% - var(--is-chrome-h));
  display: block;
  touch-action: none;
}

/* The surface remote-rendered frames land in — see
   common/js/configurator/remote-render.js.

   Over the canvas rather than instead of it, and the whole fallback story is
   in that word: the canvas underneath never stops rendering the same build, so
   losing the stream is a fade rather than an interruption, and drags go
   straight through to the controls that were always handling them. */
#remote-scene {
  position: fixed;
  inset: var(--is-chrome-h) 0 0 0;
  width: 100%;
  height: calc(100% - var(--is-chrome-h));
  display: block;
  object-fit: cover;
  background: transparent;
  opacity: 0;
  transition: opacity 240ms ease;
  pointer-events: none;
}

#remote-scene.is-showing {
  opacity: 1;
}

/* ── The app bar ────────────────────────────────────────── */

/* The configurator's navigation bar, in the shape the native apps use: a way
   back, the name of what you are looking at, and the trailing controls — the
   session badge, and one menu.

   It is a real bar rather than a title floating over the scene. The floating
   version read beautifully on the render and was the wrong idea: the apps put
   the product's name, the session's status and every action in one strip
   because they are the same subject, and spreading them over four corners of
   a canvas meant nothing was next to the thing it applied to. It also
   guarantees a legible surface for the menus, which a translucent title over
   a sunlit sea does not. */
.appbar {
  position: fixed;
  top: var(--is-topbar-h);
  left: 0;
  right: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 0.8rem;
  height: var(--is-appbar-h);
  padding: 0 clamp(0.7rem, 2.4vw, 1.4rem);
  border-bottom: 1px solid var(--bo-line);
  background: var(--bo-chrome);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

/* Without a product there is nothing in this bar — see `renderMenus`, which
   sets the class. The catalog screen is about its cards and the studio frame
   above already badges the vertical, so the strip is pure border. The two
   layers that fill the area below the chrome move up to meet the frame; the
   canvas deliberately keeps its inset, since it sits behind both of them and
   moving it would resize the stage on every pick. */
body.is-appbar-empty .appbar { display: none; }
body.is-appbar-empty .picker,
body.is-appbar-empty .failure { top: var(--is-topbar-h); }

/* The chevron back to the catalog, which is where the apps put it — and the
   only way back now that "Change" has left the panel. On the catalog itself
   the bar it lives in is usually gone — except in a session, where the bar
   stays up to carry the badge (see `renderMenus`) and this would be a way
   back to the screen you are already on. Same for the two menus, which are
   hidden by `renderMenus` on their own account. */
body.is-picking .appbar__back { display: none; }

.appbar__back {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  flex: none;
  padding: 0.35rem 0.7rem 0.35rem 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--bo-dim);
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.appbar__back:hover { color: var(--bo-text); background: var(--bo-glass-hi); border-color: var(--bo-line); }
.appbar__chevron { font-size: 1.15rem; line-height: 1; margin-top: -2px; }

.appbar__titles { min-width: 0; flex: 1; }
.appbar__title {
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.appbar__subtitle {
  font-size: 0.74rem;
  color: var(--bo-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.appbar__trail {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
}

/* ── Menus ──────────────────────────────────────────────── */

/* One control, and a sheet of rows under it. The apps fold every action into
   one of these per screen; see `renderMenu` for why that trade is worth the
   extra tap. */
.menu { position: relative; flex: none; }
.menu[hidden] { display: none; }

/* Save, Send and Configure together sit beside the gear and are dressed as it
   is — see `renderActions`. `display: contents` so they are laid out by
   `.appbar__trail`'s own gap rather than a second one nested inside it, which
   also means the group is the only thing the page has to hide. */
.appbar__actions { display: contents; }
.appbar__actions[hidden] { display: none; }

.menu__button,
.appbar__action {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius-sm);
  background: var(--bo-glass-hi);
  color: var(--bo-dim);
  cursor: pointer;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.appbar__action { flex: none; }

/* The tooltip, drawn here rather than left to the browser.
   A `title` takes about a second to appear, renders in the OS's font at the
   OS's size, and follows the pointer — three reasons it reads as something
   outside the page. These buttons carry no label at all, so the tooltip is
   the label: it has to arrive quickly and look like it belongs.
   Anchored by its right edge so the rightmost button's tooltip stays on
   screen, and hidden while a menu below it is open, because the sheet is
   already saying what the button is for. */
.appbar__action,
.appbar__trail .menu__button { position: relative; }
.appbar__action::after,
.appbar__trail .menu__button::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 0.45rem);
  right: 0;
  z-index: 45;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius-sm);
  background: var(--bo-sheet);
  color: var(--bo-text);
  font-size: 0.72rem;
  font-weight: 400;
  line-height: 1.3;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  /* A short delay in, none out: a pointer crossing the row on its way
     somewhere else shouldn't leave four tooltips flashing behind it. */
  transition: opacity 0.12s ease 0.3s;
}
.appbar__action:hover::after,
.appbar__action:focus-visible::after,
.appbar__trail .menu__button:hover::after,
.appbar__trail .menu__button:focus-visible::after { opacity: 1; }
.appbar__trail .menu__button[aria-expanded="true"]::after { opacity: 0; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .appbar__action::after,
  .appbar__trail .menu__button::after { transition: none; }
}
.menu__button:hover,
.menu__button[aria-expanded="true"],
.appbar__action:hover {
  color: var(--bo-text);
  background: rgba(var(--bo-ink-rgb), 0.13);
  border-color: var(--bo-line-hi);
}
.menu__label { font-size: 0.8rem; font-weight: 500; }
/* "Something in here wants you" — the notification a control folded into a
   closed menu would otherwise have stopped being. */
.menu__button--badged::after {
  content: "";
  position: absolute;
  top: -1px;
  right: -1px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--bo-accent-hi);
  box-shadow: 0 0 0 2px rgba(6, 8, 12, 0.9);
}

.menu__sheet {
  position: absolute;
  top: calc(100% + 0.45rem);
  right: 0;
  z-index: 40;
  width: max(240px, 18rem);
  padding: 0.35rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: var(--bo-sheet);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.menu__sheet[hidden] { display: none; }

.menu__item {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  width: 100%;
  padding: 0.5rem 0.55rem;
  border: 0;
  border-radius: var(--bo-radius-sm);
  background: none;
  color: var(--bo-text);
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}
.menu__item:hover:not(:disabled) { background: rgba(var(--bo-ink-rgb), 0.09); }
.menu__item:disabled { opacity: 0.38; cursor: default; }
.menu__item--danger { color: var(--bo-bad-soft); }
.menu__glyph { display: grid; place-items: center; width: 18px; padding-top: 1px; color: var(--bo-dim); }
.menu__item:hover:not(:disabled) .menu__glyph { color: var(--bo-accent-hi); }
.menu__text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.menu__item-label { font-size: 0.86rem; font-weight: 500; }
.menu__hint { font-size: 0.72rem; color: var(--bo-faint); line-height: 1.35; }
.menu__rule { height: 1px; margin: 0.25rem 0.35rem; background: var(--bo-line); }

/* Icons are text-sized and take the row's own colour — see `icon` in ui.js. */
.ico-svg { flex: none; display: block; }
.btn .ico-svg { margin-right: 0.35rem; }
.btn .ico-svg:only-child { margin-right: 0; }

/* ── Controls panel ─────────────────────────────────────── */

/* **The left-hand side of the page.** It sat on the right for as long as
   there has been a panel, and it moved because the right is where the
   assistant is: the pill, the controls that appear beside it during a call,
   and the transcript that opens above them are all anchored to that corner,
   and the sheet was landing on top of the last of them. One thing per side —
   what you change the product with on the left, what you talk to it with on
   the right — and neither has to be moved out of the way to use the other. */
.panel {
  position: fixed;
  top: calc(var(--is-chrome-h) + 0.9rem);
  left: 0.9rem;
  bottom: 5.4rem;
  z-index: 12;
  width: var(--bo-panel-w);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: var(--bo-glass);
  backdrop-filter: blur(14px);
  transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.32s;
}
body.is-panel-collapsed .panel {
  transform: translateX(calc(-100% - 1.2rem));
  opacity: 0;
  /* Off-screen and transparent, but still a full-height element: without this
     it keeps swallowing clicks meant for the scene behind it. */
  pointer-events: none;
}
/* The voice controls belong to a product.
   `startVoice` builds them when one opens and never takes them down, so the
   pill was still sitting in the corner of the catalog screen offering to
   discuss a car nobody had picked — and the agent's whole context is "the
   person is looking at the product", which on this screen is not true.
   A conversation already running is the exception, and for the reason the
   session badge is: it is live state, and hiding the only way to end it is
   how a WebRTC session gets left open in an empty room. `is-talking` is set
   by `createDock` in `common/js/configurator/ai.js`; the transcript goes with
   the button, since reading one with no way to answer is half a control. */
body.is-picking:not(.is-talking) .convai-dock,
body.is-picking:not(.is-talking) .convai-log { display: none; }

body.is-picking .panel { opacity: 0; pointer-events: none; }

/* Shown only while the panel is hidden.
   Hidden by a rule on `body` rather than on the element: `.btn` also declares
   a `display`, it is later in this file, and a single class can't win against
   another single class — so the plain `.panel-reopen { display: none }` this
   used to be stopped hiding anything the day the buttons grew icons. */
.panel-reopen {
  position: fixed;
  top: calc(var(--is-chrome-h) + 0.9rem);
  left: 0.9rem;
  z-index: 12;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  /* The same defect as the viewpoint pill above, in the one other control that
     floats over the scene: `.btn--primary` gives it the accent wash and `.btn`
     gives it 6% ink, which over a bright sea is nothing to read a navy label
     against. A pill cannot borrow a surface here — the dark pages have no
     `--bo-glass` — so it gets the blur instead, which is what everything else
     floating on these pages is made of and needs no token at all. */
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
body:not(.is-panel-collapsed) .panel-reopen,
body.is-picking .panel-reopen { display: none; }

.panel__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid var(--bo-line);
}
.panel__title {
  flex: 1;
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--bo-faint);
}
.panel__body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.85rem;
  display: flex;
  flex-direction: column;
  /* The gap a grouped list leaves between sections — enough that a section's
     footnote reads as belonging to the card above it rather than to the
     heading below. */
  gap: 1.35rem;
}
.panel__body::-webkit-scrollbar { width: 8px; }
.panel__body::-webkit-scrollbar-thumb {
  background: rgba(var(--bo-ink-rgb), 0.14);
  border-radius: 4px;
}

/* ── Groups ─────────────────────────────────────────────── */

/* An inset-grouped list, which is what the native panel is: a heading, a card
   of rows, and the catalog's own explanation under the card. Copied on
   purpose rather than reinvented — named groups of choices, each with a
   footnote, is the shape a grouped list already has, and drawing it by hand
   was a `Divider()` and a guess at the insets. */

.group { display: flex; flex-direction: column; gap: 0.42rem; }

/* Name on the left, and on the right either a value group's figure or the
   name of the colour currently picked. Baseline rather than centre, so the two
   sit on the same line of text however much their sizes differ. */
.group__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0 0.25rem;
}
.group__head h3 {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bo-dim);
}
.group__value {
  font-family: var(--bo-mono);
  font-size: 0.8rem;
  color: var(--bo-text);
  white-space: nowrap;
  /* The figure changes under a moving thumb; equal-width digits keep it from
     shuffling sideways while it does. */
  font-variant-numeric: tabular-nums;
}
/* Which colour is picked. A swatch's ring says which one; only this says what
   it is called, and the name of a colour is half of what is being chosen. */
.group__chosen {
  font-size: 0.8rem;
  color: var(--bo-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 55%;
}

/* A read-out that folds. The Specification arrives closed — see `specPanel` —
   because it restates decisions made above it and at nine rows it was pushing
   the quote and Reset off the bottom of the panel.
   `<details>` brings its own disclosure triangle, which is the OS's and not
   ours, so it goes and the chevron takes its place. The gap between the head
   and the card is `.group`'s own, restated here because the two are no longer
   siblings in it. */
.group__fold { display: flex; flex-direction: column; }
.group__fold-head { cursor: pointer; align-items: center; }
.group__fold-head::-webkit-details-marker { display: none; }
.group__fold-head::marker { content: ''; }
.group__fold-head:hover h3 { color: var(--bo-text); }
.group__fold[open] .group__fold-head { margin-bottom: 0.42rem; }
.group__fold-chevron {
  flex: none;
  color: var(--bo-faint);
  /* The glyph points left; right is closed and down is open, and the turn
     between them is a quarter clockwise rather than three. */
  transform: rotate(180deg);
  transition: transform 0.18s ease;
}
.group__fold[open] .group__fold-chevron { transform: rotate(270deg); }
@media (prefers-reduced-motion: reduce) {
  .group__fold-chevron { transition: none; }
}

/* The card. One surface per group, rows separated by hairlines rather than by
   gaps — so a group of six options reads as one control with six rows in it,
   which is what it is. */
.group__card {
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: rgba(var(--bo-ink-rgb), 0.045);
  overflow: hidden;
}

/* The group's explanation, under the card, where a grouped list puts them. */
.group__foot {
  font-size: 0.74rem;
  color: var(--bo-faint);
  line-height: 1.45;
  padding: 0 0.35rem;
}

/* Swatches — a group where every option carries a colour */
.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  padding: 0.7rem;
}
.swatch {
  --swatch: #888;
  position: relative;
  width: 58px;
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
}
.swatch__ring {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--swatch);
  border: 1px solid rgba(var(--bo-ink-rgb), 0.22);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  transition: transform 0.18s, box-shadow 0.18s, outline-color 0.18s;
  outline: 2px solid transparent;
  outline-offset: 3px;
}
.swatch:hover:not(:disabled) .swatch__ring { transform: scale(1.08); }
.swatch[aria-current="true"] .swatch__ring { outline-color: var(--bo-accent-hi); }
.swatch__label {
  font-size: 0.66rem;
  color: var(--bo-dim);
  text-align: center;
  line-height: 1.25;
}
.swatch[aria-current="true"] .swatch__label { color: var(--bo-text); }
.swatch:disabled { cursor: not-allowed; }
.swatch:disabled .swatch__ring { opacity: 0.28; }
.swatch:disabled .swatch__label { opacity: 0.4; }
/* Struck through only when a rule forbids it. A control the whole panel has
   gone read-only for is not "unavailable", it is somebody else's. */
.swatch:disabled[data-blocked] .swatch__label { text-decoration: line-through; }

/* Options — rows, when they aren't colours. A tick on the chosen one, as the
   apps do it: a filled purple block reads as a button about to be pressed
   rather than as the state of the thing. */
.options { display: flex; flex-direction: column; }
.option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  text-align: left;
  border: 0;
  border-top: 1px solid var(--bo-line);
  background: none;
  padding: 0.6rem 0.75rem;
  cursor: pointer;
  transition: background 0.15s;
}
.option:first-child { border-top: 0; }
.option:hover:not(:disabled) { background: rgba(var(--bo-ink-rgb), 0.07); }
.option:disabled { cursor: not-allowed; opacity: 0.42; }
.option__text { display: flex; flex-direction: column; gap: 0.1rem; flex: 1; min-width: 0; }
.option__name { font-size: 0.88rem; }
.option__summary { font-size: 0.74rem; color: var(--bo-faint); line-height: 1.35; }
.option__blocked {
  font-size: 0.64rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bo-bad-text);
}
.option__check {
  flex: none;
  display: grid;
  place-items: center;
  width: 16px;
  color: var(--bo-accent-hi);
  opacity: 0;
  transition: opacity 0.15s;
}
.option[aria-current="true"] .option__check { opacity: 1; }
.option[aria-current="true"] .option__name { font-weight: 500; }

/* Toggle — the row carries the group's own name, which is why the section
   above it has no heading. */
.switch-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  padding: 0.62rem 0.75rem;
}
.switch-row__label { flex: 1; font-size: 0.88rem; }
.switch-row__state {
  font-size: 0.74rem;
  color: var(--bo-faint);
  font-variant-numeric: tabular-nums;
}
.switch { position: relative; width: 42px; height: 24px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch input:disabled { cursor: default; }
.switch__track {
  position: absolute;
  inset: 0;
  border-radius: 100px;
  background: rgba(var(--bo-ink-rgb), 0.16);
  transition: background 0.22s;
  pointer-events: none;
}
.switch__track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--bo-knob);
  transition: transform 0.22s;
}
.switch input:checked + .switch__track { background: var(--bo-accent); }
.switch input:checked + .switch__track::after { transform: translateX(18px); }
.switch input:disabled + .switch__track { opacity: 0.45; }

/* Value slider */
.slider { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.7rem 0.75rem; }
.slider__row { display: flex; align-items: center; gap: 0.5rem; }
.slider__bound { font-size: 0.68rem; color: var(--bo-faint); white-space: nowrap; }
input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 3px;
  border-radius: 2px;
  background: rgba(var(--bo-ink-rgb), 0.2);
  cursor: pointer;
}
input[type="range"]:disabled { cursor: default; opacity: 0.5; }
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--bo-knob);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}
input[type="range"]::-moz-range-thumb {
  width: 15px; height: 15px; border: 0; border-radius: 50%; background: var(--bo-knob);
}

/* A row that does something, rather than choosing something — Reset, and the
   rows in the sheets. */
.row-action {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  padding: 0.68rem 0.75rem;
  border: 0;
  background: none;
  color: var(--bo-accent-hi);
  font-size: 0.88rem;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}
.row-action:hover:not(:disabled) { background: var(--bo-accent-fill); }
.row-action:disabled { color: var(--bo-faint); cursor: default; }
.row-action__glyph { display: grid; place-items: center; width: 16px; }

/* Read-outs */
.spec {
  display: grid;
  grid-template-columns: minmax(90px, auto) 1fr;
  gap: 0.3rem 0.7rem;
  font-size: 0.82rem;
  padding: 0.7rem 0.75rem;
}
.spec dt { color: var(--bo-faint); text-transform: capitalize; }
.spec dd { color: var(--bo-text); text-align: right; }
.spec .spec__meta { font-style: normal; }
.spec .spec__total { font-weight: 600; padding-top: 0.35rem; border-top: 1px solid var(--bo-line); }

/* ── Buttons ────────────────────────────────────────────── */

.btn {
  /* Inline-flex rather than inline-block: most buttons now carry a symbol as
     well as a word — see `icon` in ui.js — and the two have to share a
     baseline without either being pushed off centre. */
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--bo-line-hi);
  border-radius: var(--bo-radius-sm);
  background: rgba(var(--bo-ink-rgb), 0.06);
  padding: 0.4rem 0.7rem;
  font-size: 0.8rem;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.18s, border-color 0.18s;
}
.btn:hover { background: rgba(var(--bo-ink-rgb), 0.13); border-color: rgba(var(--bo-ink-rgb), 0.5); }
.btn--primary {
  border-color: var(--bo-accent-edge);
  background: var(--bo-accent-soft);
  font-weight: 500;
}
.btn--primary:hover { background: var(--bo-accent-soft); }
.btn--icon { padding: 0.32rem 0.5rem; line-height: 1; }

/* ── Viewpoint pills ────────────────────────────────────── */

.viewpoints {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  z-index: 12;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: center;
  max-width: min(680px, calc(100vw - 2rem));
  transition: opacity 0.3s;
}
/* `renderViewpoints` hides the row when there is only one framing to pick, and
   a class with `display: flex` beats the UA's `[hidden] { display: none }` —
   so the row has to say it itself, or a product with one viewpoint shows a
   lone pill that does nothing when tapped. */
.viewpoints[hidden] { display: none; }
body.is-picking .viewpoints { opacity: 0; pointer-events: none; }
.viewpoints .ui-pill { cursor: pointer; font-family: inherit; }
/* A tint is not a surface.
   `--bo-accent-soft` is a wash meant to sit *on* something — 12% of the yard's
   red, 22% of the studio's violet — and this rule was using it as the pill's
   whole background, `background` being a shorthand that throws away the
   surface `.ui-pill` had. On the black pages nobody noticed. On `marine.html`
   the chosen framing went from an 88%-white pill to a 12%-red one over moving
   water, so picking a viewpoint made its own label the hardest to read of the
   two — backwards, and worse over the sea than over the sky.
   `background-image` carries the wash and `background-color` stays whatever
   the page's pill is made of. */
.viewpoints .ui-pill[aria-current="true"] {
  border-color: var(--bo-accent-line);
  color: var(--bo-text);
  background-image: linear-gradient(var(--bo-accent-soft), var(--bo-accent-soft));
}

/* ── Picker ─────────────────────────────────────────────── */

.picker {
  position: fixed;
  inset: var(--is-chrome-h) 0 0 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 1.6rem;
  padding: clamp(1.5rem, 5vh, 4rem) clamp(1rem, 4vw, 3rem);
  background: var(--bo-picker-bg);
  backdrop-filter: blur(10px);
  overflow-y: auto;
}
.picker[hidden] { display: none; }
/* Centred when the catalog fits, top-aligned when it doesn't — and never
   `justify-content: center`, which on a scrolling column puts the overflow
   above the scroll origin instead of below it. A catalog of more than a few
   cars would centre the heading off the top of the box, where no amount of
   scrolling brings it back. Auto margins centre the same way and resolve to
   zero the moment the free space runs out. */
.picker > :first-child { margin-top: auto; }
.picker > :last-child { margin-bottom: auto; }

/* Except on the configurators, which are top-anchored: centred horizontally,
   and starting under the app bar rather than floating in the middle of the
   box.

   Vertical centring is right for a screen that is one block of content — it is
   why the rule above exists — but these two are a filter rail beside a grid,
   and a rail is read from its top down. Centring moved that top with the
   number of results: filtering eight cars to two slid the whole panel down the
   page, so the facet you had just pressed was no longer where you pressed it.
   Anchored, the rail's first row is in the same place whatever the grid below
   it is doing.

   Matched on `.picker__body` rather than a modifier class, because that child
   IS the condition — it is the rail-and-grid layout, and only these two pages
   have one. `fashion.html`'s picker is a single centred column and keeps the
   rule above. */
.picker:has(.picker__body) > :first-child { margin-top: 0; }
.picker:has(.picker__body) > :last-child { margin-bottom: 0; }
.picker__head { text-align: center; }
.picker__eyebrow {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--bo-accent-hi);
  margin-bottom: 0.7rem;
}
.picker__head h1 {
  font-size: clamp(1.7rem, 4vw, 2.6rem);
  font-weight: 500;
  letter-spacing: -0.02em;
}
.picker__head p { color: var(--bo-dim); margin-top: 0.5rem; }

/* Results with a rail down their left, the way a shop lists cars. `stretch`
   rather than the flex default so the rail's own background runs the height of
   the grid beside it, and `min-width: 0` on the results because a grid child of
   a flex row will otherwise refuse to shrink below its columns' minimum and
   push the rail off the screen.

   The rail is closed to start with and the key to it lives up in the picker's
   control row — see `.filters__toggle` — so this layout is the two things it
   always was. */
.picker__body {
  display: flex;
  align-items: stretch;
  gap: clamp(1rem, 2.5vw, 2rem);
  width: 100%;
  max-width: var(--picker-measure);
  margin: 0 auto;
}
.picker__results {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
}
.picker__none {
  padding: clamp(2rem, 8vh, 4rem) 0;
  text-align: center;
  color: var(--bo-dim);
}
.picker__none[hidden] { display: none; }

.picker__cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
  /* Every card the same size, not just every card in the same row.
     A grid row already stretches its items to the tallest one, so the cards
     were level with their neighbours and stepped between rows — which reads as
     a bug rather than as a layout. `1fr` on the implicit rows makes every row
     as tall as the tallest card in the grid. What that costs is that the
     longest summary sets the height for all of them, which is why the body
     below clamps rather than wraps. */
  grid-auto-rows: 1fr;
  gap: 1rem;
  max-width: 1080px;
  width: 100%;
  margin: 0 auto;
}
/* Bigger cards inside the rail's layout, and only there: the photograph is the
   thing you are choosing by, and at 248px a car was a thumbnail with a caption.
   Scoped to `.picker__results` so the fashion page — same class, no rail, and
   its own card besides — keeps the size its grid was tuned at.

   **Never one column on a desktop.** The track minimum is `50% - half a gap`
   as well as a pixel figure, and that is what guarantees it: a track that
   never asks for more than half the row always leaves room for a second one.
   Before, the top tier asked for a flat 660px, and between about 1500px and
   1690px of viewport the grid is 1121–1320px — enough for one 660px track and
   not two. A 15" laptop at full screen sat exactly in that hole and got a
   single 700px card per row, which is the one shape this grid should never
   take. The pixel half of the `min()` still does the work on a wide screen,
   where it decides when a third column is worth having.

   `min(…, 100%)` on the phone tier rather than 50%, because down there one
   column *is* the right answer and two 165px cards are not.

   `--card-w` caps the card itself, which is what stops it resizing as the grid
   empties. `auto-fit` collapses the tracks a filter left empty, so filtering
   eight cars down to one used to hand that one car the whole 1345px measure —
   a card twice the size of its neighbours a moment earlier. 560px in both
   tiers now: 700 was tuned for the one-column case that no longer happens,
   and a card that size is a banner with a photograph in it. */
.picker__results {
  --card-w: 560px;
}
.picker__results .picker__cards {
  /* The cap is on the *grid*, not on the track.
     With `1fr` tracks the row took the whole measure and each card sat at the
     left of a track wider than itself, so the space between two cards grew
     with the window while the cards stayed the same size — the pair drifted
     apart as you widened it. The obvious fix, a definite track maximum, is
     worse than it looks: `auto-fit` counts its repetitions off the maximum
     when that maximum is a length, so `minmax(520px, 560px)` in a 1136px grid
     fits *one* 560px track and drops back to a single column — measured, at
     1512px of viewport, which is the width this whole thing was wrong at
     before.
     So the tracks stay `1fr` and the grid is capped at two columns' worth
     instead. Below the cap the tracks share what there is; above it they are
     exactly a card wide. Either way the gap between two cards is the grid's
     gap and nothing else. `margin: 0` because the base rule centres this. */
  grid-template-columns: repeat(auto-fit, minmax(min(520px, calc(50% - 0.7rem)), 1fr));
  gap: 1.4rem;
  max-width: calc(var(--card-w) * 2 + 1.4rem);
  margin: 0;
}
/* Left of its track rather than stretched across it, so the first card starts
   at the same place however many there are. */
.picker__results .product-card {
  max-width: var(--card-w);
  justify-self: start;
  width: 100%;
}
@media (max-width: 1500px) {
  .picker__results { --card-w: 520px; }
  .picker__results .picker__cards {
    grid-template-columns: repeat(auto-fit, minmax(min(420px, calc(50% - 0.575rem)), 1fr));
    gap: 1.15rem;
    max-width: calc(var(--card-w) * 2 + 1.15rem);
  }
}
/* Below this the rail is gone and a card has the width to itself — see the
   phone rules further down — so one column is the answer and the 50% floor
   above would make it two cramped ones. */
@media (max-width: 860px) {
  .picker__results .picker__cards {
    grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
    max-width: none;
  }
}

/* ── The filter rail ────────────────────────────────────── */

/* The same dark glass the app bar and the control panel are made of, so the
   rail reads as part of the app rather than as a form dropped on it. Sticky,
   because the grid beside it scrolls and a filter you have to scroll back up
   to reach is a filter you stop using. */
.filters {
  flex: none;
  width: 248px;
  align-self: flex-start;
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: var(--bo-glass);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  overflow: hidden;
}
.filters[hidden] { display: none; }

/* The key to the rail: first control in the picker's row, at the left end of
   the same measure the rail and the grid start at, so it lines up with the
   panel it opens rather than floating in the space above it. Its neighbours —
   Saved configurations, Configure together — are errands you might run before
   choosing; this one is about the list itself, which is why it sits at the
   other end of the row from them.

   Icon-only, and square: the rail has a title of its own once it is open, and
   a button captioned "Filter" beside a grid of photographs is a word doing
   work the glyph already does. The word is still there for anybody who needs
   it — as the `title` and the `aria-label`, both written by filters.js.

   With something to resume, the chips sit next to it and their own
   `margin-right: auto` pushes the pair on the right away from both. With
   nothing to resume the chips are gone, and the auto margin has to move to the
   button or the row collapses into one huddle on the right — two auto margins
   in the same row would split the space between them and leave the chips
   adrift in the middle, which is why this is conditional. */
.filters__toggle {
  justify-content: center;
  width: 2rem;
  padding: 0.4rem;
  color: var(--bo-dim);
  transition: background 0.18s, border-color 0.18s, color 0.18s;
}
.picker__saved:has(.resume[hidden]) .filters__toggle { margin-right: auto; }
.filters__toggle:hover { color: var(--bo-text); }
.filters__toggle[hidden] { display: none; }
/* Open, or filtering: the accent says the panel is out, and — while the panel
   is shut — that the grid is showing less than the whole catalog. */
.filters__toggle[aria-expanded="true"],
.filters__toggle.is-on {
  border-color: var(--bo-accent-edge);
  background: var(--bo-accent-soft);
  color: var(--bo-accent-hi);
}

.filters__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-height: 1.6rem;
  padding: 0.85rem 1.05rem;
  border-bottom: 1px solid var(--bo-line);
}
.filters__title {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--bo-dim);
}
/* A quiet text control, not another pill. It is the least important thing in
   the rail and it only exists once a filter is on. */
.filters__reset {
  padding: 0;
  border: 0;
  background: none;
  color: var(--bo-accent-hi);
  font-size: 0.72rem;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.2s;
}
.filters__reset:hover { color: var(--bo-text); }
.filters__reset[hidden] { display: none; }

/* A hairline between facets rather than a gap: at a glance the rail is a list
   of questions, and a rule is what says where one ends. */
.filters__facet {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.95rem 1.05rem 1.1rem;
}
.filters__facet + .filters__facet { border-top: 1px solid var(--bo-line); }

/* The facet's name on the left and where it currently stands on the right —
   "Price  €26,000 – €148,900". The read-out is the label: a slider whose value
   is only in its thumb position is a slider you have to guess at. */
.filters__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.4rem;
}
.filters__label > span:first-child {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bo-dim);
}
.filters__readout {
  flex: 1;
  font-size: 0.72rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--bo-text);
  text-align: right;
}

/* The one-ended facets — "Up to 62 m". One slider, the full width of the row:
   the read-out above it says which end it is, so there is nothing to put
   beside it. */
.filters__solo {
  display: flex;
  align-items: center;
}

/* The price band: one track, two handles, and the chosen span lit between
   them. Two range inputs are stacked here at the same size and position — see
   `priceFacet` in filters.js for why it is still two — so the track and the
   band are drawn on this wrapper and the inputs bring nothing but their
   thumbs.

   `--from` and `--to` are where those thumbs are. The padding is half a thumb
   at each end so the band's paint lines up with the thumb that marks it: a
   range input keeps its thumb inside its own box, so its travel is the track
   minus one thumb width and a gradient run edge to edge would sit ahead of the
   thumb at one end and behind it at the other. */
.filters__track {
  --from: 0%;
  --to: 100%;
  --thumb: 15px;
  position: relative;
  height: var(--thumb);
  padding: 0 calc(var(--thumb) / 2);
}
.filters__track::before {
  content: "";
  position: absolute;
  inset: calc(50% - 2px) 0 auto 0;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(to right,
    var(--bo-accent-hair) 0 var(--from),
    var(--bo-accent) var(--from) var(--to),
    var(--bo-accent-hair) var(--to) 100%);
}


/* The track is drawn rather than left to the platform: a native range on dark
   glass is a grey line with a grey dot on it. `--at` comes from `paintTrack`
   in filters.js and is where the thumb is, so the filled half shows how far
   along the slider stands without reading the number.

   The one-ended facets use this as it stands. The price band's two handles
   turn the track off again and let `.filters__track` draw it, because a fill
   that starts at the left is the wrong picture for a span with two ends. */
.filters__range {
  --at: 100%;
  flex: 1;
  min-width: 0;
  height: 1.15rem;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}
.filters__range::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(to right,
    var(--bo-accent) 0 var(--at), var(--bo-line-hi) var(--at) 100%);
}
.filters__range::-moz-range-track {
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(to right,
    var(--bo-accent) 0 var(--at), var(--bo-line-hi) var(--at) 100%);
}
.filters__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  /* Half the thumb above the 3px track, so it rides on the line rather than
     sitting under it. */
  margin-top: -5px;
  border: 0;
  border-radius: 50%;
  background: var(--bo-knob);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
  transition: transform 0.15s;
}
.filters__range::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--bo-knob);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
}
.filters__range:hover::-webkit-slider-thumb { transform: scale(1.15); }
.filters__range:active::-webkit-slider-thumb { transform: scale(1.15); }
.filters__range:focus-visible {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 3px;
  border-radius: 999px;
}

/* ── The one-ended sliders, in the band's clothes ──

   Same weight, same thumb, same gradient as the price band above: they are
   two rows of one rail, and a band next to a hairline with a different knob
   on it reads as two controls that arrived from different places.
   The fill is still `--at` — one end, filled up to the thumb — because that
   is what "up to 4.6 m" means.

   `input.` on each selector for the reason given in the block below: there is
   a global `input[type="range"]` rule near the top of this file and it
   outweighs a lone class. That is why `.filters__range`'s own thumb rules
   have never taken effect. */
input.filters__range {
  --thumb: 15px;
  height: var(--thumb);
  background: none;
}
input.filters__range::-webkit-slider-runnable-track { height: 4px; }
input.filters__range::-moz-range-track { height: 4px; }
input.filters__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: var(--thumb);
  height: var(--thumb);
  margin-top: calc((4px - var(--thumb)) / 2);
  border: 0;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--bo-accent-hi), var(--bo-accent));
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
}
input.filters__range::-moz-range-thumb {
  width: var(--thumb);
  height: var(--thumb);
  border: 0;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--bo-accent-hi), var(--bo-accent));
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
}

/* ── The price band's two handles ──

   **Every selector here names the element as well as the class, and that is
   load-bearing.** There is a global `input[type="range"]` rule at the top of
   this file — 3px tall, a grey background, a 15px white knob — and
   `input[type="range"]` outweighs a lone class, so `.filters__range--handle`
   on its own loses to it and so does `.filters__range` above. It was visible:
   the band had a grey 3px line and two white knobs sitting above the purple
   it was supposed to be drawn on, because the handles were 3px boxes pinned
   to the top of a 22px wrapper with their own background showing.

   Both handles fill the wrapper and stack, so either thumb can be dragged to
   any point on the track. The inputs are transparent to the pointer and their
   thumbs are not: that is what keeps a press on the track from being
   swallowed by the handle lying over it. */
.filters__track > input.filters__range--handle {
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  background: none;
  border-radius: 0;
  pointer-events: none;
}
/* The band is the wrapper's, so the input brings no track of its own — only
   the height, which is what the thumb is centred against. */
.filters__track input.filters__range--handle::-webkit-slider-runnable-track {
  height: 4px;
  background: none;
}
.filters__track input.filters__range--handle::-moz-range-track {
  height: 4px;
  background: none;
}

/* Big enough to put a finger on, and the one thing in the rail with a
   gradient in it: a band has two ends and they are what you grab. Sized to
   the rail rather than to a phone's thumb — this is a 248px column of 0.7rem
   type, and a knob big enough to press with a finger sat on it like a bead.
   The negative margin centres it on the track it rides. */
.filters__track input.filters__range--handle::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  pointer-events: auto;
  width: var(--thumb);
  height: var(--thumb);
  margin-top: calc((4px - var(--thumb)) / 2);
  border: 0;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--bo-accent-hi), var(--bo-accent));
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
}
.filters__track input.filters__range--handle::-moz-range-thumb {
  pointer-events: auto;
  width: var(--thumb);
  height: var(--thumb);
  border: 0;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--bo-accent-hi), var(--bo-accent));
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
}
/* Set by `paint`: the handle with room left to travel lies over the other, so
   the two never lock each other at an end of the track. */
.filters__track > input.filters__range--handle.is-over { z-index: 1; }
/* The focus ring goes round the thumb rather than the track, because the
   track belongs to both handles and an outline on it could not say which one
   has the keyboard. */
.filters__track > input.filters__range--handle:focus-visible { outline: none; }
.filters__track input.filters__range--handle:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 2px;
}
.filters__track input.filters__range--handle:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 2px;
}

/* Selectable pills, not checkboxes. "5" in a box you tick is a form; "5" you
   press is what every shop's facet list looks like, and on a dark panel a
   native checkbox is the one unstyleable thing in the rail. The input stays in
   the markup and stays focusable — it is what holds the state and what a
   screen reader reads — it just stops being what you see. */
.filters__choices {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.filters__choice {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.1rem;
  padding: 0.28rem 0.6rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--radius-pill);
  color: var(--bo-dim);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.filters__choice input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.filters__choice:hover {
  color: var(--bo-text);
  border-color: var(--bo-line-hi);
}
.filters__choice:has(input:checked) {
  border-color: var(--bo-accent-line);
  background: var(--bo-accent-soft);
  color: var(--bo-text);
}
.filters__choice:has(input:focus-visible) {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 2px;
}

@media (max-width: 880px) {
  .picker__body { flex-direction: column; }
  .filters {
    position: static;
    width: 100%;
    align-self: auto;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
  }
  .filters__head {
    width: 100%;
    order: -1;
  }
  /* Side by side along the band, so the rule between them runs the short way. */
  .filters__facet { flex: 1 1 13rem; }
  .filters__facet + .filters__facet {
    border-top: 0;
    border-left: 1px solid var(--bo-line);
  }
  .picker__results { --card-w: 420px; }
  .picker__results .picker__cards {
    grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  }
}

.product-card {
  display: flex;
  flex-direction: column;
  text-align: left;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: var(--bo-card);
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.25s, border-color 0.25s, background 0.25s, box-shadow 0.25s;
}
.product-card:hover {
  transform: translateY(-3px);
  border-color: var(--bo-accent-edge);
  background: var(--bo-accent-fill);
  /* The lift needs something under it or the card looks like it slid rather
     than rose. Tinted rather than black: the cards sit on black already. */
  box-shadow: var(--bo-accent-glow);
}
/* The art is the brightest thing on a dark card and it ends on a hard edge
   against the body. A few pixels of shade at the foot of the picture carries
   one into the other. */
.product-card__art::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 38%;
  background: var(--bo-art-veil);
  pointer-events: none;
}
.product-card__art { aspect-ratio: 16 / 10; background: var(--bo-art); overflow: hidden; position: relative; }
.product-card__art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.product-card__placeholder {
  width: 100%; height: 100%;
  display: grid; place-items: center;
  font-size: 2rem; color: var(--bo-faint);
}
/* Fills whatever the tallest card left over, so the price below can sit on the
   floor of every card rather than wherever its own text ran out. */
.product-card__body {
  padding: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 1;
  min-height: 0;
}
.product-card__name {
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1.3;
  /* Two lines is enough for "Kodiaq 2.0 TSI 4x4 L&K" and stops a long name
     pushing the picture around. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Clamped, not wrapped. These summaries run from eleven words to sixty, and a
   card that grows to fit the longest one is a card that has decided the grid's
   height on behalf of the other seven.

   Two lines, down from three. A card is a name, a picture and a price, and the
   description is there to say which car this is — not to finish the sentence.
   Two lines is what the catalog opens with in every case (the make, the engine
   and the year), and what follows is the part the product page is for. The
   whole thing is in the `title` for anyone hovering. */
.product-card__summary {
  font-size: 0.82rem;
  color: var(--bo-dim);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* Reserved as well as capped, so the price below starts at the same height
     on a one-line summary as on a two-line one. */
  min-height: 2.9em;
}
/* What the product starts at. `margin-top: auto` puts it on the floor of the
   card, level across the whole grid, because it is the same kind of fact on
   every one of them — and a column of prices is only comparable if they are
   in a line. The label is small and dim and the figure is not: the words are
   the same on all eight cards, and the number is the part anybody is reading. */
.product-card__price {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-top: auto;
  padding-top: 0.5rem;
}
.product-card__price-label {
  font-size: 0.7rem;
  color: var(--bo-faint);
}
.product-card__price-amount {
  font-size: 0.92rem;
  font-weight: 500;
  color: var(--bo-text);
  white-space: nowrap;
}
.product-card:disabled { cursor: default; opacity: 0.5; }

/* ── Loading ────────────────────────────────────────────── */

.loading {
  position: fixed;
  inset: var(--is-chrome-h) 0 0 0;
  z-index: 25;
  display: grid;
  place-items: center;
  /* The catalog's own backdrop, not a scrim over the stage. Opening a product
     used to darken whatever was behind it — an empty stage on the way in, the
     *previous* product on the way between two — so the wait read as something
     happening to the scene rather than as the page it is: the same surface
     the cards were sitting on a moment ago, with the name of the one you
     picked on it. Same variable as `.picker`, so a page that re-themes the
     catalog gets a matching wait for free. */
  background: var(--bo-picker-bg);
}
.loading[hidden] { display: none; }
.loading__inner { width: min(320px, 80vw); text-align: center; }
.loading__label { font-size: 0.85rem; color: var(--bo-dim); margin-bottom: 0.7rem; }
.loading__track {
  height: 3px;
  border-radius: 2px;
  background: rgba(var(--bo-ink-rgb), 0.12);
  overflow: hidden;
}
.loading__bar {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--bo-accent), var(--bo-accent-hi));
  transition: width 0.25s;
}
.loading__hint { font-size: 0.72rem; color: var(--bo-faint); margin-top: 0.6rem; }

/* ── Failure ────────────────────────────────────────────── */

.failure {
  position: fixed;
  inset: var(--is-chrome-h) 0 0 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: 2rem;
  background: var(--bo-scrim-solid);
}
.failure[hidden] { display: none; }
.failure__card {
  width: min(460px, 100%);
  border: 1px solid var(--bo-line);
  border-radius: 18px;
  background: var(--bo-panel);
  padding: 1.8rem;
}
.failure__card h2 { font-size: 1.25rem; font-weight: 500; margin-bottom: 0.5rem; }
.failure__card p { color: var(--bo-dim); line-height: 1.6; }
.failure__card form { margin-top: 1.4rem; display: flex; flex-direction: column; gap: 0.8rem; }
.field { display: flex; flex-direction: column; gap: 0.28rem; }
.field label {
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bo-faint);
}
.field input {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius-sm);
  background: rgba(var(--bo-ink-rgb), 0.04);
  font-family: var(--bo-mono);
  font-size: 0.82rem;
}
.field input:focus { outline: none; border-color: var(--bo-accent-edge); }

/* ── Notices ────────────────────────────────────────────── */

.notices {
  position: fixed;
  left: 50%;
  bottom: 5.2rem;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: center;
  max-width: min(520px, calc(100vw - 2rem));
  pointer-events: none;
}
.notice {
  border: 1px solid var(--bo-line-hi);
  border-left-width: 2px;
  border-radius: var(--bo-radius-sm);
  background: var(--bo-toast);
  backdrop-filter: blur(var(--blur));
  padding: 0.6rem 0.9rem;
  font-size: 0.85rem;
  line-height: 1.45;
  text-align: center;
  transition: opacity 0.26s;
  animation: notice-in 0.24s ease-out;
}
.notice--blocked { border-left-color: var(--bo-bad); }
.notice--warn { border-left-color: var(--bo-warn); }
.notice--info { border-left-color: var(--bo-accent-hi); }
@keyframes notice-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ── Saving and sending ─────────────────────────────────── */

/* Save, Send and Configure together used to be three buttons across the foot
   of the panel. They are menu rows now — see `renderMenus` — which is where
   the apps keep them, and which gives the panel its full height back for the
   thing it is actually for. */

.btn--small { padding: 0.28rem 0.55rem; font-size: 0.74rem; }
.btn--danger { border-color: var(--bo-bad-line); }
.btn--danger:hover { background: var(--bo-bad-fill); border-color: rgba(255, 107, 107, 0.7); }
.btn:disabled { opacity: 0.4; cursor: default; }
.btn:disabled:hover { background: rgba(var(--bo-ink-rgb), 0.06); border-color: var(--bo-line-hi); }

/* ── Keyboard focus ─────────────────────────────────────── */

/* Every control on this page is a custom one — a bare button with its border
   taken off, a range with its track redrawn, a checkbox hidden under a track
   of our own — so each was falling back to the browser's default ring. It is
   visible, but it is a different shape and a different colour on each of them,
   and on the swatches it lands as a yellow rectangle in the middle of a purple
   panel. One ring, in the page's own accent, on all of them.

   `:focus-visible` rather than `:focus`, so a mouse click doesn't leave a ring
   behind on a swatch somebody has simply pressed. */
.btn:focus-visible,
.option:focus-visible,
.swatch:focus-visible,
.ui-pill:focus-visible,
.product-card:focus-visible,
.menu__button:focus-visible,
.menu__item:focus-visible,
.appbar__back:focus-visible,
.session-badge__button:focus-visible,
.row-action:focus-visible,
.sheet-action:focus-visible,
.sessions__join:focus-visible {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 2px;
}
/* Rows inside a card have no gap between them, so a ring drawn outside one
   overlaps its neighbours. Inset, it reads as the row it belongs to. */
.menu__item:focus-visible,
.option:focus-visible,
.row-action:focus-visible,
.sheet-action:focus-visible { outline-offset: -2px; }
/* The swatch's own selected state is already an outline on the circle, so the
   focus ring goes round the whole control — circle and name — where it reads
   as a different thing rather than a thicker version of the same one. */
.swatch:focus-visible { border-radius: 8px; }

/* The checkbox is transparent and sits on top of the track it draws, so a ring
   on the input itself would be a rectangle floating over the switch. */
.switch input:focus-visible + .switch__track {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 2px;
}

input[type="range"]:focus-visible {
  outline: 2px solid var(--bo-accent-hi);
  outline-offset: 4px;
}

/* Resuming, on the picker screen.
   Ranged over the cards rather than centred in the viewport: with no heading
   above it this row is the only thing between the appbar and the grid, and a
   lone button in the middle of all that space reads as something that has come
   loose. Sharing the grid's width and sitting at its right edge makes it what
   it is — an action belonging to the list underneath. */
/* Right-aligned on the same measure as the grid under it, so the two buttons
   end where the last card ends rather than floating short of it. */
.picker__saved {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.45rem;
  width: 100%;
  max-width: var(--picker-measure);
  margin: 0 auto;
}
/* The chips naming what you can resume read left-to-right off the label, so
   they take the free space and leave the button on the right. */
.picker__saved .resume { margin-right: auto; }
/* The sessions on the picker: the listed ones anybody may join.
   Above the cards because it is the shorter list and the more perishable one —
   a session is running now, a catalog is always there. */
.sessions {
  width: 100%;
  max-width: var(--picker-measure);
  margin: 0 auto 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.sessions[hidden] { display: none; }
.sessions__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--bo-dim);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.sessions__head .btn { margin-left: auto; letter-spacing: 0.04em; text-transform: none; }
.sessions__list { display: flex; flex-direction: column; gap: 0.45rem; }

.sessions__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.5rem 0.3rem 0.3rem;
  border-radius: 12px;
  border: 1px solid var(--bo-ok-soft);
  background: var(--bo-ok-soft);
  transition: border-color 0.25s, background 0.25s;
}
.sessions__row:hover {
  border-color: var(--bo-ok-line);
  background: var(--bo-ok-soft);
}
/* The row itself is the join. The copy button beside it is the other thing
   you might want from a listed session — its link, which carries no key. */
.sessions__join {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  min-width: 0;
  padding: 0.4rem 0.6rem;
  border: 0;
  border-radius: 9px;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.sessions__live { display: grid; place-items: center; flex: none; color: var(--bo-ok); }
.sessions__text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.sessions__name { font-size: 0.95rem; font-weight: 500; }
.sessions__meta { color: var(--bo-dim); font-size: 0.78rem; }

.resume {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}
.resume[hidden] { display: none; }
.resume__label {
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bo-faint);
}

/* ── Dialogs ────────────────────────────────────────────── */

.dialog {
  width: min(420px, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  /* A modal `<dialog>` centres itself through `margin: auto`, which the page's
     own `* { margin: 0 }` reset takes away. Put back explicitly. */
  margin: auto;
  padding: 0;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: var(--bo-sheet);
  backdrop-filter: blur(18px);
  color: var(--bo-text);
  font-family: inherit;
  overflow: hidden;
}
.dialog--wide { width: min(620px, calc(100vw - 2rem)); }
.dialog::backdrop { background: var(--bo-backdrop); backdrop-filter: blur(3px); }

.dialog__head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 0.95rem;
  border-bottom: 1px solid var(--bo-line);
}
.dialog__title { flex: 1; font-size: 0.98rem; font-weight: 600; }
.dialog__body {
  padding: 0.95rem;
  max-height: calc(100vh - 9rem);
  overflow-y: auto;
}
.dialog__form { display: flex; flex-direction: column; gap: 0.85rem; }
.dialog__note { font-size: 0.76rem; color: var(--bo-dim); line-height: 1.45; }
.dialog__note--bad { color: var(--bo-bad-soft); }
.dialog__actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.95rem;
}
.dialog__actions--split { justify-content: space-between; }

/* ── The list of saved configurations ──────────────────────────── */

.saved-configurations { display: flex; flex-direction: column; gap: 0.4rem; }
.saved-configurations__row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius-sm);
  background: var(--bo-glass-hi);
}
.saved-configurations__main { flex: 1; min-width: 0; }
.saved-configurations__name {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 500;
}
.saved-configurations__meta { font-size: 0.74rem; color: var(--bo-dim); margin-top: 0.15rem; }
.saved-configurations__actions { display: flex; gap: 0.35rem; flex-shrink: 0; }

/* A saved configuration that no longer matches what's published says so here rather than
   being hidden or quietly migrated — see ADR `common/docs/005`. */
.saved-configurations__badge {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.1rem 0.34rem;
  border-radius: 999px;
  border: 1px solid var(--bo-line-hi);
  color: var(--bo-dim);
  white-space: nowrap;
}
.saved-configurations__badge--warn { border-color: var(--bo-warn-line); color: var(--bo-warn); }

/* ── Sheets ─────────────────────────────────────────────── */

/* The vocabulary the two big sheets share — Send, and Configure together.
   Both are grouped lists inside a `<dialog>`, for the same reason the control
   panel is one: a heading, a card of rows, a footnote. Same shape as the
   panel, deliberately, so the page has one idea of what a list of things
   looks like instead of three. */

.session-sheet { display: flex; flex-direction: column; gap: 1.3rem; }

.sheet-group { display: flex; flex-direction: column; gap: 0.42rem; }
.sheet-group__head {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0 0.25rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bo-dim);
}
/* The refresh control sits at the far end of its own heading. */
.sheet-group__head > .btn { margin-left: auto; }
.sheet-group__glyph { display: grid; place-items: center; color: var(--bo-faint); }
/* A live session is the one thing on this page worth a colour of its own:
   green because it is happening, and nothing else on the page is green. */
.sheet-group__live { display: grid; place-items: center; color: var(--bo-ok); }
.sheet-group--invite .sheet-group__card { border-color: var(--bo-ok-soft); }

.sheet-group__card {
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius);
  background: rgba(var(--bo-ink-rgb), 0.045);
  overflow: hidden;
}
.sheet-group__foot {
  font-size: 0.74rem;
  color: var(--bo-faint);
  line-height: 1.45;
  padding: 0 0.35rem;
}

.sheet-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.62rem 0.75rem;
  border-top: 1px solid var(--bo-line);
}
.sheet-row:first-child { border-top: 0; }
.sheet-row--stack { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.sheet-row--switch { cursor: pointer; }
.sheet-row--empty { color: var(--bo-faint); font-size: 0.82rem; line-height: 1.45; display: block; }
.sheet-row__main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.12rem; }
.sheet-row__name { font-size: 0.88rem; }
.sheet-row__meta { font-size: 0.74rem; color: var(--bo-faint); line-height: 1.4; }
.sheet-row__label {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bo-faint);
}
.sheet-row__buttons { display: flex; flex-wrap: wrap; gap: 0.4rem; }

/* A link, in a field rather than as text.
   `navigator.clipboard` needs a secure context and this page is served over
   plain HTTP from a laptop on the Wi-Fi as often as not — so every link is
   also somewhere it can be selected and sent by hand. */
.sheet-link {
  width: 100%;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius-sm);
  background: var(--bo-inset);
  color: var(--bo-text);
  font-family: var(--bo-mono);
  font-size: 0.7rem;
  text-overflow: ellipsis;
}
.sheet-link:focus { outline: none; border-color: var(--bo-accent-edge); }
.sheet-link::placeholder { font-family: 'Inter', system-ui, sans-serif; color: var(--bo-faint); }

/* A row that does something, with a symbol, a word and a line on why. This is
   the row shape both sheets are built out of — see `SendSavedConfigurationSheet` and
   `SessionView`, which are lists of exactly these. */
.sheet-action {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  width: 100%;
  padding: 0.68rem 0.75rem;
  border: 0;
  border-top: 1px solid var(--bo-line);
  background: none;
  color: var(--bo-text);
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}
.sheet-action:first-child { border-top: 0; }
.sheet-action:hover:not(:disabled) { background: rgba(var(--bo-ink-rgb), 0.07); }
.sheet-action:disabled { cursor: default; }
.sheet-action:disabled .sheet-action__label { color: var(--bo-dim); }
.sheet-action__glyph { display: grid; place-items: center; width: 18px; padding-top: 2px; color: var(--bo-accent-hi); }
.sheet-action:disabled .sheet-action__glyph { color: var(--bo-faint); }
.sheet-action__text { display: flex; flex-direction: column; gap: 0.14rem; min-width: 0; }
.sheet-action__label { font-size: 0.88rem; font-weight: 500; }
.sheet-action__hint { font-size: 0.74rem; color: var(--bo-faint); line-height: 1.4; }

/* Something the session has to say for itself — the host left, the two ends
   are on different catalogs, the server refused this one. */
.sheet-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--bo-line);
  border-left-width: 2px;
  border-radius: var(--bo-radius-sm);
  background: rgba(var(--bo-ink-rgb), 0.04);
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--bo-dim);
}
.sheet-banner--bad { border-left-color: var(--bo-bad); }
.sheet-banner--warn { border-left-color: var(--bo-warn); }
.sheet-banner--pending { border-left-color: var(--bo-warn); }
.sheet-banner .ico-svg { margin-top: 2px; }

/* ── Sending ────────────────────────────────────────────── */

.send__hero {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--bo-radius-sm);
  border: 1px solid var(--bo-line);
}

/* ── Live sessions ──────────────────────────────────────── */

/* In the app bar, beside the menu, which is where the apps put it — it is
   status about the whole page, so it belongs with the page's own title rather
   than floating over the middle of the scene.

   It is a control, not a read-out: pressing it opens the sheet where leaving,
   the invite and the roster live. It used to carry its own Leave button, which
   put the one irreversible thing in a session next to a status dot and nowhere
   near the explanation of what leaving does. */
.session-badge { flex: none; }
.session-badge[hidden] { display: none; }

.session-badge__button {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.34rem 0.7rem;
  border: 1px solid var(--bo-ok-line);
  border-radius: var(--radius-pill);
  background: var(--bo-ok-soft);
  color: var(--bo-text);
  font-size: 0.76rem;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
}
.session-badge__button:hover {
  background: var(--bo-ok-soft);
  border-color: var(--bo-ok-line);
}
.session-badge__label { max-width: 16ch; overflow: hidden; text-overflow: ellipsis; }

.session-badge__dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--bo-ok);
  box-shadow: 0 0 0 3px var(--bo-ok-soft);
}
/* Connecting pulses; a refusal or a catalog mismatch stops pretending. */
.session-badge[data-status="connecting"] .session-badge__button {
  border-color: var(--bo-warn-line);
  background: var(--bo-warn-soft);
}
.session-badge[data-status="connecting"] .session-badge__dot {
  background: var(--bo-warn);
  box-shadow: 0 0 0 3px var(--bo-warn-soft);
  animation: session-pulse 1.4s ease-in-out infinite;
}
.session-badge[data-status="mismatch"] .session-badge__button,
.session-badge[data-status="refused"] .session-badge__button,
.session-badge[data-status="ended"] .session-badge__button {
  border-color: var(--bo-bad-line);
  background: var(--bo-bad-fill);
}
.session-badge[data-status="mismatch"] .session-badge__dot,
.session-badge[data-status="refused"] .session-badge__dot,
.session-badge[data-status="ended"] .session-badge__dot {
  background: var(--bo-bad);
  box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.18);
  animation: none;
}

@keyframes session-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* In a shared session: the controls still work — a selection is a request and
   the server answers it — but they should look like they belong to a build
   everybody is in, rather than to this browser alone. Applied to every
   participant, because there is no host any more and none of them owns it. */
body.is-following .panel__body { --bo-line-hi: rgba(var(--bo-ink-rgb), 0.22); }
body.is-following .panel::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--bo-radius);
  border: 1px solid var(--bo-ok-line);
  pointer-events: none;
}
/* A viewer, though, is not making requests at all — the server refuses them by
   definition — so the panel says so rather than offering controls that move
   and snap back. The individual controls are disabled in `ui.js`; this is the
   panel admitting it as a whole. */
body.is-viewing .panel__body { opacity: 0.72; }

/* ── Spatial markers ───────────────────────────────────── */

/* The catalog's own `SpatialMarker`s, drawn on the model — `car.html`'s look,
   built from the same field the visionOS apps read. See
   common/js/configurator/markers.js.

   The overlay is matched to the canvas rather than to the window: the scene
   sits under two stacked toolbars, and a full-window layer would put every
   marker that many pixels low. `pointer-events: none` on the layer and `auto`
   on the buttons, so a drag through the gaps still turns the car. */
.markers {
  position: fixed;
  inset: var(--is-chrome-h) 0 0 0;
  z-index: 10;
  pointer-events: none;
}
body.is-picking .markers,
body.is-markers-hidden .markers { display: none; }

/* A circular glyph, glass over whatever it is floating on. No transform in the
   transition: `CSS2DRenderer` writes one every frame, and animating it makes
   every marker lag a frame behind the car it is stuck to. */
.marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--bo-line);
  background: var(--bo-glass);
  color: rgba(var(--bo-ink-rgb), 0.86);
  backdrop-filter: blur(12px);
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
  transition: opacity 0.2s ease, background 0.2s, border-color 0.2s, box-shadow 0.2s, color 0.2s;
}
.marker svg { width: 18px; height: 18px; display: block; }
.marker:hover,
.marker:focus-visible {
  background: var(--bo-sheet);
  border-color: var(--bo-accent-hi);
  color: var(--bo-text);
  box-shadow: 0 0 0 3px var(--bo-accent-soft), 0 2px 12px rgba(0, 0, 0, 0.45);
  outline: none;
}
/* The one whose panel is open. The panel floats beside the car rather than on
   the glyph, so without this it would be a panel from nowhere. */
.marker.is-open {
  background: var(--bo-accent);
  border-color: var(--bo-accent-hi);
  color: var(--bo-text);
}

/* A marker that flips a toggle group instead of opening a panel — a door, the
   lamps, the drive note. It has no panel to tint it, and it is the only thing
   on screen saying so once the Configure panel is collapsed. Lit rather than
   filled, so it doesn't read as "a panel is open here". */
.marker.is-on {
  border-color: var(--bo-accent-hi);
  color: var(--bo-text);
  box-shadow: inset 0 0 0 1px var(--bo-accent-hi), 0 0 14px var(--bo-accent-halo),
    0 2px 12px rgba(0, 0, 0, 0.45);
}

/* On the far side of the car. Faded rather than removed, so it doesn't pop back
   as a new element the instant the car turns another degree — and unclickable,
   which is the part that matters: a button you can't see must not be a button
   you can press. */
.marker.is-behind {
  opacity: 0;
  pointer-events: none;
}

/* The panel a marker opens.

   The element `CSS2DRenderer` drives is this empty point, anchored beside the
   model rather than on the marker; the panel inside it is what has a size, and
   the centring is here. One transform per frame from the renderer and one
   correction from here, and neither has to know about the other. */
.marker-mount {
  width: 0;
  height: 0;
}
.marker-panel {
  position: absolute;
  left: 50%;
  top: 50%;
  /* `--marker-shift-*` is how far `markers.js` has had to nudge the panel to
     keep it inside the scene; zero when the anchor is comfortably on screen. */
  transform: translate(
    calc(-50% + var(--marker-shift-x, 0px)),
    calc(-50% + var(--marker-shift-y, 0px))
  );
  width: min(19rem, calc(100vw - 2rem));
  max-height: 46vh;
  display: flex;
  flex-direction: column;
  pointer-events: auto;
  border: 1px solid var(--bo-accent-hair);
  border-radius: var(--bo-radius);
  background: var(--bo-glass);
  backdrop-filter: blur(16px) saturate(1.25);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(var(--bo-ink-rgb), 0.07);
  /* Opacity only. The renderer owns `transform`, so a keyframe that animated it
     would be overwritten on the next frame — mid-animation, every time. */
  animation: marker-panel-in 0.22s ease both;
}
@keyframes marker-panel-in { from { opacity: 0; } to { opacity: 1; } }

.marker-panel__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.55rem 0.6rem 0.8rem;
  border-bottom: 1px solid var(--bo-line);
}
.marker-panel__title {
  flex: 1;
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--bo-faint);
}
.marker-panel__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--bo-line);
  background: var(--bo-glass-hi);
  color: var(--bo-dim);
  font-size: 0.72rem;
  line-height: 1;
  cursor: pointer;
}
.marker-panel__close:hover { color: var(--bo-text); border-color: var(--bo-accent-hi); }
.marker-panel__summary {
  padding: 0.6rem 0.8rem 0;
  font-size: 0.74rem;
  line-height: 1.45;
  color: var(--bo-dim);
}
.marker-panel__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.75rem 0.8rem 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
/* A group heading inside a panel that already carries the group's name in its
   own header would say the same word twice. */
.marker-panel__body .group__head h3 { display: none; }
.marker-panel__body .group__head { justify-content: flex-start; }
.marker-panel__body .group__head:empty { display: none; }
/* A toggle group is one switch. It doesn't need the card's padding around it. */
.marker-panel__body--tight { padding-top: 0.55rem; }

/* ── The colour picker, the way car.html draws it ────────── */

/* Same control, different room. In the side panel a swatch carries its name
   underneath, because a scrolling list of eight groups gives you no other place
   to put it. A spatial panel is about one thing, so it does what `car.html`
   does: name the chosen colour once, large, at the top — then a row of bare
   circles under it, close together, with the ring doing the work.

   The markup is `ui.js`'s either way. Two swatch controls that happened to agree
   would be one swatch control too many. */
.marker-panel__body .group__chosen {
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  color: var(--bo-text);
}
.marker-panel__body .swatches {
  gap: 0.55rem;
  padding: 0.55rem 0.6rem 0.7rem;
}
/* Circles only. The name is in the heading above, and repeating it eight times
   at 0.66 rem is what makes the side panel's version a grid rather than a row. */
.marker-panel__body .swatch { width: auto; }
.marker-panel__body .swatch__label { display: none; }
.marker-panel__body .swatch__ring {
  width: 38px;
  height: 38px;
  border: 0;
  outline: 0;
  box-shadow:
    inset 0 0 0 1px rgba(var(--bo-ink-rgb), 0.18),
    0 2px 6px rgba(0, 0, 0, 0.35);
}
.marker-panel__body .swatch:hover:not(:disabled) .swatch__ring {
  transform: translateY(-3px) scale(1.06);
  box-shadow:
    inset 0 0 0 1px rgba(var(--bo-ink-rgb), 0.28),
    0 7px 18px rgba(0, 0, 0, 0.45);
}
/* The chosen one: a gap ring in the panel's own colour, then a glowing accent
   ring outside it. Drawn with shadows rather than an outline so the gap can be
   the panel's dark glass instead of a hole. */
.marker-panel__body .swatch[aria-current="true"] .swatch__ring {
  transform: translateY(-1px);
  box-shadow:
    inset 0 0 0 1px rgba(var(--bo-ink-rgb), 0.22),
    0 0 0 2px #0a0e1c,
    0 0 0 4px var(--bo-accent-hi),
    0 4px 16px var(--bo-accent-halo);
}
.marker-panel__body .swatch:disabled .swatch__ring { opacity: 0.3; }

/* An `info` marker's content: text, a picture, a clip. */
.marker-info__text {
  font-size: 0.82rem;
  line-height: 1.5;
  color: rgba(var(--bo-ink-rgb), 0.86);
}
.marker-info__figure {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.marker-info__figure img {
  width: 100%;
  border-radius: var(--bo-radius-sm);
  display: block;
}
.marker-info__figure audio { width: 100%; }
.marker-info__figure figcaption {
  font-size: 0.7rem;
  color: var(--bo-faint);
}
.marker-info__missing {
  font-size: 0.74rem;
  color: var(--bo-faint);
}

/* ── The gear panel ────────────────────────────────────── */

/* Rows in a `.menu__sheet`, so it is the same sheet as every other control in
   the app bar — see common/js/configurator/scene-settings.js. */
.scene { padding: 0.45rem 0.2rem; }
.scene__row {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.5rem 0.7rem;
  cursor: pointer;
}
.scene__row--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
  cursor: default;
}
.scene__text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1; }
.scene__label { font-size: 0.86rem; font-weight: 500; }
.scene__hint { font-size: 0.72rem; color: var(--bo-faint); line-height: 1.35; }

/* The weather rows, on the page with a sea in it — see the sun and sea state
   sliders in common/js/configurator/scene-settings.js. The value sits on the
   label's own line rather than under it, because it is the label's other half:
   "Sun" on its own names a control and "Sun 26°" names a setting. */
.scene__head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
.scene__value { font-size: 0.74rem; color: var(--bo-dim); font-variant-numeric: tabular-nums; }

.scene__slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 1.1rem;
  margin: 0;
  background: none;
  cursor: pointer;
}
/* Track and thumb have to be declared twice: the two pseudo-elements are
   vendor-specific and a selector list containing an unknown one is dropped
   whole, so sharing a rule between them loses both. */
.scene__slider::-webkit-slider-runnable-track {
  height: 0.25rem;
  border-radius: 999px;
  background: var(--bo-line-hi);
}
.scene__slider::-moz-range-track {
  height: 0.25rem;
  border-radius: 999px;
  background: var(--bo-line-hi);
}
.scene__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 0.85rem;
  height: 0.85rem;
  margin-top: -0.3rem;
  border: none;
  border-radius: 50%;
  background: var(--bo-accent-hi);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.scene__slider::-moz-range-thumb {
  width: 0.85rem;
  height: 0.85rem;
  border: none;
  border-radius: 50%;
  background: var(--bo-accent-hi);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.scene__slider:focus-visible { outline: 2px solid var(--bo-accent-hi); outline-offset: 3px; }

/* The environment picker: one control, three states, and only one of them on.
   A row of buttons rather than a submenu, because the whole value of it is
   flicking between the three and watching the paint change. */
.seg { display: flex; gap: 0.3rem; }
.seg__option {
  flex: 1;
  padding: 0.4rem 0.25rem;
  font-size: 0.72rem;
  font-weight: 500;
  border: 1px solid var(--bo-line);
  border-radius: var(--bo-radius-sm);
  background: var(--bo-glass-hi);
  color: var(--bo-dim);
  cursor: pointer;
  transition: color 0.18s, background 0.18s, border-color 0.18s;
}
.seg__option:hover { color: var(--bo-text); border-color: var(--bo-line-hi); }
.seg__option[aria-checked="true"] {
  color: var(--bo-text);
  background: var(--bo-accent);
  border-color: var(--bo-accent);
}

/* ── The printed sheet ──────────────────────────────────── */

/* A second rendering of the two read-outs the panel already shows, laid out
   for paper. `window.print()` makes no file — see ADR `common/docs/005` — so this is what
   the browser's own "Save as PDF" saves.

   The sheet's own styling lives out here rather than inside `@media print`,
   and only its *visibility* is print-gated. Two reasons: the rules apply to
   nothing else on the page, and a layout that can only exist inside a print
   dialog is a layout nobody can look at while building it — add
   `.is-proofing-sheet` to <body> to see it on screen. */

#spec-sheet {
  display: none;
  background: #fff;
  color: #000;
  font-size: 10.5pt;
  line-height: 1.5;
}

.sheet__head { border-bottom: 1.5pt solid #111; padding-bottom: 0.5rem; }
.sheet__eyebrow {
  font-size: 8pt;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #555;
}
.sheet__title { font-size: 22pt; font-weight: 600; margin-top: 0.15rem; line-height: 1.15; }
.sheet__summary { color: #444; margin-top: 0.2rem; }

.sheet__hero {
  display: block;
  width: 100%;
  margin: 0.8rem 0;
  border: 0.5pt solid #bbb;
  break-inside: avoid;
}

.sheet__columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.4rem;
  align-items: start;
}
/* One column when there's no quote, so a marine catalog doesn't print a
   specification down half the page with white space beside it. */
.sheet__columns:has(> .sheet__block:only-child) { grid-template-columns: 1fr; }

.sheet__block { break-inside: avoid; }
.sheet__block h2 {
  font-size: 9pt;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #555;
  border-bottom: 0.5pt solid #ccc;
  padding-bottom: 0.2rem;
  margin-bottom: 0.35rem;
}

.sheet__list {
  display: grid;
  grid-template-columns: auto 1fr;
  row-gap: 0.16rem;
}
.sheet__list dt { color: #555; }
/* The gap between label and figure is padding rather than `column-gap`, so the
   rule above the total is one line across the block instead of two with a
   break in the middle — `border-top` on two grid items can't cross a gap. */
.sheet__list dd { text-align: right; padding-left: 0.8rem; }
.sheet__list .sheet__total {
  font-weight: 600;
  border-top: 0.5pt solid #999;
  padding-top: 0.22rem;
  margin-top: 0.22rem;
}

.sheet__note { font-size: 8.5pt; color: #666; margin-top: 0.4rem; }
.sheet__foot {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.4rem;
  padding-top: 0.4rem;
  border-top: 0.5pt solid #ccc;
  font-size: 8.5pt;
  color: #666;
}

/* Proofing the sheet on screen, which is also how it was verified. */
body.is-proofing-sheet { overflow: auto; background: #7a7a82; }
body.is-proofing-sheet > *:not(#spec-sheet) { display: none !important; }
body.is-proofing-sheet #spec-sheet {
  display: block;
  width: 210mm;
  min-height: 297mm;
  margin: 2rem auto;
  padding: 14mm;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}

@media print {
  /* Everything the screen needs and paper doesn't, including the canvas — the
     scene reaches paper as the captured hero image instead. */
  body > *:not(#spec-sheet) { display: none !important; }
  body { overflow: visible; background: #fff; color: #000; }
  #spec-sheet { display: block; }
  @page { margin: 14mm; }
}

/* ── Narrow screens ─────────────────────────────────────── */

@media (max-width: 760px) {
  /* The bar keeps the title and the menu; the subtitle and the back button's
     word are what a phone gives up. */
  :root { --is-appbar-h: 48px; }
  .appbar { gap: 0.4rem; }
  .appbar__subtitle { display: none; }
  .session-badge__label { max-width: 8ch; }

  .panel {
    top: auto;
    left: 0.6rem;
    right: 0.6rem;
    bottom: 0.6rem;
    width: auto;
    max-height: 52vh;
  }
  body.is-panel-collapsed .panel { transform: translateY(calc(100% + 1rem)); }
  /* Bottom-left on a phone, where the panel slides down rather than across —
     and the same side the panel itself is on at every wider size. The pill
     keeps the right, so the two halves of the screen mean the same thing on a
     phone as on a laptop.
     `right: auto` is not decoration: a button given both insets stretches the
     width of the screen. */
  .panel-reopen { top: auto; bottom: 0.8rem; left: 0.8rem; right: auto; }

  /* ── The voice dock ──
     `common/js/configurator/ai.js` injects its stylesheet at run time and it
     lands after this file, so `body` in front of the selector is what takes
     the geometry back — the same trick, for the same reason, as the colours in
     `marine.css`. Only where the dock sits is restated; its shape, its dot and
     the pulse are the studio's and stay.

     It keeps the bottom-right corner it has at every size. What it used to do
     here was move to the left to get out of Configure's way, and Configure is
     on the other side now — but a conversation puts up to three more buttons
     beside the pill, and on a 360px screen that row and the Configure button
     cannot share a baseline. So the dock steps *up* instead: above the strip
     when the sheet is down, above the sheet when it is up.
     The dock rather than the pill, because the pill stopped carrying its own
     position when the pause button joined it — see `createDock`. */
  body .convai-dock { right: 0.8rem; bottom: 0.8rem; }
  body.is-talking .convai-dock { bottom: 3.6rem; }
  /* While the panel is up, that strip is the panel: a pill floating over a
     sheet that scrolls covers whichever row happens to be under it. It steps
     above the sheet rather than being hidden, because the × on it is the only
     way to end a conversation and the state colour beside it is the only thing
     saying one is live. `52vh` is the sheet's own height and `0.6rem` its
     inset, both from the `.panel` rule above.
     `is-picking` because the catalog screen hides the panel rather than
     collapsing it — there is no sheet to clear there. */
  body:not(.is-panel-collapsed):not(.is-picking) .convai-dock {
    bottom: calc(52vh + 1.1rem);
  }
  /* The transcript hangs off the dock, so it crosses with it. Above the sheet
     there is no room for a panel of it; it comes back with the strip. */
  body .convai-log { right: 0.8rem; left: auto; bottom: 4.4rem; }
  body.is-talking .convai-log { bottom: 7.2rem; }
  body:not(.is-panel-collapsed):not(.is-picking) .convai-log { display: none; }
  /* The pills move under the bar, and stop wrapping: three rows of them is
     what pushed them into the scene. One scrolling row instead. */
  .viewpoints {
    bottom: auto;
    top: calc(var(--is-chrome-h) + 0.55rem);
    left: 0.6rem;
    right: 0.6rem;
    transform: none;
    max-width: none;
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .viewpoints::-webkit-scrollbar { display: none; }
  .viewpoints .ui-pill { flex: none; }
  .notices { bottom: 56vh; }
  /* A menu sheet the width of its button would be unreadable; on a phone it
     spans most of the bar instead. */
  .menu__sheet { width: min(19rem, calc(100vw - 1.4rem)); }
  .saved-configurations__row { flex-wrap: wrap; }
  .saved-configurations__actions { width: 100%; justify-content: flex-end; }
  .sheet-row__buttons .btn { flex: 1; justify-content: center; }
}
