/* The page's shell and the look of its controls (src/LAYOUT.md).
   Sizes are the old page's (briefs/2-shell-look.md, sections 2 and 3), which
   recorded no reasoning for most of them; colours come from look/tokens.css
   and nowhere else; alpha is a token mixed with `transparent`.

   Nothing here reads state. The controls are built, as views over the store,
   in the steps after health (the status line) and after the store and the
   settings (everything else). Their look is written first so that they can
   only be built the accessible way: a switch is a button with a role, a field
   is styled for being a field, not for its id.

   Sans for prose, mono for anything data-shaped (DESIGN_NOTES section 5). */

:root {
  --mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* ---- the panel ---------------------------------------------------------- */

.panel {
  position: absolute; top: 14px; left: 14px; z-index: 1000;
  box-sizing: border-box; width: 284px; max-width: calc(100vw - 28px);
  max-height: calc(100vh - 28px);     /* for a browser without dvh, which would drop the next line */
  max-height: calc(100dvh - 28px); overflow-y: auto;
  background: color-mix(in srgb, var(--surface) 93%, transparent);
  border: 1px solid var(--hairline); border-radius: 10px;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--shadow) 45%, transparent);
  /* Its own size and line height, so it looks the same on any page that
     hosts it: look.html's body text must not reach into the specimen. */
  color: var(--text); font: 400 12.5px/normal var(--sans);
}
/* The old panel also blurred what was behind it. DESIGN_NOTES section 10
   lists backdrop-filter among the per-frame costs on the Quest, and the
   surface is 93% opaque already. look.html shows the two side by side. */
.panel--blur { backdrop-filter: blur(6px); }

.panel-header { padding: 12px 14px 10px; }
.panel-title {
  display: flex; align-items: center; gap: 8px;
  font: 400 12.5px var(--mono); letter-spacing: .06em;
}
.panel-sub {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 6px;
  margin-top: 5px; font: 400 10px var(--mono); color: var(--text-dim);
}
.panel-sub-right { display: flex; align-items: center; gap: 6px; grid-row: 1; grid-column: 2; }

/* The restart countdown (step 7d-2; issue 071): a line of its own under the
   status, the header's dim mono, amber (repair) for the last half hour and
   never blinking (DESIGN_NOTES). The header is a grid, so the line spans
   both columns and widens nothing; empty, it takes no room. */
.restart-text { grid-row: 2; grid-column: 1 / -1; margin-top: 4px; }
.restart-text:empty { display: none; }
.restart-text[data-soon="true"] { color: var(--repair); }

/* The status line. Its place is reserved: a collapsed panel is as wide as its
   widest line, and a width that follows the text makes the box breathe with
   every poll (issue 048). 20ch is the longest text of an ordinary state,
   "no new data for 120s"; "exporter · 100s ago" is 19. Errors may still
   widen it: they are rare, and should be read whole. */
.status-text { min-width: 20ch; }

/* The dot: steady when all is well. It pulses only while something is loading
   or failing -- a blinking mark is a promise that something needs attention
   (DESIGN_NOTES section 8); the old one pulsed when live too. */
.status-dot {
  flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: var(--repair); box-shadow: 0 0 6px var(--repair);
  animation: status-pulse 1.6s ease-in-out infinite;
}
/* Waiting on the game server: loading, or its data has stopped moving. Spelled
   out, though it is the look above, so that neither depends on a missing
   attribute; the bare dot is only the moment before the first script runs. */
.status-dot[data-state="waiting"] { background: var(--repair); box-shadow: 0 0 6px var(--repair); }
.status-dot[data-state="live"] { background: var(--live); box-shadow: 0 0 6px var(--live); animation: none; }
.status-dot[data-state="paused"] { background: var(--text-dim); box-shadow: none; animation: none; }
.status-dot[data-state="failing"] { background: var(--red); box-shadow: 0 0 6px var(--red); }
@keyframes status-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

.version {
  padding: 1px 5px; border: 1px solid var(--hairline); border-radius: 4px;
  font: 400 9px var(--mono); color: var(--text-faint);
}
.version:empty { display: none; }
/* The tag is the help card's button (issue 128; ib-2): its look stays the pill above, and its tap reaches 6 px above
   and below it, about 24 px tall, without growing the header (it was a 14 px target 6 px from collapse: the step's UX
   review). "· new" in the bright ink while this browser hasn't opened the card's news: text, not a dot, since the
   status dot beside the title means live, waiting or broken (DESIGN_NOTES section 8). */
.version { position: relative; background: none; cursor: pointer; }
.version::before { content: ''; position: absolute; inset: -6px 0; }
.version:hover { color: var(--text-dim); border-color: var(--hairline-strong); }
/* The leading space kept where the tag is a flex box (VR mode), which would drop it as a line's first space (the
   step's review, 5). */
.version-new { color: var(--text-bright); white-space: pre; }
.version-new[hidden] { display: none; }
/* The tag's text is boot's: before it runs, or where a file failed to load, no empty pill (the step's review, 6). */
.version:has(> #versionText:empty) { display: none; }

/* The help card (ib-2; ui/help.js): part of the panel, under its header, so the status dot and the tag that closes it
   still show (the step's review, 1); while it is open it takes the body's place, and the panel is its full width even
   when collapsed. Fades in as a map card does, none under reduced motion, and goes at once. */
.help {
  padding: 10px 14px 12px; border-top: 1px solid var(--hairline);
  font: 400 12.5px/1.45 var(--sans);
  animation: help-in .12s ease-out;
}
.help[hidden] { display: none; }
.panel:has(> .help:not([hidden])) { width: 284px; }
.panel:has(> .help:not([hidden])) > #panelBody { display: none; }
@keyframes help-in { from { opacity: 0; } to { opacity: 1; } }
.help-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.help-head .section-label { margin-bottom: 0; }
.help-x {
  min-width: 32px; min-height: 32px; margin-right: -8px; border: 0; background: none; color: var(--text-dim);
  font: 400 15px/1 var(--mono); cursor: pointer;
}
.help-x:hover { color: var(--text-bright); }
/* The card's bullets (step 8a-5; the user asked for a style upgrade on them): no browser bullet, a small dim dash
   hung in the indent, so a wrapped line lines up under its text and not under the dash (DESIGN_NOTES section 2.18). */
.help-list { margin: 4px 0 0; padding: 0; list-style: none; }
.help-item { position: relative; margin: 6px 0; padding-left: 14px; }
.help-item::before { content: '–'; position: absolute; left: 0; color: var(--text-dim); }
.help-news { margin-top: 12px; }
.help-news[hidden] { display: none; }

/* Collapse (the controls step, 2i): the header stays and the body goes, so
   the panel is as wide as its widest line -- the reserved status width
   above is what keeps it from breathing. The button is a .btn at the old
   collapse button's size (mono 10 px, 1px 6px); both classes, so that this
   rule outranks .btn's, which comes later at the same specificity. */
.btn.panel-toggle { padding: 1px 6px; font-size: 10px; }
.panel--collapsed { width: auto; }
/* The toggle's word in the phone look (issue 093), where the toggle reaches over the bar; not shown elsewhere. */
.panel-toggle-word { display: none; }

/* ---- sections, rows, notes --------------------------------------------- */

.section { padding: 10px 14px; border-top: 1px solid var(--hairline); }
.section-label {
  margin-bottom: 8px; font: 400 9.5px var(--mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-dim);
}
.row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 5px 0; font-size: 12.5px;
}
.row-first { padding-top: 8px; }
.row-side { display: flex; align-items: center; gap: 8px; }
.row-name { color: var(--text); }
.row-count {
  padding: 1px 6px; border-radius: 8px; font: 400 10.5px var(--mono); color: var(--text-dim);
  background: color-mix(in srgb, var(--white) 5%, transparent);
}
/* The Layers section's rows (step 3e): each row's hue, from the kind's
   token, colours its swatch and nothing else -- the switches and the lit
   label toggle take the panel's red, as every control does (the user,
   2026-09-25). The swatch as the old one: a 10 px square, or a dashed ring
   (spotted), or an outline (the boundary). A row's own settings sit in a
   block under it, indented behind a thin line down from the row, so two or
   more still read as that row's. */
.hue-red { --hue: var(--red); }
.hue-repair { --hue: var(--repair); }
.hue-facility { --hue: var(--facility); }
.hue-blue { --hue: var(--blue); }
.hue-neutral { --hue: var(--neutral); }
.hue-note { --hue: var(--note); }
.hue-csar { --hue: var(--csar); }
.hue-airf { --hue: var(--airf); }
.hue-boundary { --hue: var(--boundary); }
.hue-point { --hue: var(--point); }
.hue-shared { --hue: var(--shared); }
.hue-ruler { --hue: var(--ruler); }
.hue-zone { --hue: var(--zone); }
.swatch { flex: none; box-sizing: border-box; width: 10px; height: 10px; border-radius: 3px; }
.swatch--fill { background: var(--hue); }
.swatch--dashed { border: 1.5px dashed var(--hue); border-radius: 50%; }
.swatch--outline { border: 1.5px solid var(--hue); }
.row-subs { margin: 0 0 4px 4px; padding-left: 10px; border-left: 1px solid var(--hairline-strong); }
/* A row's note (issue 173: the shared points' while their layer isn't live): a dim line under it, as a control's note. */
.row-note { margin: 0 0 4px 14px; color: var(--text-dim); font-size: 9.5px; }
.row-note[hidden] { display: none; }
/* The Layers section's second heading, "Tools" (issue 177): a section label among the rows, spaced from the layers
   above it as a section's own label is from the section before. */
.layer-group-label { margin-top: 14px; }
.row--sub { padding: 3px 0; font-size: 11px; }
.row--sub .row-name { color: var(--text-dim); }
.labels-row { align-items: center; margin-top: 6px; }
.labels-word { font: 400 9.5px var(--mono); color: var(--text-dim); }
/* A row's label switch: "Aa", lit in the row's hue when on. */
.label-toggle {
  flex: none; padding: 1px 5px; border: 1px solid var(--hairline); border-radius: 4px;
  background: none; color: var(--text-faint); font: 600 9.5px var(--mono); cursor: pointer;
}
.label-toggle:hover { color: var(--text-dim); border-color: var(--hairline-strong); }
.label-toggle[aria-pressed="true"] { color: var(--text-bright); border-color: var(--red); }   /* no fill: lighter (the user) */
.field-label { margin: 0 0 3px; font: 400 9px var(--mono); letter-spacing: .06em; color: var(--text-dim); }
.note { margin-top: 6px; font: 400 9.5px/1.5 var(--mono); color: var(--text-dim); }
/* A note whose parts are lines: the text's newlines show (the exporter's
   note, the controls step 2i: the user asked for one part per line). */
.note--lines { white-space: pre-line; }
.btn-row { display: flex; flex-wrap: wrap; gap: 6px; }
/* The clear row (issue 116's step (a1); the user, the trial's round 3, 8): "clear" a label on its own line, its
   buttons a grid three to a line of equal widths, a long word wrapping inside its button, as a new kind joins. Armed,
   a button asks "clear 12?", one line (ui/panel/clear.js), so the row never grows at a press. */
.clear-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.clear-row .labels-word { grid-column: 1 / -1; text-transform: uppercase; letter-spacing: .08em; }
.clear-row .btn { min-width: 0; white-space: normal; line-height: 1.2; text-align: center; }
.field-gap { margin-top: 8px; }     /* a field or a row of buttons after another control: the old 8 px */

/* Advanced (the controls step, 2i): a native <details>, its summary the
   section label as one row -- the whole row is the tap target, 40 px tall in
   VR mode -- with the caret drawn here in place of the browser's marker. */
/* A disclosure: a native <details> whose label row is the tap target, the
   caret drawn here in the browser's marker's place (DESIGN_NOTES section
   2.14). Advanced (the controls step) and Accessibility (step 8a-2: "a
   dropdown like Advanced", the user). */
.disclosure > summary {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 0; list-style: none; cursor: pointer;
}
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary::after { content: '▸'; font-size: 9px; }
.disclosure[open] > summary { margin-bottom: 8px; }
.disclosure[open] > summary::after { content: '▾'; }
.disclosure > summary:hover { color: var(--text); }
/* Advanced sits inside Data source, not a section of its own (the user, 6g),
   the panel's 8 px between controls above it (the two sections' 21 px read
   as too large: the user, at 6g-1's check). */
.advanced { margin-top: 8px; }

/* Sync (issue 033, step 6; ui/panel/sync.js): above Advanced, inside Data source, the same 8 px above it. The
   browser's [hidden] rule loses to the rows' flex, so it is said here, and above every look's display: VR mode's
   .vr .btn-row and .vr .btn-row .btn come later or weigh more, and showed start and join while synced (the step's
   review, 1), as .phone [hidden] is said for the phone look. */
.sync { margin-top: 8px; }
.sync [hidden] { display: none !important; }
/* The words: a button showing them or their mask, the copy mark beside it, as a point card's value. */
.sync-words { display: flex; align-items: center; gap: 8px; }
.sync-reveal {
  flex: 1; min-width: 0; padding: 3px 0; border: 0; background: none; text-align: left; cursor: pointer;
  font: 400 11.5px var(--mono); letter-spacing: .02em; color: var(--text-bright); overflow-wrap: anywhere;
}
.sync-reveal:hover { color: var(--white); }
.sync-reveal:focus-visible { outline: 2px solid var(--facility); outline-offset: 2px; border-radius: 4px; }
.sync-words .card-copy { flex: none; margin: 0; }
/* The words field, and over it the same text drawn again, transparent, the field's padding and border its own
   padding: a finished word that is no word underlined in place, the rest of a completed word faint after the caret.
   The field's line height set, so the two lines meet. */
.sync-field { position: relative; }
.panel .sync-field input.mono { line-height: 16px; }
.sync-over {
  position: absolute; inset: 0; padding: 8px 10px; overflow: hidden; white-space: pre; pointer-events: none;
  font: 400 11.5px/16px var(--mono); color: transparent;
}
.sync-bad { text-decoration: underline wavy color-mix(in srgb, var(--red) 80%, transparent); text-underline-offset: 3px; }
.sync-ghost { color: var(--text-faint); }

/* ---- controls ------------------------------------------------------------ */

/* A switch is a button that says what it is: role="switch" and aria-checked.
   The old one was a div no keyboard could reach. */
.switch {
  flex: none; position: relative; width: 30px; height: 16px; padding: 0;
  border: 0; border-radius: 9px; background: var(--track-off); cursor: pointer;
  transition: background .15s;
}
.switch::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px;
  border-radius: 50%; background: var(--knob); transition: transform .15s;
}
.switch[aria-checked="true"] { background: var(--red); }
.switch[aria-checked="true"]::after { transform: translateX(14px); }

/* Fields by what they are, not by id: a new one gets the look by existing. */
.panel select, .panel input[type="text"], .panel input[type="search"], .panel input[type="url"] {
  box-sizing: border-box; width: 100%; padding: 7px 9px;
  border: 1px solid var(--hairline); border-radius: 6px;
  background: var(--field); color: var(--text); font: inherit; font-size: 12.5px;
}
.panel input.mono { font: 400 11.5px var(--mono); }
.panel ::placeholder { color: var(--text-faint); }
.panel select:focus, .panel input:focus { outline: none; border-color: var(--red-dim); }

/* ---- search (step 5f-1; ui/search.js) ------------------------------------
   The field with one clear button, ours (the native one hidden, so every
   browser shows the same); under it a scroll box a few rows tall
   (ui/search.js's SEARCH_SHOWN), its height the rows shown times one fixed
   row height, grown and shrunk by a height transition (the user), none
   under reduced motion. A row is the drawer's: a swatch, a name in the
   panel's sans, what matched in mono under it, the matched part marked. */
.search-field { position: relative; }
.panel .search-field input[type="search"] { padding-right: 28px; }
.search-field input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; display: none; }
.search-clear {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%); padding: 2px 6px;
  border: 0; background: none; color: var(--text-dim); font: 400 13px/1 var(--mono); cursor: pointer;
}
.search-clear:hover { color: var(--text-bright); }
.search-clear:focus-visible { outline: 2px solid var(--facility); outline-offset: 0; border-radius: 4px; }
.search-box {
  /* --search-rows: the rows shown, the view's (inline, so it wins); none before it writes. */
  --search-rows: 0; --search-row-h: 36px; height: calc(var(--search-rows) * var(--search-row-h)); margin-top: 4px;
  overflow-y: auto; overscroll-behavior: contain; transition: height .16s ease-out;
}
.search-row {
  display: flex; flex-direction: column; justify-content: center; gap: 2px; box-sizing: border-box;
  width: 100%; height: var(--search-row-h); padding: 0 6px; border: 0; border-radius: 4px;
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.search-row:hover { color: var(--text-bright); background: color-mix(in srgb, var(--white) 5%, transparent); }
.search-row:focus-visible { outline: 2px solid var(--facility); outline-offset: -2px; }
.search-row-line { display: flex; align-items: center; gap: 6px; min-width: 0; }
.search-swatch { flex: none; width: 10px; height: 10px; border-radius: 3px; }
.search-name, .search-sub { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-sub { padding-left: 16px; font: 400 9.5px var(--mono); color: var(--text-dim); }
.search-text { color: inherit; }
.search-mark { border-radius: 2px; background: color-mix(in srgb, var(--facility) 24%, transparent); color: var(--text-bright); }
.search-list { min-width: 0; }
/* Go to's row (5f-2): its reading in mono, in the mark's magenta (the points' family), on one line. */
.search-coord { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 400 11.5px var(--mono); color: var(--point); }
.search-section .note { margin-top: 4px; }

.btn {
  padding: 2px 7px; border: 1px solid var(--hairline); border-radius: 5px;
  background: none; color: var(--text-dim); font: 400 9.5px var(--mono);
  white-space: nowrap; text-decoration: none; cursor: pointer;
}
.btn:hover { color: var(--text); border-color: var(--hairline-strong); }

/* The keyboard gets a ring on a switch, a button and a select; a mouse click
   doesn't. A text field is left with its border colour, as the old page had
   it: browsers count any focused text field as :focus-visible, mouse
   included, so a ring there would show on every click. */
.switch:focus-visible, .btn:focus-visible, .label-toggle:focus-visible, .panel select:focus-visible, .panel summary:focus-visible {
  outline: 2px solid var(--facility); outline-offset: 2px;
}

.switch:disabled, .btn:disabled, .label-toggle:disabled, .panel select:disabled, .panel input:disabled {
  opacity: .45; cursor: default;
}

/* The Intel drawer (step 5e-1; ui/drawer.js; briefs/5-drawer.md section 1):
   over the map's right edge, full height, the old drawer's 390 px (one width,
   --drawer-width, look/tokens.js's SIZES) at most 92 vw, on the one surface
   with no blur (the design's D1). Closed, it sits off the edge with only its
   tab showing; it slides 0.28 s on the old curve, none under reduced motion.
   CLOSED, IT ISN'T DRAWN (the user, 5e-1's check): once the slide out ends
   it turns `visibility: hidden` -- nothing painted, nothing in it focusable
   -- and its tab alone stays visible. Above the panel (1000), as the old
   drawer was (1100). */
/* Closed until the drawer's view says otherwise: the credit stays put before
   any script runs. */
/* --drawer-slide: 1, or 0 for the drawer's first show, so a reload with it
   open starts open rather than sliding in (the user, 5g-1; ui/drawer.js
   sets it back two frames later). */
:root { --drawer-on: 0; --drawer-slide: 1; }
/* The phone look's sizes (issue 093; its block below): --panel-on, 1 or 0, the panel's sheet up (ui/collapse.js
   writes it, as the drawer's view writes --drawer-on); the bar's height and its INTEL end's width; a sheet's height
   -- half the screen upright, the height above the bar sideways -- and, sideways, its width: the drawer's, at most
   half the screen. */
:root { --panel-on: 0; --bar-h: 40px; --tab-w: 64px; --sheet-h: 50dvh; --sheet-slide: 0; }
@media (orientation: landscape) { :root { --sheet-h: calc(100dvh - var(--bar-h)); --sheet-w: min(var(--drawer-width), 50vw); } }
.drawer {
  position: absolute; top: 0; right: 0; z-index: 1100; height: 100%;
  width: var(--drawer-width); max-width: 92vw; box-sizing: border-box;
  display: flex; flex-direction: column;
  background: var(--surface); border-left: 1px solid var(--hairline);
  /* Its own ink and type, as the panel's (the user, 5e-2's check): what sets neither inherited the page's
     defaults, black serif on the surface. */
  color: var(--text); font: 400 12.5px/normal var(--sans);
  transform: translateX(100%); visibility: hidden;
  transition: transform calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1), visibility 0s linear calc(var(--drawer-slide) * .28s);
}
.drawer.drawer--open { transform: translateX(0); visibility: visible; transition: transform calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1), visibility 0s; }
/* The tab hangs off the drawer's left edge, so it shows at the screen's edge
   while the drawer is closed: "INTEL" read bottom to top (the user: no
   arrow). `sideways-lr` turns the text alone; the old page turned the whole
   tab, which put its open side and its centring the wrong way round (the
   user, 5e-1's check). Visible while the drawer isn't. */
.drawer-tab {
  position: absolute; top: 50%; left: -30px; width: 30px; padding: 16px 0; transform: translateY(-50%);
  border: 1px solid var(--hairline); border-right: 0; border-radius: 6px 0 0 6px; background: var(--surface);
  color: var(--text-dim); font: 400 10px var(--mono); letter-spacing: .08em; cursor: pointer;
  writing-mode: sideways-lr; visibility: visible;
}
.drawer-tab:hover { color: var(--text-bright); border-color: var(--hairline-strong); }
.drawer-tab:focus-visible { outline: 2px solid var(--facility); outline-offset: 2px; }
.drawer-head { padding: 14px 16px 6px; }
/* As the panel's title (the design review). */
.drawer-title { margin: 0; font: 400 12.5px var(--mono); letter-spacing: .06em; }
.drawer-body { flex: 1; overflow-y: auto; padding: 4px 16px 24px; contain: content; }
/* Each row lays itself out on its own, and one out of the body's view not at all (5g-perf-b: after every 2 Hz
   batch the aircraft's rows' text changed and the browser laid out some 440 objects, 6-8 ms on the Quest, in the
   frame after). `auto` keeps a row's last size while it is skipped, so the scroll height holds; a row never shown
   counts as one line's height. Its focus ring inside its box: the containment clips what paints outside it. */
.drawer-row, .drawer-bar { content-visibility: auto; contain-intrinsic-size: auto 24px; }
/* The drawer's sections (step 5e-2; ui/drawer.js, kinds/drawer.js): the old
   drawer's look in the page's tokens (brief 5-drawer section 1), headers in
   the panel's style (the user), one row style for every list where the old
   page had two (boxed facility cards, plain crew rows), and the bars with no
   transition (the design's F: the old one never played). */
/* The browser's [hidden] rule loses to any author display (.drawer-tiles' grid, .drawer-value-sub's flex): the
   view hides with `hidden`, so the drawer says it again (5e-2's review, 4). */
.drawer [hidden] { display: none; }
/* The same for the Layers rows (.row's flex, VR mode's .row-subs flex): a row the kinds that show don't name is hidden
   with `hidden` (7a-t; its review, 2). */
.layer-rows [hidden] { display: none; }
/* And any panel row the page or a view hides with `hidden` (7c-2's review, 1): public's Blob mode row, which the markup
   holds hidden and only master's boot shows -- .row's flex would show it, inert, on public. */
.row[hidden] { display: none; }
.drawer-section { padding: 14px 0; border-bottom: 1px solid var(--hairline); }
.drawer-section:last-child { border-bottom: 0; }
.drawer-section-title, .drawer-heading {
  margin-bottom: 10px; font: 400 9.5px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim);
}
/* A heading inside a section (the red types' list): a second list starts, as the old page's title did. */
.drawer-heading { margin: 14px 0 8px; }
.drawer-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 7px; }
.drawer-tile {
  display: flex; flex-direction: column; gap: 3px; min-width: 0; padding: 8px 7px;
  border: 1px solid var(--hairline); border-radius: 7px; background: color-mix(in srgb, var(--white) 3.5%, transparent);
}
.drawer-tile-value { font: 400 17px/1.1 var(--mono); }
/* 9 px, the scale's floor (DESIGN_NOTES section 5; the design review). */
.drawer-tile-key { font: 400 9px var(--mono); letter-spacing: .03em; color: var(--text-dim); white-space: nowrap; }
/* A row: the panel's row height, its 5 px padding (the user, 5e-2's check: 8 px made the drawer's rows a quarter
   taller than the panel's; larger targets for the Quest are the VR milestone's, for both alike), its value level
   with its name (baseline), its hover a fill the ray can see -- the ink alone moved too little (the design
   review). The fill reaches 6 px past the text either side. */
.drawer-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px; box-sizing: border-box;
  width: calc(100% + 12px); margin: 0 -6px; padding: 5px 6px; border: 0; border-radius: 4px;
  border-bottom: 1px dashed color-mix(in srgb, var(--white) 6%, transparent); background: none;
  color: inherit; font: inherit; text-align: left;
}
button.drawer-row { cursor: pointer; }
button.drawer-row:hover { color: var(--text-bright); background: color-mix(in srgb, var(--white) 5%, transparent); }
button.drawer-row:focus-visible { outline: 2px solid var(--facility); outline-offset: -2px; }
/* A kind switched off: its parts fade, and the words that say why stay readable (the design review). Opacity,
   because the parts' inks are written inline. */
.drawer-row--hidden .drawer-row-line, .drawer-row--hidden .drawer-sub, .drawer-row--hidden .drawer-row-side { opacity: .45; }
.drawer-row-main { display: flex; flex-direction: column; min-width: 0; }
.drawer-row-line { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.drawer-swatch { flex: none; align-self: center; width: 10px; height: 10px; border-radius: 3px; }
/* A long name: the airframe beside it gives way first, then the name (the design review: a twin's "#2" is
   at the name's end); the whole name is its title. */
.drawer-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drawer-aside { flex-shrink: 10; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-faint); }
.drawer-count { flex: none; font: 400 11px var(--mono); color: var(--text-dim); white-space: nowrap; }
/* Keys are data (DESIGN_NOTES section 5): the unrecognised keys in mono, as the old page printed them. */
.drawer-section--unknown .drawer-name { font: 400 11.5px var(--mono); }
/* The sub line keeps its spaces: the aircraft's data line is fixed slots, padded so that a change in one never
   moves another (DESIGN_NOTES section 4; the design review). */
.drawer-sub { white-space: pre-wrap; }
.drawer-sub, .drawer-off { margin-top: 2px; font: 400 9px var(--mono); color: var(--text-dim); }
.drawer-row-side { display: flex; flex-direction: column; align-items: flex-end; flex: none; }
.drawer-value { font: 400 11.5px var(--mono); white-space: nowrap; }
.drawer-value-sub { display: flex; align-items: center; gap: 3px; margin-top: 2px; font: 400 9px var(--mono); color: var(--text-dim); }
.drawer-wrench { width: 10px; height: 10px; background: var(--repair); mask-size: contain; mask-repeat: no-repeat; }
.drawer-value-word { white-space: nowrap; }
.drawer-bar { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 3px 8px; margin-bottom: 7px; font-size: 11.5px; }
/* A bar that searches (5f-1): the rows' button look and hover. */
button.drawer-bar {
  box-sizing: border-box; width: calc(100% + 12px); margin: 0 -6px 3px; padding: 3px 6px 4px; border: 0; border-radius: 4px;
  background: none; color: inherit; font: inherit; font-size: 11.5px; text-align: left; cursor: pointer;
}
button.drawer-bar:hover { color: var(--text-bright); background: color-mix(in srgb, var(--white) 5%, transparent); }
button.drawer-bar:focus-visible { outline: 2px solid var(--facility); outline-offset: -2px; }
.drawer-bar-count { font: 400 10.5px var(--mono); color: var(--text-dim); }
.drawer-bar-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drawer-value-line { display: flex; align-items: center; }
.drawer-bar-track { grid-column: 1 / -1; height: 5px; border-radius: 3px; overflow: hidden; background: color-mix(in srgb, var(--white) 8%, transparent); }
.drawer-bar-fill { display: block; height: 100%; border-radius: 3px; }
/* The rows' box holds its notes' margins, so a note first or last doesn't push past the section's padding. */
.drawer-rows { display: flow-root; }
/* A note row, as the panel's notes. */
.drawer-note { margin: 6px 0; font: 400 9.5px/1.5 var(--mono); color: var(--text-dim); }
/* Chromium's scrollbars (the user, 5e-2's check): Edge's default is a wide light bar on the dark surface;
   Firefox's own is kept untouched. Thin, in the hairline's ink, its room kept in the drawer so rows don't reflow
   as the list grows past the view. A pull at the list's top never reaches the page (Android's refresh). */
@supports not (-moz-appearance: none) {
  .drawer-body, .panel { scrollbar-width: thin; scrollbar-color: var(--hairline-strong) transparent; }
  .drawer-body { scrollbar-gutter: stable; }
}
.drawer-body { overscroll-behavior: contain; }

/* The cursor readout (5f-3; ui/readout.js): the old 10 px mono on a dark
   translucent box with a 7 px radius, one line, in the bottom-right control
   container above the credit. No pointer events: over it the pointer would
   leave the map and the readout hide -- a flicker. */
.readout {
  padding: 3px 7px; border-radius: 7px; background: color-mix(in srgb, var(--surface) 85%, transparent);
  border: 1px solid var(--hairline); color: var(--text-dim); font: 400 10px/1.4 var(--mono); white-space: pre;
  /* Never wider than the room left of the drawer (the corner's own offset) and its margins: cut with an ellipsis
     rather than run past the map's edge (5f-3's review, 1); boot hides it where the drawer covers most of the map. */
  max-width: calc(100vw - 20px - var(--drawer-on, 0) * min(var(--drawer-width), 92vw, 100vw - 44px));
  overflow: hidden; text-overflow: ellipsis; box-sizing: border-box;
}
/* The control alone (readout.html loads this sheet, and its body was classed `readout` until issue 126).
   Meant as its own layout root (step 7g-perf part 2; issue 124): its text changes every frame while the map is
   dragged, and uncontained each change laid out the whole document (about 0.5 ms a frame on the Quest). Size
   containment wants a size, so the view hands the text's longest line and its lines (ui/readout.js): one cell a glyph
   in this monospace font; the line height 1.4 at 10 px. THE CONTENT BOX is sized, not the border box (7g-perf part
   2's review, 1): a border snaps to a whole device pixel, so under 100% zoom on a 1x screen it grows past 1 CSS px
   while the text shrinks, and a border-box width with the borders counted at 1 px left the text a fraction short --
   its last character cut to an ellipsis. Sized by its content, a snapped border grows the box outward. The cap is
   the plain rule's, less the padding and border each side (16 px), as max-width now caps the content box. */
.maplibregl-ctrl.readout {
  pointer-events: none;
  --readout-cols: 0; --readout-lines: 1;
  contain: strict;
  box-sizing: content-box;
  width: calc(var(--readout-cols) * 1ch);
  height: calc(var(--readout-lines) * 1.4em);
  max-width: calc(100vw - 36px - var(--drawer-on, 0) * min(var(--drawer-width), 92vw, 100vw - 44px));
}
.readout[hidden] { display: none; }

/* The hint (6e-1; ui/hint.js): what a waiting tool wants next and a × that
   cancels it, the readout's box at the map's bottom middle, over the map and
   under the panel and the drawer; the × at least 32 px square to hit on a
   phone and with the ray. No fade; it slides with the drawer as the alert does, and not under reduced motion. */
.hint {
  /* Centred on the map the open drawer leaves uncovered, sliding with it as the alert's band does (ib-1's review, 1:
     the cue's × went under the drawer below about 1,100 px); where the drawer leaves under 248 px -- a phone with it
     open -- the room stops shrinking and the line runs under the drawer, as the alert's band does over it. */
  --hint-drawer: min(calc(var(--drawer-on, 0) * min(var(--drawer-width), 92vw, 100% - 44px)), max(0px, 100% - 248px));
  position: absolute; bottom: 34px; left: calc((100% - var(--hint-drawer)) / 2); transform: translateX(-50%); z-index: 900;
  transition: left calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1);
  display: flex; align-items: center; gap: 4px; padding: 2px 2px 2px 10px; border-radius: 7px;
  background: color-mix(in srgb, var(--surface) 90%, transparent); border: 1px solid var(--hairline);
  color: var(--text-bright); font: 400 11px/1.4 var(--mono); white-space: nowrap;
}
.hint[hidden] { display: none; }
.hint-words { overflow: hidden; text-overflow: ellipsis; }
/* The ring's cue (issue 119; ib-1) wraps, where a tool's words hold one line: about 50 characters, longer than a
   narrow phone's map is wide. Sized to its text up to the uncovered map less 12 px a side, the box's padding and
   border counted (border-box; a centred shrink-to-fit box would otherwise be capped at half its room). */
.hint[data-kind="cue"] { white-space: normal; width: max-content; box-sizing: border-box; max-width: calc(100% - var(--hint-drawer) - 24px); }
.hint-x {
  min-width: 32px; min-height: 32px; border: 0; background: none; color: var(--text-dim);
  font: 400 15px/1 var(--mono); cursor: pointer;
}
.hint-x:hover { color: var(--text-bright); }

/* The tripwire readout (step 6f-2; ui/alert.js): the old #zoneAlert's look
   in the page's tokens, top middle of the uncovered map -- its band's right
   edge the open drawer's, sliding with it as the credit does (the user's
   answer 7). Over the panel and the drawer, as the old one was (z 1100,
   after both: 6f-2's review, 1), taking no pointer, so a press through it
   reaches what is under it. Never narrower than the box and its margins
   (248 px): where the drawer leaves less -- a phone with it open -- the
   band runs over the drawer instead of pushing the box off screen; and
   centred safely, so a box wider than its band starts at its left edge. */
.alert-band {
  position: absolute; top: 14px; left: 0; z-index: 1150; display: flex; justify-content: safe center; pointer-events: none;
  right: min(calc(var(--drawer-on, 0) * min(var(--drawer-width), 92vw, 100% - 44px)), max(0px, 100% - 248px));
  transition: right calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1);
}
.alert {
  min-width: 220px; max-width: calc(100% - 28px); box-sizing: border-box; padding: 6px 10px; border-radius: 8px;
  background: color-mix(in srgb, var(--surface) 94%, transparent); border: 1px solid var(--airr-dim);
  box-shadow: 0 8px 24px color-mix(in srgb, var(--shadow) 45%, transparent);
  color: var(--text); font: 400 11px/1.45 var(--mono);
}
.alert[hidden] { display: none; }
.alert--muted { border-color: var(--hairline); color: var(--text-dim); }
.alert-zone + .alert-zone, .alert-groups:not(:empty) + .alert-note { margin-top: 5px; padding-top: 5px; border-top: 1px solid var(--hairline); }
.alert-head { display: flex; justify-content: space-between; gap: 14px; color: var(--zone); font-weight: 600; }
.alert-zone-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alert-rows { display: flex; flex-direction: column; }
.alert-radius { color: var(--text-dim); font-weight: 400; }
.alert-row { display: flex; justify-content: space-between; gap: 14px; padding-left: 8px; white-space: nowrap; }
.alert-name { color: var(--label-red); overflow: hidden; text-overflow: ellipsis; }
.alert-where { flex: none; }
.alert-note { color: var(--text-dim); }
.alert-note[hidden] { display: none; }

/* MapLibre's bottom-right controls (the credit; 5f-3's readout beside it)
   keep left of the open drawer and slide with it (issue 036; the design's
   D5): the drawer's view sets --drawer-on, 1 or 0, and the offset is the
   drawer's real width, capped as the drawer is (5e's direction review, 3),
   and never closer than 44 px to the map's left edge, the room MapLibre's
   folded credit takes (its 24 px button and 10 px margins), so on a narrow
   phone it stays on screen beside the drawer (5e-1's review, 3). */
.maplibregl-ctrl-bottom-right {
  right: calc(var(--drawer-on, 0) * min(var(--drawer-width), 92vw, 100% - 44px));
  transition: right calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1);
}

/* A card (step 5d-1; ui/cards.js): the old .pop-* look (brief
   5-popups-hover A.1) in the page's tokens, over MapLibre's popup, whose
   white surface and tip are replaced. The close control is small and plain
   (the user: most close a card by tapping elsewhere). A row's colour, when
   it has one, is set by the view from a token (ui/patch.js's tone). */
.card-popup .maplibregl-popup-content {
  background: var(--surface); color: var(--text-bright); border: 1px solid var(--hairline); border-radius: 8px;
  padding: 10px 12px; box-shadow: 0 2px 10px color-mix(in srgb, var(--shadow) 50%, transparent);
  font: 400 11.5px/1.55 var(--mono);
}
/* The tip is ours (the user, 5d-1's check: the border has to follow the
   point). MapLibre's tip is one border triangle, which can't carry an
   outline. Ours is two, the bordered caret: the tip element's own, 16 px
   wide and 8 tall in the border's colour, and its ::after, 14 px wide in the
   surface, 1.41 px inside it, so the outline along each slanted edge is
   1 px across like the box's. The tip overlaps the box by its border's 1 px,
   and the surface triangle covers that stretch of the box's border, so the
   outline runs down into the point and back up. Its layout box ends at its
   point, so the Popup's offset still puts the point on the marker's top
   edge.
   NO TRANSFORM (the user, 5d-2's check): an earlier tip, a square turned
   45 deg, was rasterised apart from the box, and under sub-pixel
   positioning it slid smoothly while the box and text stepped on whole
   pixels, so the two parted. Borders paint with the box, as one shape.
   Static CSS: nothing to repaint as a card moves. */
.card-popup .maplibregl-popup-tip { position: relative; width: 0; height: 0; border: 8px solid transparent; }
.card-popup .maplibregl-popup-tip::after { content: ''; position: absolute; left: -7px; width: 0; height: 0; border: 7px solid transparent; }
.card-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip { margin-top: -1px; border-bottom: 0; border-top-color: var(--hairline); }
.card-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip::after { top: -8.41px; border-bottom: 0; border-top-color: var(--surface); }
.card-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip { margin-bottom: -1px; border-top: 0; border-bottom-color: var(--hairline); }
.card-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip::after { top: 1.41px; border-top: 0; border-bottom-color: var(--surface); }
/* FIREFOX SNAPS A CARD'S FRACTIONAL TRANSLATE TO WHOLE PIXELS (the user,
   5d-2's check; Chromium and the Quest don't), so an aircraft's card stepped
   beside its gliding icon. A turn of 0.02 deg stops it (the user, in
   Firefox: a depth offset didn't; 0.1 and 0.15 deg worked too, and 0.02 is
   the user's pick). The individual `rotate` property stacks with the
   transform MapLibre writes inline, so MapLibre is untouched. Firefox
   draws a faint light fringe at the turned card's edge at every angle
   tried, accepted by the user as Firefox's cost. Firefox alone (the feature
   query is true only there); a moving card only, a ground card being
   rounded anyway. */
@supports (-moz-appearance: none) {
  .card-popup.card-moves { rotate: 0.02deg; }
}
/* A card fades in when it opens and out when it closes (the user, at 5d-1's
   check): 120 ms each (ui/cards.js's CARD_FADE_MS, a check holds them
   equal), opacity alone, which MapLibre's transform never touches. One card
   switched for another crossfades. A closing card takes no press. None under
   reduced motion (below), where the view removes a card at once. */
.card-popup { animation: card-in 120ms ease-out; }
.card-popup.card-out { animation: card-out 120ms ease-in forwards; }
.card-popup.card-out .maplibregl-popup-content { pointer-events: none; }
@keyframes card-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes card-out { from { opacity: 1; } to { opacity: 0; } }
.card { position: relative; }
.card-close {
  position: absolute; top: -6px; right: -8px; padding: 0 4px; border: 0; background: none;
  color: var(--text-dim); font: 400 14px/1 var(--mono); cursor: pointer;
}
.card-close:hover { color: var(--text-bright); }
.card-title { margin: 0 14px 5px 0; font-size: 12.5px; font-weight: 600; letter-spacing: .04em; }
.card-kv { display: flex; justify-content: space-between; gap: 12px; color: var(--text-dim); }
.card-v { color: var(--text-bright); text-align: right; }
.card-note { color: var(--text-faint); }
.card-count, .card-name, .card-k, .card-rows { color: inherit; }
.card-line { color: var(--text-bright); }
.card-sep { height: 1px; margin: 6px 0; background: var(--hairline); }
.card-bar { height: 5px; margin-top: 4px; border-radius: 3px; overflow: hidden; background: color-mix(in srgb, var(--white) 8%, transparent); }
.card-fill { display: block; height: 100%; border-radius: 3px; }
/* A point's card (step 6c-3; the old popup's: brief 6-points-rulers section 1). The position's copy button: two sheets,
   a mask in the text's dim ink, its check mark for a moment once copied, in the bright ink (CSAR's hue is not borrowed:
   the user). The sheets are the old page's stroked outlines (COPY_SVG), smoother than 6c-3's filled squares, turned
   180 degrees so the whole sheet sits at the bottom left, beside the text it copies (the user, step 6h-1, from
   artifacts/copy-icon/). */
.card-v-text { color: inherit; }
/* The value and its button on one line (the user, at 6c-3's check: DMS wrapped the button under it). A value too long to
   sit beside its label (DMS was, at 34 characters, past the card's 302 px text column, until issue 111 took its spaces
   out: 27) takes a line of its own under it,
   right-aligned, the card's padding kept (the same check: on one line it ran to the card's edge). The mark the text's
   size, sat on its baseline as the old .pt-copy's (11 px, -1 px). */
.card-kv:has(> .card-v > .card-copy) { flex-wrap: wrap; column-gap: 12px; row-gap: 0; }
.card-v:has(> .card-copy) { margin-left: auto; white-space: nowrap; }
.card-copy {
  display: inline-block; width: 11px; height: 11px; margin: 0 0 0 6px; vertical-align: -1px; padding: 0; border: 0; cursor: pointer;
  background: var(--text-dim);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Crect x='1.2' y='4.2' width='8.6' height='8.6' rx='1.6' fill='none' stroke='black' stroke-width='1.4'/%3E%3Cpath d='M4.2 1.2H11.4a1.4 1.4 0 0 1 1.4 1.4V9.8' fill='none' stroke='black' stroke-width='1.4'/%3E%3C/svg%3E") center / 11px 11px no-repeat;
}
.card-copy:hover { background: var(--text-bright); }
.card-copy--done {
  background: var(--text-bright);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M1.5 7.2l1.4-1.4 2.6 2.6 5.6-5.6 1.4 1.4-7 7z'/%3E%3C/svg%3E") center / 11px 11px no-repeat;
}
/* The note: a field in the panel's field look, three rows, the mono text of a card. */
.card-field {
  display: block; box-sizing: border-box; width: 100%; margin-top: 2px; padding: 5px 7px; resize: vertical;
  border: 1px solid var(--hairline); border-radius: 5px; background: var(--field); color: var(--text-bright); font: 400 11.5px/1.45 var(--mono);
}
.card-field::placeholder { color: var(--text-faint); }
.card-field:focus { outline: none; border-color: var(--hairline-strong); }
/* An action's button (a point's delete): a .btn at rest, as the old .minibtn pt-del was; its hue only once armed (the
   user, at 6c-3's check, as its hue at rest stuck out). A card's buttons side by side in one row, wrapping where the
   card is narrow (issue 173, the look as chosen: share beside delete; the user, at 6b-3's check). The browser's
   [hidden] rule loses to the display, so it is said here. */
.card-action {
  display: inline-block; vertical-align: top; margin: 8px 6px 0 0; padding: 2px 7px; border: 1px solid var(--hairline); border-radius: 5px;
  background: none; color: var(--text-dim); font: 400 9.5px var(--mono); cursor: pointer;
}
.card-action[hidden] { display: none; }
.card-action:hover { color: var(--text); border-color: var(--hairline-strong); }
.card-action:disabled, .card-action:disabled:hover { color: var(--text-faint); border-color: var(--hairline); cursor: default; }
/* The shared points' card parts (issue 173, the look as chosen). A share's status line keeps its line's height with
   nothing to say, so the buttons under it never move; its tone the shared hue, or red for a refusal, dim otherwise. A
   shared point's note, read only, shown whole as written. A share's lengths, offered in place of its button in the
   card's row of buttons: plain buttons, none marked a default (the status line says what they are for). */
.card-status { min-height: 1.45em; margin-top: 6px; color: var(--text-dim); font: 400 10px/1.45 var(--mono); }
.card-readonly { margin-top: 2px; color: var(--text-bright); font: 400 11.5px/1.45 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
.card-choose { display: inline-block; vertical-align: top; margin: 8px 6px 0 0; }
.card-choose .card-action { margin: 0; }
.card-choices { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.card-choices[hidden] { display: none; }
/* A button armed for its second press (ui/confirm.js): its action's hue (red without one: the clear), filled, the
   question in the bright ink. */
.confirm--armed { color: var(--text-bright); border-color: var(--hue, var(--red)); background: color-mix(in srgb, var(--hue, var(--red)) 22%, transparent); }
.confirm--armed:hover { color: var(--text-bright); border-color: var(--hue, var(--red)); background: color-mix(in srgb, var(--hue, var(--red)) 30%, transparent); }
/* The clipboard's old way (boot's copyText): a field off the eye for one select. */
.copy-scratch { position: fixed; top: 0; left: 0; opacity: 0; pointer-events: none; }

/* The ring (step 6c-2; ui/ring.js; milestone 6's design, E6, as the user's
   pick of the look's trials leaves it, 2026-10-03): one solid disk in a
   Popup with no surface and no tip of MapLibre's. An annulus 160 px across,
   its middle open (the box takes no pointer, the parts do), cut into equal
   parts parted by lines in the border colour, one outline round it. A part
   is its tool's mark, white, over its mono word; hovered or focused, the
   mark takes its tool's hue (`--hue`) and the part an outline in it
   (`.ring-outline`, drawn over everything), no fill. A disabled part is
   faint and answers neither. Unselectable, no callout, no backdrop-filter.
   It opens by rolling out counterclockwise -- a quarter turn back into place
   while a conic mask draws it round from the top part's right-hand edge
   (`--ring-from`, set by ui/ring.js by the count; `--ring-sweep` a
   registered angle, so it animates) -- and closes by fading, as a card. */
@property --ring-sweep { syntax: '<angle>'; inherits: false; initial-value: 360deg; }
/* A tripwire's rename (step 6f-3; ui/rename.js): the field is the tag while
   renaming -- the tag's look (look/rulers.js's RULER_TAG: 10 px mono on 14.5,
   2 by 6 padding, a 4 px corner, the backdrop at .82, a 1 px edge), the
   zone's dim edge; the name in --zone semibold, the radius in --label-zone
   (look/zones.js's ZONE_TAG). A field that reads as the tag's text (the old
   .zone-name): no box of its own, as wide as its text in characters. */
.rename-popup .maplibregl-popup-content { background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0; }
.rename-popup .maplibregl-popup-tip { display: none; }
.rename-tag {
  display: flex; align-items: baseline; padding: 1px 5px; border-radius: 4px; white-space: nowrap;
  background: color-mix(in srgb, var(--backdrop) 82%, transparent); border: 1px solid var(--zone-dim);
  font: 400 10px/14.5px var(--mono); color: var(--label-zone);
  /* As the frame tag draws it (the user, 6f-3's check: "a little different"): its edge lies inside its box, so the
     padding is the frame's less the 1 px border; glyph by glyph, so no ligatures; and each run keeps its leading
     space (" · 5.0 NM"), which a flex item would otherwise drop. */
  font-variant-ligatures: none; font-feature-settings: "calt" 0, "liga" 0;
}
.rename-tag > span { white-space: pre; }
.rename-field {
  box-sizing: content-box; min-width: 1ch; padding: 0; margin: 0; border: 0; outline: none; background: none; border-radius: 0;
  appearance: none; letter-spacing: normal; font-variant-ligatures: none; font-feature-settings: "calt" 0, "liga" 0;
  color: var(--zone); font: 600 10px/14.5px var(--mono); caret-color: var(--zone); user-select: text;
}
.rename-after { color: var(--label-zone); }
.rename-lost { color: var(--label-ruler-true); }
.rename-lost[hidden] { display: none; }
/* A mock FARP's (issue 056): its tag the same chip (look/markers.js's MOCK_TAG), edged in the facilities' dim, its name
   in their label ink. */
.rename-tag--mock { border-color: var(--facility-dim); color: var(--label-facility); }
.rename-tag--mock .rename-field { color: var(--label-facility); caret-color: var(--label-facility); }
/* A slope area's (issue 116): its tag's chip edged in the neutral's dim, its name in the neutral label ink. */
.rename-tag--slope { border-color: var(--neutral-dim); color: var(--label-neutral); }
.rename-tag--slope .rename-field { color: var(--label-neutral); caret-color: var(--label-neutral); }
/* The tag's other looks while it is a field (6f-3's review, 5): the hostile's dim edge while anyone is inside; the
   whole tag at .6 while its anchor is lost. */
.rename-tag--alert { border-color: var(--airr-dim); }
.rename-tag--lost { opacity: .6; }

.ring-popup .maplibregl-popup-content { background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0; pointer-events: none; }
.ring-popup .maplibregl-popup-tip { display: none; }
.ring-popup .ring { transform-origin: 50% 50%; animation: ring-roll 300ms cubic-bezier(.45, 0, .35, 1), ring-wipe 300ms cubic-bezier(.45, 0, .35, 1) forwards;
  mask-image: conic-gradient(from var(--ring-from, 0deg), transparent calc(360deg - var(--ring-sweep)), var(--white) 0); }
.ring-popup.ring-out { animation: card-out 120ms ease-in forwards; }
.ring-popup.ring-out .ring-part, .ring-popup.ring-out .ring-fan-gap { pointer-events: none; }
@keyframes ring-roll { from { opacity: 0; transform: rotate(90deg) scale(.8); } to { opacity: 1; transform: none; } }
@keyframes ring-wipe { from { --ring-sweep: 0deg; } to { --ring-sweep: 360deg; } }
.ring { --ring-from: 0deg; position: relative; width: 256px; height: 256px; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.ring-disk { display: block; overflow: visible; }
.ring-part { pointer-events: auto; cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent; }
.ring-part-bg { fill: color-mix(in srgb, var(--surface) 94%, transparent); }
.ring-mark-fill { fill: var(--text-bright); }
.ring-mark-line { fill: none; stroke: var(--text-bright); }
.ring-part:hover .ring-mark-fill, .ring-part:focus-visible .ring-mark-fill { fill: var(--hue); }
.ring-part:hover .ring-mark-line, .ring-part:focus-visible .ring-mark-line { stroke: var(--hue); }
.ring-word { fill: var(--text); font: 400 9.5px var(--mono); letter-spacing: .04em; text-anchor: middle; }
.ring-part--off { cursor: default; }
.ring-part--off .ring-mark-fill, .ring-part--off:hover .ring-mark-fill { fill: var(--text-faint); }
.ring-part--off .ring-mark-line, .ring-part--off:hover .ring-mark-line { stroke: var(--text-faint); }
.ring-part--off .ring-word { fill: var(--text-faint); }
.ring-lines { fill: none; stroke: var(--hairline-strong); stroke-width: 1; pointer-events: none; }
.ring-outline { fill: none; stroke: var(--hue); stroke-width: 1.5; pointer-events: none; }
/* Its fan (issue 165; issue 116's step (a1); ui/ring.js): the tools less needed in flight, a second tier over the
   core's top in the core's own fill and lines, opened by three dots on the core's rim. The dots draw nothing else at
   rest; under the pointer the capsule they sit in shows faintly; a soft shadow keeps them on a bright map. The fan
   unfolds out of them -- grown from their place, its slots fading in from the middle outwards (`--ring-fan-delay`,
   set by ui/ring.js) -- and none of it under reduced motion. The box (256 px, the fan's extent: the roll-out's mask is
   the box's) takes no pointer; the parts and the dots' press do. */
.ring-fan { --ring-fan-from: 50% 50%; --ring-fan-delay: 0ms; }
.ring-hit { fill: transparent; }
/* The gap between the core and the open fan takes a press and does nothing, so a double press on the dots stays the
   ring's (the box takes none, and a press through it is the map's, closing the ring). */
.ring-fan-gap { pointer-events: auto; }
.ring-dots .ring-part-bg { fill: transparent; }
.ring-dots:hover .ring-part-bg, .ring-dots:focus-visible .ring-part-bg { fill: color-mix(in srgb, var(--surface) 80%, transparent); }
.ring-dot { fill: var(--text); filter: drop-shadow(0 0 1.5px color-mix(in srgb, var(--casing) 90%, transparent)); }
.ring-dots:hover .ring-dot, .ring-dots:focus-visible .ring-dot { fill: var(--hue); }
.ring-fan--out { transform-box: view-box; transform-origin: var(--ring-fan-from); animation: ring-fan-unfold 200ms cubic-bezier(.2, .8, .3, 1) both; }
.ring-fan--out .ring-part { animation: ring-fan-part 160ms ease-out var(--ring-fan-delay) both; }
@keyframes ring-fan-unfold { from { opacity: 0; transform: scale(.7); } to { opacity: 1; transform: none; } }
@keyframes ring-fan-part { from { opacity: 0; } to { opacity: 1; } }
/* Where the ring is shifted off its spot (an edge, the drawer), the spot keeps a mark of its own, placed back by the
   shift: a small hollow ring, cased. */
.ring-spot { position: absolute; left: 123px; top: 123px; width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%;
  border: 2px solid var(--text-bright); box-shadow: 0 0 0 1px color-mix(in srgb, var(--casing) 70%, transparent); pointer-events: none; }
.ring-spot[hidden] { display: none; }

/* ---- VR mode (step 6g; ui/panel/display.js; DESIGN_NOTES section 2.13) -----
   The same elements, a second look, for a controller's ray or a fingertip:
   `device.vr` puts the `vr` class on the body, and every rule of this block
   starts `.vr ` (a check), so with it off nothing here applies. The user's
   pick of the trial (artifacts/vr-panel, "D", 2026-10-04): quiet -- no
   outline at rest, the layer's colour once on -- and one width a group. A
   row's VR mark is hidden outside the block, which shows it. */
.vr-mark { display: none; }

/* Lines of buttons: two to a line, each half the width less half the gap and
   growing, so a button alone on its line fills it (the user); what isn't a
   button takes a line of its own. */
.vr .section, .vr .layer-rows, .vr .row-subs { display: flex; flex-wrap: wrap; gap: 6px; }
.vr .section > *, .vr .layer-rows > * { flex: 1 1 100%; min-width: 0; }
.vr .section-label { margin-bottom: 2px; }
.vr .layer-group-label { margin-top: 10px; }
.vr .section > .field-label { margin: 4px 0 -3px; }

/* A switch row is a button: its mark, its name, its count under the name;
   the switch stretched over it with its knob hidden, so a tap lands on the
   element its handler is bound to and no handler changes. */
.vr .row {
  flex: 1 1 calc(50% - 3px); position: relative; box-sizing: border-box; min-width: 0;
  display: grid; grid-template-columns: auto 1fr; align-items: center; align-content: center;
  min-height: 52px; padding: 7px 9px; font-size: 12px;
  border: 1px solid transparent; border-radius: 9px; background: color-mix(in srgb, var(--white) 4.5%, transparent);
}
.vr .row:not([class*="hue-"]) { --hue: var(--facility); }
.vr .row-side { display: contents; }
.vr .swatch { display: none; }
.vr .vr-mark { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; grid-row: 1 / span 2; margin-right: 8px; }
.vr .vr-mark > canvas { display: block; flex: none; }
/* The letter the map writes over a marker (a FARP's H): the facility layer's 12 px SemiBold, in its colour, centred on
   the mark as MapLibre centres it on the sprite. */
.vr .vr-mark { position: relative; }
.vr .vr-mark-letter { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font: 600 12px/1 var(--mono); color: var(--facility); pointer-events: none; }
.vr .row-name { grid-column: 2; line-height: 1.2; color: var(--text-dim); }
.vr .row:not(:has(.vr-mark)) { grid-template-columns: 1fr; }
.vr .row:not(:has(.vr-mark)) .row-name { grid-column: 1; }
.vr .row:not(:has(.row-count)) .row-name { grid-row: 1 / span 2; }
.vr .row-count { grid-column: 2; justify-self: start; margin-top: 2px; padding: 0; background: none; }
/* No transition: the switch's background change would play across the whole stretched row as VR mode comes on, a
   flash of red or grey (6g-1's review, 2). */
.vr .row .switch { position: absolute; inset: 0; z-index: 1; width: auto; height: auto; border-radius: 9px; background: none; transition: none; }
.vr .row .switch::after { display: none; }
.vr .row .switch:focus-visible { outline-offset: -2px; }

/* On: the border at 65% of the row's hue (steel for a switch of no layer), a
   brighter fill, the name bright. Off: the mark grey and faded. Disabled:
   faint. Hover lightens the fill, last, so it shows on an "on" too. */
.vr .row:has(> .row-side > .switch[aria-checked="true"]), .vr .row:has(> .switch[aria-checked="true"]) {
  border-color: color-mix(in srgb, var(--hue) 65%, transparent); background: color-mix(in srgb, var(--white) 6%, transparent);
}
.vr .row:has(> .row-side > .switch[aria-checked="true"]) .row-name, .vr .row:has(> .switch[aria-checked="true"]) .row-name { color: var(--text); }
.vr .row:has(> .row-side > .switch[aria-checked="false"]) .vr-mark { filter: grayscale(1); opacity: .4; }
.vr .row:has(> .switch:disabled) { opacity: .45; }
.vr .row:has(.switch:not(:disabled)):hover { background: color-mix(in srgb, var(--white) 10%, transparent); }

/* The Layers rows: a list, every layer one line of its own, 44 px; the name
   centred on the mark, the count at the right in one column, a lane kept on
   every row for the label strip. */
.vr .layer-rows > .row { flex-basis: 100%; grid-template-columns: auto 1fr auto; min-height: 44px; padding-right: 52px; }
.vr .layer-rows > .row .row-name { grid-row: 1 / span 2; }
.vr .layer-rows > .row .row-count { grid-column: 3; grid-row: 1 / span 2; justify-self: end; margin: 0 4px 0 0; }
/* "Aa": a strip 40 px wide at the row's right edge, past a hairline, lit in
   the layer's hue (not the panel's red) and kept lit under the ray; the ray
   on it lightens it alone, not its row. */
.vr .row .label-toggle {
  position: absolute; top: 4px; bottom: 4px; right: 4px; z-index: 2; width: 40px;
  display: flex; align-items: center; justify-content: center; padding: 0;
  border-radius: 6px; font-size: 10.5px; background: none;
}
.vr .layer-rows > .row:has(> .row-side > .label-toggle)::after {
  content: ''; position: absolute; top: 9px; bottom: 9px; right: 48px; width: 1px; background: var(--hairline); pointer-events: none;
}
.vr .row .label-toggle[aria-pressed="true"] { color: var(--text-bright); border-color: color-mix(in srgb, var(--hue) 65%, transparent); }
.vr .row .label-toggle:not(:disabled):hover { color: var(--text); background: color-mix(in srgb, var(--white) 12%, transparent); }
.vr .row .label-toggle:not(:disabled):not([aria-pressed="true"]):hover { border-color: var(--hairline-strong); }
/* Disabled (its layer off), the strip takes no pointer, so a tap there reaches the row's switch, which the row's hover
   promises (6g-1's review, 3: it swallowed the tap while the row lit). */
.vr .row .label-toggle:disabled { pointer-events: none; }
.vr .layer-rows > .row:has(.label-toggle:not(:disabled):hover) { background: color-mix(in srgb, var(--white) 4.5%, transparent); }
.vr .layer-rows > .row:has(> .row-side > .switch[aria-checked="true"]):has(.label-toggle:not(:disabled):hover) { background: color-mix(in srgb, var(--white) 6%, transparent); }
/* A row's settings: one per line under it, 40 px, behind the desktop's thin
   line down from the row, lit in the row's hue (the user, the trial). */
.vr .row-subs { flex: 1 1 100%; margin: 0 0 2px 22px; padding-left: 10px; }
.vr .row--sub { flex-basis: 100%; min-height: 40px; padding: 5px 9px; font-size: 11.5px; border-radius: 7px; }
.vr .row--sub .switch { border-radius: 7px; }

/* The small buttons: 32 px, equal widths, a long word wrapping inside its
   button; "clear" and "labels" a label over their line. */
.vr .btn-row { display: flex; flex-wrap: wrap; gap: 6px; }
.vr .btn-row .btn {
  flex: 1 1 0; min-width: 0; display: inline-flex; align-items: center; justify-content: center; min-height: 32px;
  padding: 4px 6px; white-space: normal; line-height: 1.2; text-align: center; font-size: 10.5px; border-radius: 7px;
}
.vr .btn-row .labels-word { flex: 1 1 100%; text-transform: uppercase; letter-spacing: .08em; }
/* The clear row keeps its grid, three to a line, its buttons as tall as the other small buttons (32 px). */
.vr .clear-row { display: grid; }

/* The header: the status on a line of its own, so its length moves nothing,
   then the restart countdown (7d-2: under the buttons it looked stranded),
   then the version and collapse, shaped alike, 32 px. */
.vr .panel-sub { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
.vr .restart-text { margin-top: 0; }
.vr .panel-sub-right { gap: 6px; }
.vr .version { margin-right: auto; display: inline-flex; align-items: center; min-height: 32px; padding: 0 10px; border-radius: 7px; font-size: 10.5px; color: var(--text-dim); }
.vr .version::before { inset: 0; }     /* the 32 px pill is its own target */
/* The help card (ib-2) for the ray: larger text and room between its lines, the × the card's controls' 32 px. */
.vr .help { font-size: 14px; }
.vr .help-item { margin: 8px 0; }
.vr .btn.panel-toggle { display: inline-flex; align-items: center; justify-content: center; min-height: 32px; min-width: 36px; padding: 0 10px; font-size: 14px; border-radius: 7px; }

/* Fields keep their look; their focus border steel, since the Quest keeps
   focus after a picker closes and the red stayed on show. Search's rows 44 px
   and its × 32; Advanced's summary 40, the section's 6 px gap above it. */
.vr .panel select:focus, .vr .panel input:focus { border-color: color-mix(in srgb, var(--facility) 65%, transparent); }
.vr .search-box { --search-row-h: 44px; }
.vr .search-clear { min-width: 32px; min-height: 32px; right: 2px; }
.vr .panel .search-field input[type="search"] { padding-right: 36px; }     /* the text clear of the × (the review) */
.vr .advanced { margin-top: 0; }
.vr .sync { margin-top: 0; }
.vr .sync-reveal { min-height: 32px; font-size: 13px; }
.vr .sync-words .card-copy { margin: -4px -6px -4px 0; }
.vr .disclosure > summary { min-height: 40px; }

/* The drawer (milestone 5's D6): its rows and bars at least 40 px tall, their
   content centred. Its tab keeps the desktop's 30 px: D6's 48 looked too
   bulky (the user, 6g-2's check), and the tab is tall enough to hit. */
.vr button.drawer-row, .vr button.drawer-bar { min-height: 40px; align-content: center; }
.vr button.drawer-row { align-items: center; }
.vr .drawer-row, .vr .drawer-bar { contain-intrinsic-size: auto 40px; }

/* A card's small controls: 32 px to hit, the mark the same size inside. */
.vr .card-close { top: -12px; right: -14px; min-width: 32px; min-height: 32px; padding: 0; display: flex; align-items: center; justify-content: center; }
.vr .card-title { margin-right: 20px; }     /* a long title's first line clear of the × (the review) */
.vr .card-copy { width: 32px; height: 32px; margin: -11px -10px -11px -4px; vertical-align: middle; }
.vr .card-copy--done { width: 32px; height: 32px; }
.vr .card-action { min-height: 32px; padding: 0 10px; font-size: 10.5px; border-radius: 7px; }

/* Hidden stays hidden: a row's or a section's display here beats the
   browser's own [hidden] rule (PROJECT_NOTES section 9). */
.vr [hidden] { display: none !important; }
/* ---- end of VR mode ---- */

/* ---- the phone look (issue 093, step 2) ----
   Keyed on the body's `phone` class, which boot sets from the `layout`
   slice: a window under 500 px wide or under 480 high (state/layout.js),
   whatever VR mode says. One element, two looks (DESIGN_NOTES sections 2.13
   and 2.20):
   - the panel's header is a bar along the bottom: the status, the
     countdown, the version tag and MENU, the title hidden;
   - the drawer's tab is its right end, INTEL, open or closed;
   - each opens its sheet over the bar, one at a time (domain/sheets.js).
   Upright a sheet is half the screen's height and full width; sideways the
   height above the bar and the drawer's width, the panel's at the left and
   the drawer's at the right (the sizes on :root above).
   The map's container ends at the bar, which nothing is seen through
   (step 2's architecture review, F1): the map's own resize follows it, and
   the credit and the readout ride on it in their corner. Every rule here
   starts `.phone ` (a check). The block comes after VR mode's, so where
   both apply its header rules win. */
.phone #map { bottom: var(--bar-h); }

/* The panel: the bar at the foot of a box the whole screen's size, its
   sheets above the bar. The box draws nothing and takes no press: only the
   bar and the sheets do, so INTEL beside the bar and the map beside a
   sideways sheet take theirs (step 2a's review, 1). It is clipped to an L,
   everything but INTEL's corner of the bar: the panel stacks over the
   drawer, so a sheet sliding away under the bar had passed over INTEL (the
   user, at 2b's check); cut at the bar's top there, it passes behind it. */
.phone .panel, .phone .panel:has(> .help:not([hidden])) {
  top: 0; right: 0; bottom: 0; left: 0; width: auto; max-width: none; max-height: none; overflow: visible;
  display: flex; flex-direction: column;
  background: none; border: 0; border-radius: 0; box-shadow: none; pointer-events: none;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--bar-h)), calc(100% - var(--tab-w)) calc(100% - var(--bar-h)),
    calc(100% - var(--tab-w)) 100%, 0 100%);
}
.phone .panel-header, .phone #panelBody, .phone .help { pointer-events: auto; }
/* The bar: one line, clear of INTEL at its right end, over both sheets as they slide under it. The title's words go
   and its dot stays. The status gives way (an ellipsis) to the countdown, the tag and MENU. */
.phone .panel-header {
  position: relative; z-index: 2; margin-top: auto; display: flex; align-items: center; gap: 8px;
  height: var(--bar-h); box-sizing: border-box; margin-right: var(--tab-w); padding: 0 12px;
  background: var(--surface); border-top: 1px solid var(--hairline);
}
.phone .panel-title { flex: none; gap: 0; font-size: 0; letter-spacing: 0; }
.phone .panel-sub { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: center; gap: 8px; margin-top: 0; }
.phone .status-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phone .restart-text { flex: none; margin-top: 0; }
.phone .panel-sub-right { flex: none; margin-left: auto; gap: 10px; }
/* MENU: the toggle's word. The toggle reaches over the whole bar (VR mode's stretched switch, DESIGN_NOTES section
   2.13), its own glyph hidden; the version tag keeps its own tap over it. */
/* MENU and INTEL are bright while their sheet is up, dim otherwise, with no hover of their own: a touch's hover stuck
   after the tap and left them bright until a press elsewhere (the user, at 2b's check). */
.phone .panel-toggle-word { display: inline; font: 400 10px var(--mono); letter-spacing: .08em; color: var(--text-dim); }
.phone .panel:not(.panel--collapsed) .panel-toggle-word { color: var(--text-bright); }
.phone .btn.panel-toggle { width: 0; min-width: 0; padding: 0; border: 0; font-size: 0; }
.phone .btn.panel-toggle::before { content: ''; position: absolute; inset: 0; }
.phone .btn.panel-toggle:focus-visible { outline: none; }
.phone .btn.panel-toggle:focus-visible::before { outline: 2px solid var(--facility); outline-offset: -2px; }
.phone .version { z-index: 1; }
/* The panel's sheets: its body, and the help card over it, each a layer above the bar, each scrolling itself, a pull
   at its end never reaching the page.
   Both slide both ways, as the drawer's does (the user, at 2b's check: "both the close and the open have to be
   animated"; the help card's fade read oddly beside them). The views keep them rendered in the phone look -- the
   body while its panel is collapsed, the card while it is `help--shut` -- so closed each slides under the bar and
   turns hidden, nothing in it focusable. The body is up while the card is (domain/sheets.js: the card is the panel's
   sheet), so the card slides over it and away from it. `--sheet-slide` holds both still until the page's first show
   has been drawn (boot): a rise keyed on a sheet being shown had played at every reload and whenever the card and
   the body traded places (step 2a's review, 3). */
.phone #panelBody, .phone .help {
  position: absolute; right: 0; bottom: var(--bar-h); left: 0;
  height: var(--sheet-h); box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface); border-top: 1px solid var(--hairline);
  transition: transform calc(var(--sheet-slide) * .28s) cubic-bezier(.4, 0, .2, 1), visibility 0s;
}
.phone .panel:has(> .help) > #panelBody { display: block; }
.phone .help { z-index: 1; animation: none; }
/* Under the card once it has arrived the body turns hidden: nothing in it focusable or read out, nor shown above the
   card by search's full height (the round's review, 2). The card still slides over it, and off it, uncovering it. */
.phone .panel:has(> .help:not(.help--shut)) > #panelBody {
  visibility: hidden;
  transition: transform calc(var(--sheet-slide) * .28s) cubic-bezier(.4, 0, .2, 1), visibility 0s linear calc(var(--sheet-slide) * .28s);
}
/* Closed, each slides by its own height and the bar's, whatever its height (search's full height included). */
.phone .panel--collapsed #panelBody, .phone .help.help--shut {
  transform: translateY(calc(100% + var(--bar-h))); visibility: hidden;
  transition: transform calc(var(--sheet-slide) * .28s) cubic-bezier(.4, 0, .2, 1), visibility 0s linear calc(var(--sheet-slide) * .28s);
}
@supports not (-moz-appearance: none) {
  .phone #panelBody, .phone .help { scrollbar-width: thin; }
}
/* While a text field in it has the focus -- search's, the exporter's address, or sync's words -- and the keyboard is up, the
   panel's sheet takes the whole height above the bar, so the field sits at the screen's top with its list under it,
   not at its middle with the list under the keyboard (step 2's design review, 3). Text fields alone: a switch, a
   select or a button tapped takes the focus too, and the sheet leapt up over the map (2b's review, 1). The keyboard
   too (the body's `keyboard` class, ui/keyboard.js): put away with Back, it left the field focused and the sheet at
   full height (the user, at 2b's check). */
.phone.keyboard #panelBody:has(input[type="search"]:focus) { height: calc(100dvh - var(--bar-h)); }
.phone.keyboard #panelBody:has(input[type="url"]:focus) { height: calc(100dvh - var(--bar-h)); }
.phone.keyboard #panelBody:has(input[type="text"]:focus) { height: calc(100dvh - var(--bar-h)); }

/* The drawer: its box is the sheet's place, and stays put, so INTEL hangs below it in the bar, open or closed. Its
   head and body slide, under the bar (z under the panel's) and under the tab. */
.phone .drawer {
  z-index: 950; top: auto; right: 0; bottom: var(--bar-h); left: 0; width: auto; max-width: none; height: var(--sheet-h);
  background: none; border: 0; transform: none; pointer-events: none;
  transition: visibility 0s linear calc(var(--drawer-slide) * .28s);
}
.phone .drawer.drawer--open { transform: none; transition: visibility 0s; }
.phone .drawer-head, .phone .drawer-body {
  background: var(--surface); pointer-events: auto;
  transform: translateY(calc(var(--sheet-h) + var(--bar-h)));
  transition: transform calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1);
}
.phone .drawer--open .drawer-head, .phone .drawer--open .drawer-body { transform: none; }
.phone .drawer-head { border-top: 1px solid var(--hairline); }
.phone .drawer-tab {
  z-index: 1; top: 100%; right: 0; left: auto; width: var(--tab-w); height: var(--bar-h); padding: 0; transform: none;
  writing-mode: horizontal-tb; border: 0; border-top: 1px solid var(--hairline); border-left: 1px solid var(--hairline);
  border-radius: 0; pointer-events: auto;
}
.phone .drawer-tab, .phone .drawer-tab:hover { color: var(--text-dim); border-color: var(--hairline); }
.phone .drawer-tab[aria-expanded="true"] { color: var(--text-bright); }

/* The edges. Upright, the tripwire alert keeps the top, the whole width; the
   hint, the credit and the readout ride on the bar, or on the open sheet,
   following its edge as it slides (the user, at 2b's check: they had jumped
   there). The readout shows over either sheet (boot). */
.phone .alert-band { right: 0; }
.phone .hint {
  --hint-drawer: 0px; left: 50%; bottom: calc(var(--bar-h) + max(var(--panel-on), var(--drawer-on)) * var(--sheet-h) + 12px);
  transition: left calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1), bottom calc(var(--sheet-slide) * .28s) cubic-bezier(.4, 0, .2, 1);
}
.phone .maplibregl-ctrl-bottom-right {
  right: 0; bottom: calc(max(var(--panel-on), var(--drawer-on)) * var(--sheet-h));
  transition: right calc(var(--drawer-slide) * .28s) cubic-bezier(.4, 0, .2, 1), bottom calc(var(--sheet-slide) * .28s) cubic-bezier(.4, 0, .2, 1);
}
.phone .readout, .phone .maplibregl-ctrl.readout { max-width: calc(100vw - 36px); }

/* Sideways: the sheets at the sides, the edges clear of the open one. */
@media (orientation: landscape) {
  .phone #panelBody, .phone .help { width: var(--sheet-w); border-right: 1px solid var(--hairline); }
  .phone .drawer { left: auto; width: var(--sheet-w); }
  .phone .drawer-head, .phone .drawer-body { border-left: 1px solid var(--hairline); }
  .phone .alert-band { left: calc(var(--panel-on) * var(--sheet-w)); right: calc(var(--drawer-on) * var(--sheet-w)); }
  .phone .hint {
    --hint-drawer: calc((var(--panel-on) + var(--drawer-on)) * var(--sheet-w));
    left: calc(var(--panel-on) * var(--sheet-w) + (100% - var(--hint-drawer)) / 2); bottom: calc(var(--bar-h) + 12px);
  }
  .phone .maplibregl-ctrl-bottom-right { right: calc(var(--drawer-on) * var(--sheet-w)); bottom: 0; }
  .phone .readout, .phone .maplibregl-ctrl.readout { max-width: calc(100vw - 36px - var(--drawer-on) * var(--sheet-w)); }
}

/* Hidden stays hidden, as under VR mode (PROJECT_NOTES section 9). */
.phone [hidden] { display: none !important; }
/* ---- end of the phone look ---- */

/* ---- Reduced motion (issue 086; issue 024, step 8a-2) ----
   Keyed on the body's `reduce-motion` class, which boot sets from its one
   answer, followed live: the system's setting (prefers-reduced-motion) OR
   the page's own Reduce motion switch, since the Quest has no system
   setting and a page can't switch a media query. Every rule here starts
   `.reduce-motion `, and every selector that moves is named here (a check). */
.reduce-motion .status-dot { animation: none; }
.reduce-motion .card-popup, .reduce-motion .card-popup.card-out { animation: none; }
.reduce-motion .ring-popup .ring, .reduce-motion .ring-popup.ring-out { animation: none; mask-image: none; }
.reduce-motion .ring-fan--out, .reduce-motion .ring-fan--out .ring-part { animation: none; }
/* The open drawer declares its own transition, more specific than `.drawer`, so it is named here too, or the
   slide in still ran (the user, 5f's start). */
.reduce-motion .drawer, .reduce-motion .drawer.drawer--open, .reduce-motion .maplibregl-ctrl-bottom-right,
.reduce-motion .alert-band, .reduce-motion .hint { transition: none; }
.reduce-motion .switch, .reduce-motion .switch::after { transition: none; }
.reduce-motion .search-box { transition: none; }
.reduce-motion .help { animation: none; }
/* The phone look's sheets (issue 093): the panel's body's and the help card's slide, the drawer's head's and body's,
   its box's hold, and the edges riding on them. */
.reduce-motion .phone #panelBody, .reduce-motion .phone .help, .reduce-motion .phone .panel--collapsed #panelBody,
.reduce-motion .phone .help.help--shut, .reduce-motion .phone .hint, .reduce-motion .phone .maplibregl-ctrl-bottom-right { transition: none; }
.reduce-motion .phone .panel:has(> .help:not(.help--shut)) > #panelBody { transition: none; }
.reduce-motion .phone .drawer, .reduce-motion .phone .drawer.drawer--open, .reduce-motion .phone .drawer-head,
.reduce-motion .phone .drawer-body { transition: none; }
/* ---- end of reduced motion ---- */

/* Reduce motion's own switch never slides, either way (the user, 8a-2's
   check): turned on, the class above stills it in the instant it flips, so
   only turning it off slid. No exception to the stilling: this switch has no
   motion at all. */
#motionSwitch, #motionSwitch::after { transition: none; }
