/* ---------------------------------------------------------------------------
   The public site's stylesheet.

   The palette below is assets/admin.css's token block, copied name for name and
   value for value. The two files stay separate -- a shared *shell* couples the
   site to the admin tool, which is why 5d72f80 pulled them apart -- but a shared
   *palette* does not, and the site had no business looking like a different
   product from its own admin console.

   Two colours, deliberately, and the rule is admin.css's: DeltaNerd's blue
   (#66ccff) carries every link, focus ring and interactive edge; amber
   (#ffb454) carries the numbers, the active nav item and the section rules.
   **Blue is what you click, amber is what you read.** Nothing amber is
   clickable; nothing blue is a statistic.

   ONE EXCEPTION, and it is written down rather than discovered: `.toast.info`
   is blue and is not a control. It is affordable there and nowhere else because
   a toast is `pointer-events: none`, holds no control but its ✕, and is gone in
   five seconds -- there is no moment in which somebody tries to click it and
   finds they cannot. A second blue exception means the rule has gone.

   Watch --accent when copying between the two files: here and in admin.css it
   is the amber, and the clickable colour is --blue. It used to mean the
   opposite in this file.

   No external font, and five scripts, each of which a page opts into:
   assets/tip.js moves the single .tooltip element, assets/menu.js closes the
   account menu, assets/confirm.js opens the single <dialog class="confirm">,
   assets/copy.js reaches the clipboard for a [data-copy] button, and
   assets/toast.js builds and removes the single .toast. All of them are
   same-origin, so `default-src 'self'` stays untouched. Note
   also that the CSP covers the style *attribute*, so nothing on this site may
   set a width inline -- progress bars are <progress> elements driven from their
   IDL `value`, styled from here.
   --------------------------------------------------------------------------- */

:root {
  color-scheme: dark;

  --bg: #0c0f14;
  --bg-bar: #11151d;
  --panel: #151a23;
  --panel-2: #1b212c;
  --panel-3: #202836;

  --line: #232b38;
  --line-2: #2f3a4a;

  --text: #e7eaf0;
  --text-dim: #b6bece;
  --muted: #8a93a5;
  --faint: #6b7385;

  --blue: #66ccff;
  --blue-2: #38b6ef;
  --blue-ink: #0b2030;

  --accent: #ffb454;
  --accent-2: #ff9b2f;

  /* The sessions tables' counter-clockwise arrow, and the only purple on the
     site. It is the viewer's own `PURPLE` -- `#a78bfa` in
     viewer/js/selection.js -- copied here on purpose: the two colours a driver
     reads "which way round" from must be one colour, and the viewer got there
     first. It pairs with --accent-2 for clockwise, which is this token's first
     use on the site. Neither is a link and neither is clickable; that column
     holds nothing that can be pressed. */
  --purple: #a78bfa;

  --ok: #5fd39a;
  --danger: #ff6b6b;
  --danger-line: #6d2b30;
  --danger-bg: #1e1216;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.6 system-ui, -apple-system, "Segoe UI Variable", "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--blue); text-decoration: none; }
a:hover { color: #9ddfff; text-decoration: underline; }

/* -- the bar --------------------------------------------------------------
   The site's own header, not admin's sticky topbar. Public pages are read top
   to bottom; a console is navigated. */

.bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: .75rem 1.5rem;
  background: var(--bg-bar);
  border-bottom: 1px solid var(--line);
  /* So the account menu's panel paints over the page under it, whatever the
     page is made of. */
  position: relative;
  z-index: 20;
}

.brand { font-weight: 600; letter-spacing: -.02em; color: var(--text);
         display: inline-flex; align-items: center; gap: .5rem; }
.brand:hover { text-decoration: none; }
/* Sized in the markup as well as here: the mark is decorative and alt="" , so
   a page that fails to load the CSS must not reflow around a full-size SVG. */
.brand-mark { width: 24px; height: 24px; border-radius: 6px; display: block; }
/* -- the site's own navigation ---------------------------------------------
   Between the brand and the account corner: the pages that belong to nobody.

   THE SAME LINKS SIGNED IN OR OUT, which is the whole reason they are on the
   bar rather than in the account menu. Search is public -- no account, no
   cookie, nothing withheld -- so hiding it behind "My Account" would hide it
   from exactly the people it is for. Everything that belongs to one person
   stays in that menu, and this is what is left.

   The admin console's topbar treatment, borrowed rather than reinvented: a
   muted tab, a lit one for the page you are on, and an amber rule under it.
   Amber and not blue, and it is not a bend of "nothing amber is clickable" --
   the rule is a MARK under the tab, not the tab, exactly as `h2` wears one.

   Drawn off `aria-current` alone, so the underline on screen and the state a
   screen reader announces are one fact and cannot disagree. The current tab is
   still a link and still highlights on hover: it goes back to the top of its
   own search, which is how you clear one. */
/* The negative margins cancel the bar's own vertical padding, so a tab is the
   full height of the header and its rule lands ON the bar's bottom border
   rather than floating a few pixels above it. */
.site-nav {
  display: flex;
  align-items: stretch;
  align-self: stretch;
  gap: 2px;
  min-width: 0;
  margin: -.75rem 0 calc(-.75rem - 1px);
}
.site-nav a {
  display: flex;
  align-items: center;
  padding: 0 .8rem;
  color: var(--muted);
  font-size: .89rem;
  font-weight: 500;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  transition: color .12s, background .12s;
}
.site-nav a:hover { color: var(--text); background: var(--panel-2); text-decoration: none; }
.site-nav a[aria-current] {
  color: var(--text);
  font-weight: 600;
  border-bottom-color: var(--accent);
}

@media (max-width: 640px) {
  /* The brand and the account corner are what a phone has room for. The two
     search pages are reachable from the front page, which is what the brand
     goes to. */
  .site-nav { display: none; }
}

/* The account corner: the far right of the bar, and the only part of the header
   that differs signed in from signed out. Everything an account has is inside
   the menu rather than strung along the bar, so that the header -- and the
   front page under it -- reads the same to a stranger and to the person who
   owns the data. */
.bar-right { margin-left: auto; display: flex; align-items: center; gap: .5rem; }

/* A <details>, not a scripted popover: it opens with no JavaScript, closes on a
   second press, and is already a button to a keyboard and a screen reader.
   assets/menu.js adds only the two things a native <details> has no opinion
   about -- clicking away, and Escape. */
.menu { position: relative; }
.menu > summary {
  /* Both spellings: WebKit still draws the triangle through its own
     pseudo-element and ignores list-style on a summary. */
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  white-space: nowrap;
  cursor: pointer;
}
.menu > summary::-webkit-details-marker { display: none; }
/* Drawn here rather than typed into the markup: it is the state of the control,
   not a word of the label. */
.menu > summary::after { content: "\25be"; font-size: .8em; color: var(--muted); }
.menu[open] > summary::after { color: var(--text); }

.menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + .6rem);
  z-index: 30;
  min-width: 13rem;
  padding: .45rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 9px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, .45);
}
/* The address the menu belongs to, above the things it can do. It wraps rather
   than widens the panel: an email address has no length limit. */
.menu-panel .who {
  display: block;
  padding: .25rem .55rem .5rem;
  margin-bottom: .35rem;
  border-bottom: 1px solid var(--line);
  overflow-wrap: anywhere;
}
/* The sign-out row. A class rather than `.menu-panel form`, so the style guide
   can draw the real thing without a <form> on it -- see Views/style_guide.php
   on why that page has none. */
.menu-out { padding: .5rem .55rem .15rem; margin-top: .35rem; border-top: 1px solid var(--line); }

.who { color: var(--faint); font-size: .85rem; }

/* The signed-in navigation, in the order the day goes. The current item is
   marked with an amber edge, matching admin's active tab and the site's rule
   that amber is read and never clicked -- it says where you are, and it is
   already where clicking it would take you. */
.menu-nav { display: grid; }
.menu-nav a {
  padding: .4rem .55rem;
  border-radius: 6px;
  color: var(--text-dim);
  font-size: .92rem;
  font-weight: 500;
}
.menu-nav a:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
.menu-nav a[aria-current="page"] {
  color: var(--text);
  font-weight: 650;
  box-shadow: inset 2px 0 0 var(--accent);
}

main { padding: 1.5rem; max-width: 70rem; margin: 0 auto; }
.narrow { max-width: 24rem; margin: 3rem auto; }

/* -- headings and prose ---------------------------------------------------- */

h1 {
  margin: 0 0 .35rem;
  font-weight: 650;
  letter-spacing: -.02em;
  font-size: 1.5rem;
  line-height: 1.25;
}

/* A rule across the column, so sections stay legible on a long page. */
h2 {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin: 2.6rem 0 .9rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--line);
  color: var(--text-dim);
  font-size: .84rem;
  font-weight: 650;
  letter-spacing: .1em;
  text-transform: uppercase;
}
h2::before {
  content: "";
  width: 3px;
  height: 1.05em;
  border-radius: 2px;
  background: var(--accent);
}

h3 { margin: 1.6rem 0 .5rem; font-size: 1rem; font-weight: 650; color: var(--text); }
h3 .muted { font-weight: 400; font-size: .85rem; }

h4 {
  margin: 1.3rem 0 .35rem;
  font-size: .8rem;
  font-weight: 650;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* A sentence 200 characters wide is unreadable even inside a 70rem column. */
p { max-width: 76ch; margin: .6rem 0; }
.muted { color: var(--muted); }
p.muted { font-size: .92rem; }
.error { color: var(--danger); }
.ok { color: var(--ok); }
strong { font-weight: 650; color: var(--text); }

.mono {
  font-family: ui-monospace, "Cascadia Mono", SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .85em;
}
.trunc { max-width: 26rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* -- the front page -------------------------------------------------------- */

.hero { margin: 2.5rem 0 3rem; }
.hero h1 { font-size: 2.4rem; letter-spacing: -.03em; }

.lede { max-width: 42ch; margin: .35rem 0 1.4rem; color: var(--text-dim); font-size: 1.15rem; }

.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1.1rem; max-width: none; }

/* The one filled button on the site, and it is blue, because it is the thing
   to click. Nothing amber anywhere is clickable. */
.cta {
  padding: .55rem 1.1rem;
  border-radius: 8px;
  background: linear-gradient(180deg, var(--blue) 0%, var(--blue-2) 100%);
  color: var(--blue-ink);
  font-weight: 650;
}
.cta:hover { color: var(--blue-ink); text-decoration: none; filter: brightness(1.08); }

/* A second, quieter line inside the same button, for the one thing a control
   can usefully say about itself before it is pressed: which file it hands you.
   `small` and not a class -- it is part of the label, not a component, and a
   cta without one is unchanged, which is what the :has() keeps true. Nothing
   else goes on this line: two lines of label is the limit, and a third would
   be the explanatory text the site does not have. */
.cta:has(small) { display: inline-grid; justify-items: center; padding: .42rem 1.1rem; line-height: 1.3; }
.cta small { font-size: .78rem; font-weight: 550; opacity: .78; }

.how ol { max-width: 76ch; margin: 1rem 0; padding-left: 1.3rem; }
.how li { margin: .55rem 0; color: var(--text-dim); }

/* -- forms ----------------------------------------------------------------- */

.stack { display: grid; gap: .8rem; }
.stack p { margin: 0; }

label { display: grid; gap: .3rem; font-size: .88rem; color: var(--muted); }

/* A label wrapping a box rather than sitting above one: the text goes beside
   it, and it is text you read rather than a field name, so it takes the
   brighter ink. Same rule as admin.css. */
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
  display: flex;
  align-items: center;
  gap: .5rem;
  color: var(--text-dim);
}

input, select, textarea {
  font: inherit;
  font-size: .95rem;
  padding: .5rem .65rem;
  border: 1px solid var(--line-2);
  border-radius: 7px;
  background: #0e131b;
  color: var(--text);
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--blue);
  box-shadow: 0 0 0 3px rgba(102, 204, 255, .16);
}
input::placeholder, textarea::placeholder { color: var(--faint); }
input:disabled, select:disabled, textarea:disabled { opacity: .45; cursor: not-allowed; }
/* Readonly is not disabled: the value is still worth reading and copying, it
   just cannot be edited here. */
input:read-only:not([type="checkbox"]):not([type="radio"]) { color: var(--text-dim); background: var(--panel); }

/* Vertical only. A textarea that can be dragged wider breaks the column it
   sits in, and there is nothing on this site with enough text to need it. */
textarea { min-height: 6rem; resize: vertical; }

/* Boxes and dials keep the browser's own control -- accent-color is all it
   takes to put them in the palette, and a hand-drawn checkbox loses the
   keyboard and screen-reader behaviour that comes free. Blue, because they are
   things you click. */
input[type="checkbox"], input[type="radio"] {
  width: auto;
  padding: 0;
  accent-color: var(--blue);
  /* Nudged off the text baseline so a wrapped label lines up. */
  margin: 0;
  flex: none;
}

/* A switch is a checkbox that has already happened -- see the style guide. The
   knob is a radial-gradient on the input itself rather than a pseudo-element,
   because Firefox does not render ::before/::after on an <input>. */
input[type="checkbox"].switch {
  appearance: none;
  -webkit-appearance: none;
  width: 2.6rem;
  height: 1.45rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background-color: var(--panel-3);
  background-image: radial-gradient(circle at center, var(--muted) 36%, transparent 37%);
  background-size: 1.35rem 1.35rem;
  background-position: left center;
  background-repeat: no-repeat;
  cursor: pointer;
  transition: background-color .15s, background-position .15s, border-color .15s;
}
input[type="checkbox"].switch:checked {
  border-color: var(--blue);
  background-color: rgba(102, 204, 255, .18);
  background-image: radial-gradient(circle at center, var(--blue) 36%, transparent 37%);
  background-position: right center;
}
input[type="checkbox"].switch:disabled { cursor: not-allowed; }

/* The file button is the only part of a file input we can style, and left
   alone it is a grey Windows button in the middle of the page. */
input[type="file"] { padding: .35rem .4rem; }
input[type="file"]::file-selector-button {
  font: inherit;
  font-size: .88rem;
  font-weight: 600;
  margin-right: .7rem;
  padding: .3rem .7rem;
  border: 1px solid var(--line-2);
  border-radius: 6px;
  background: linear-gradient(180deg, var(--panel-3) 0%, var(--panel-2) 100%);
  color: var(--text);
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { border-color: var(--blue); color: #fff; }

/* A group of radios or checkboxes that answer one question. The legend is the
   question and is styled like a field label, because that is what it is. */
fieldset {
  display: grid;
  gap: .45rem;
  margin: 0;
  padding: .75rem .9rem .85rem;
  border: 1px solid var(--line);
  border-radius: 8px;
}
legend {
  padding: 0 .35rem;
  color: var(--muted);
  font-size: .88rem;
}

/* Fields whose content has a known size. Nothing here hides text -- it stops a
   two-digit box spanning the column. */
.stack input[type="number"] { max-width: 8rem; }
.stack input[type="date"], .stack input[type="time"] { max-width: 12rem; }

/* One outlined button, worn by three elements. a.btn is for a link that is a
   control rather than a phrase -- the pair in the header -- and .menu > summary
   is a control that happens to be a summary. Not a second button style: the
   same rule, reached by things that are not <button>. */
button,
a.btn,
.menu > summary {
  font: inherit;
  font-size: .92rem;
  font-weight: 600;
  padding: .5rem .95rem;
  border: 1px solid var(--line-2);
  border-radius: 7px;
  background: linear-gradient(180deg, var(--panel-3) 0%, var(--panel-2) 100%);
  color: var(--text);
  cursor: pointer;
  transition: border-color .12s, color .12s;
}
button:hover:not(:disabled),
a.btn:hover,
.menu > summary:hover,
.menu[open] > summary { border-color: var(--blue); color: #fff; }
a.btn:hover { text-decoration: none; }
button:disabled { opacity: .4; cursor: not-allowed; }

button.link {
  border: 0;
  background: none;
  color: var(--blue);
  padding: 0;
  font-weight: 400;
  font-size: .9rem;
}
button.link:hover:not(:disabled) { color: #9ddfff; text-decoration: underline; }

button.danger {
  border-color: var(--danger-line);
  background: linear-gradient(180deg, #2c171b 0%, #241317 100%);
  color: #ff9c9c;
}
button.danger:hover:not(:disabled) { border-color: var(--danger); color: #ffc4c4; }

/* The two combined, for a destructive action that sits in a ROW OF LINKS rather
   than under a paragraph -- a card's actions, where a boxed button beside four
   plain words is the second button style the guide warns about. Written out
   because .link and .danger have equal specificity and .danger is second, so
   without this the box and the fill would come back.

   It is still red and still a form button. The rule that a destructive action is
   never an anchor is unchanged. */
button.link.danger {
  border: 0;
  background: none;
  padding: 0;
  font-weight: 400;
  font-size: .9rem;
  color: #ff9c9c;
}
button.link.danger:hover:not(:disabled) { color: var(--danger); text-decoration: underline; }

/* A one-line form that sits in a sentence rather than under it. */
form.inline { display: inline; margin: 0; }

/* The small print under a field: what the rule is, not that it was broken. */
.hint { color: var(--faint); font-size: .8rem; font-weight: 400; }
p.hint { margin: -.2rem 0 .2rem; }

/* The signup honeypot. Off-screen rather than display:none -- a bot that skips
   hidden fields is exactly the one this is trying to catch -- and moved from
   here rather than from a style attribute, which the CSP forbids. */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Something to read, so it is amber and nothing in it is amber-and-clickable. */
.banner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem;
  margin: 0 0 1.25rem;
  padding: .6rem .9rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text-dim);
  font-size: .9rem;
}

/* -- tables ---------------------------------------------------------------
   Rounded like the panels. admin.css gets there with a .tablewrap scroller
   around every table; the site does not take that -- its tables are four or
   five columns inside a 70rem column and are allowed to wrap rather than
   scroll -- so the corners are rounded on the corner cells instead, which needs
   border-collapse: separate. */

table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  margin: 1rem 0 1.25rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  font-size: .9rem;
}

th, td { padding: .5rem .75rem; text-align: left; border-bottom: 1px solid var(--line); }

thead th {
  background: var(--panel-2);
  color: var(--muted);
  font-size: .72rem;
  font-weight: 650;
  letter-spacing: .07em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-2);
}

tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: var(--panel-3); }

thead tr:first-child th:first-child { border-top-left-radius: 9px; }
thead tr:first-child th:last-child  { border-top-right-radius: 9px; }
tbody tr:last-child td:first-child  { border-bottom-left-radius: 9px; }
tbody tr:last-child td:last-child   { border-bottom-right-radius: 9px; }

.num { text-align: right; font-variant-numeric: tabular-nums; }
td.num, th.num { padding-right: 1rem; }
td.error { color: var(--danger); font-weight: 600; }

/* A column that answers yes or no for every row -- Telemetry, Video, GPS on the
   sessions table. Two glyphs and no third state: a blank cell in a column of
   ticks reads as "not loaded yet" rather than as "no", which is the one thing
   it must not do.

   Green and red rather than green and grey, and that is the exception it looks
   like: --danger carries failure everywhere else on the site. It is the right
   colour here because the question the column answers IS "what is missing", and
   a session that came across without its video is the answer somebody scanned
   the column for. Nothing is clickable in it, so the blue rule is untouched. */
th.mark, td.mark { width: 1%; text-align: center; padding-left: .55rem; padding-right: .55rem; }
/* The glyphs are ✓ and ✗ with U+FE0E after them, and both halves of that are
   load-bearing. Windows otherwise resolves a bare tick through Segoe UI Emoji,
   which paints its own colour and IGNORES the two rules below -- the column
   came out black in a font nobody chose. The variation selector in the markup
   demands the text presentation; this property says the same thing to browsers
   that implement it, and neither is a substitute for the other yet. */
td.mark span { font-variant-emoji: text; font-size: 1.05em; line-height: 1; }
td.mark .yes { color: var(--ok); font-weight: 700; }
td.mark .no  { color: var(--danger); font-weight: 700; }

/* Who may see a session -- the Visibility column, one icon per row and no word.
   Shaped like .mark above and coloured differently, in both directions.

   TWO COLOURS, because one is not enough. Drawn in the same ink the two shapes
   were too alike to tell apart down a column at a glance, which is the only
   thing this column has to do -- and the shape alone was carrying all of it.

   Green and AMBER, not green and red. --danger is failure everywhere else on
   the site, and it is right in `.mark` above precisely because the question
   there IS "what is missing". Neither answer is missing anything here: a driver
   who set a session private chose that, and painting it red would say they had
   lost something. Amber is the site's colour for a state worth noticing that is
   not a fault, which is exactly what private is. Not blue for either, whatever
   it would look like: blue is a link, and nothing in this column is clickable.

   The icon is a little larger than the em it is given elsewhere and has no
   right margin: .icon is sized to stand BESIDE a word, and this one stands
   alone in a cell. */
th.seen, td.seen { width: 1%; text-align: center; padding-left: .55rem; padding-right: .55rem; }
td.seen .icon { width: 1.15em; height: 1.15em; margin-right: 0; vertical-align: -.2em; }
td.seen.is-public .icon { color: var(--ok); }
td.seen.is-private .icon { color: var(--accent); }

/* -- the Direction column -------------------------------------------------
   CW or CCW, and on the rows where that is the opposite of how the matched
   track is usually driven, a mark after it.

   **The one icon on the site that FOLLOWS its word**, which is why the margin
   moves rather than being taken off: .icon's .3em belongs on the side the word
   is on, and here the word is on the left. Amber, the same ink `td.seen` uses
   for private -- a state worth noticing that is not a fault, which is exactly
   what running a circuit backwards is. */
th.way, td.way { width: 1%; white-space: nowrap; }
td.way .icon { margin-right: 0; margin-left: .35em; vertical-align: -.2em; color: var(--accent); }

/* -- the Laps column's junk mark -------------------------------------------
   On the rows that look like junk -- no laps and no sign it ever ran --
   an amber mark after the count, in the Laps cell because "no laps" is the
   heart of the claim. Laid out like td.way's: the icon follows its word, so
   the margin moves to the left. Amber for the usual reason -- a row worth a
   look, not a fault. This mark is the only icon a .num cell ever carries. */
td.num .icon { margin-right: 0; margin-left: .35em; vertical-align: -.2em; color: var(--accent); }

/* -- the Moments column ----------------------------------------------------
   What HAPPENED in the session: where the car stepped out, and whether one of
   the conditions this car is watched by held while it ran.

   THE ALERT MARK LIVES HERE and used to live in the Vehicle cell. It moved
   because this is the row's one answer to "did anything happen", and an alert
   is one of the things that did; beside a car's name it read as a fact about
   the car. `td.vehicle` therefore has no icon rule any more, and wants none.

   AN EMPTY CELL IS THE ORDINARY ANSWER. Nothing is drawn for a kind that did
   not happen -- not a zero, not a faint shape -- which is deliberately the
   opposite rule from the icon column earlier in the row, where a faint mark IS
   the answer "no video" and four shapes sit in four fixed seats. This column is
   a list of what happened, and most sessions have nothing in it.

   THREE COLOURS, AND THEY ARE A SCALE. Amber for an oversteer, the site's ink
   for a state worth noticing that is not a fault -- which is exactly what a
   catch you made is. Red for a spin, the one thing on this table that is not
   just worth noticing. And amber again on the alert triangle, which stays
   visibly a different family: the two moments are what the CAR did, and an
   alert is a line somebody wrote being crossed. */
th.moments, td.moments { width: 1%; white-space: nowrap; }
.moment {
  /* The badge is positioned against this, so it has to be a box. Inline-flex
     rather than inline-block: the shape and the number are one object and the
     number is pinned to its corner, not laid out after it. */
  position: relative;
  display: inline-flex;
  /* Wider than the cluster spacing elsewhere in the row, because the badge
     hangs off the right of its shape and the next mark has to clear it. */
  margin-left: .7em;
  vertical-align: -.2em;
}
.moment:first-child { margin-left: 0; }
/* Sized like the icon column's shapes and for its reason: .icon is drawn to
   stand beside a word and these stand alone. */
.moment .icon { width: 1.15em; height: 1.15em; margin: 0; vertical-align: 0; }
.moment-oversteer { color: var(--accent); }
.moment-spin      { color: var(--danger); }
/* The third member of the family, for the one place a mark is drawn outside a
   Moments cell: the filter menu's own legend. In the cell the triangle takes
   this amber from `td.moments > .icon` below, which cannot reach a panel. */
.moment-alert     { color: var(--accent); }
/* The count, ON the shape. A cell reading "wheel 3 skull 1" is four things to
   read; a driver scanning this column looks for the shape first and the number
   second, which is what a badge is for.

   **Top-right, and hanging mostly outside the box.** The bottom-right corner is
   the obvious seat and it is the wrong one for both of these shapes: the
   crossbones run diagonally into all four bottom corners and the wheel's stem
   and right spoke are down there, so a badge sitting on it took a bite out of
   the very thing being counted. Up here the wheel has the empty corner of a
   circle in a square, and the skull's cranium is rounded away from it.

   Plated in the page's own background rather than in the mark's colour: a
   filled amber pill next to a stroked amber wheel is two amber blobs, and the
   thing being counted stops being legible. The plate is what separates them,
   and the ring of it is what keeps the digit clear of the strokes it passes. */
.moment-n {
  position: absolute;
  right: -.5em;
  top: -.34em;
  min-width: .95em;
  padding: 0 .1em;
  border-radius: .6em;
  background: var(--bg);
  box-shadow: 0 0 0 .1em var(--bg);
  color: currentColor;
  font-size: .58rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  text-align: center;
}
/* The alert triangle, last in the cell and amber like everywhere else it
   appears. No margin rule of its own -- it is the one shape here that is a bare
   .icon rather than a .moment, so it keeps .icon's own right margin off and
   takes the cluster's spacing from this. */
td.moments > .icon {
  width: 1.15em;
  height: 1.15em;
  margin: 0 0 0 .35em;
  vertical-align: -.2em;
  color: var(--accent);
}
td.moments > .icon:first-child { margin-left: 0; }

/* -- the Track column ------------------------------------------------------
   The name the session carries, and on the rows where the GPS trace matched a
   different layout, a mark after it. The second icon on the site that follows
   its word, and it is laid out exactly like the first: the margin moves to the
   left because that is the side the word is on. Amber for the same reason --
   a state worth noticing that is not a fault. */
/* -- the icon column -------------------------------------------------------
   ONE cell where the sessions tables used to have four columns: Telemetry,
   Video, Visibility and Direction. Four headings and four columns of one glyph
   each is four widths set aside for answers nobody reads across; the questions
   they answer are all "what is this session", so they are read as a cluster and
   now sit as one.

   Every shape in it is drawn by assets/icons.js and carries its own sentence,
   in a <title> for a screen reader and a data-tip for a mouse — which is the
   same bend of "an icon never appears without its word" that the Visibility
   column already documents, and for the same reason: the alternative is four
   words repeated down every row of the table.

   THE COLUMN HAS NO HEADING AND DOES NOT SORT. A word over a cluster of four
   different facts would have to be a word for all of them, and there is none;
   sorting on it would have to pick one of the four, and there is no reason it
   should be any particular one. The filter bar is where these are asked for.

   COLOR IS THE SECOND HALF OF EACH SHAPE and is why the class is on the icon
   rather than on the cell: the cell holds four of them and they do not agree.
   Blue for a thing the session HAS and faint grey for a thing it has not —
   deliberately not the green-and-red of the `.mark` column this replaces,
   because a missing video is not a failure and a cluster with red in it reads
   as a row with something wrong. Green and amber stay on the two visibility
   shapes, which is the pairing `td.seen` sets out at length. Amber and purple
   for the two directions: the second is the viewer's own --purple, so that
   "counter-clockwise" is one color across the product.

   Nothing here is clickable, so no part of it is blue-as-a-link — see the
   note at the top of this file. */
th.icons, td.icons { width: 1%; white-space: nowrap; padding-left: .55rem; padding-right: .55rem; }
/* .icon is sized to stand BESIDE a word and carries a right margin for it.
   Nothing in this cell has a word, so the margin comes off and the spacing is
   set here — one distance for the whole cluster, on the left so the first
   shape still sits against the cell's own padding. */
td.icons .icon {
  width: 1.15em;
  height: 1.15em;
  margin: 0 0 0 .34em;
  vertical-align: -.2em;
}
td.icons .icon:first-child { margin-left: 0; }

/* Has it / has it not.

   The telemetry mark is a shape this stylesheet colours. THE VIDEO MARK IS NOT:
   it is a full-colour raster, /assets/video.png, and it is the one icon on the
   site that this file has no say over. Two blue strokes side by side read as a
   single smudge, and telemetry and video are the two facts a driver scans a row
   for -- so the second one stopped being a stroke at all. Different medium,
   different palette, nothing left to blend with. See assets/icons.js, which is
   where a name becomes either an <svg> or an <img>.

   Absent is one answer with nothing to tell apart, so both marks share --faint;
   for the raster that is a filter rather than a colour, below. It is
   deliberately NOT red -- a missing video is not a failure, and this column has
   no failures in it. */
td.icons .mark-telemetry { color: var(--blue); }
td.icons .is-off         { color: var(--faint); }
/* The raster, drained rather than recoloured: `color` cannot reach inside a
   PNG. Grayscale then dimmed lands it in the same register as --faint, which is
   what "no video" has to read as beside the stroked marks around it. */
td.icons img.icon.is-off { filter: grayscale(1) brightness(1.4); opacity: .34; }
/* `mark-video` has no rule here, and that is not an oversight: the raster
   brings its own colour and there is nothing for this file to say about it. The
   class stays because it is the NAME of the mark -- what the style guide points
   at, and what finds a row's video answer in the DOM. */
/* Who may see it — the two colours `td.seen` chose and the reasoning there. */
td.icons .is-public  { color: var(--ok); }
td.icons .is-private { color: var(--accent); }
/* Which way round it went. Nothing is drawn where nothing measured it: a
   cluster of three where the row beneath has four is what says "no answer",
   and an em dash in a cell of shapes would be a fifth kind of thing. */
td.icons .dir-cw  { color: var(--accent-2); }
td.icons .dir-ccw { color: var(--purple); }

table.dense { font-size: .85rem; }
table.dense th, table.dense td { padding: .35rem .6rem; }

/* -- a table you can tick -------------------------------------------------
   The tick column, and the bar of actions that appears once something is in it.
   Both are shared with the desktop console, which draws the same sessions table
   -- see the note at the top of win/console/console.css. */

th.pick, td.pick { width: 1%; padding-right: .25rem; text-align: center; }
/* A bare label is laid out as a grid with a gap above it, which is right for a
   field and wrong for a checkbox that is the whole cell. */
td.pick label, th.pick label { display: inline; padding: 0; border: 0; background: none; }

/* The whole row opens its session -- see assets/session-row.js. Every row is
   already lit under the pointer by the rule above, so the cursor is the only
   thing that separates a row with something to open from one without, and only
   a row that has one carries it. Word for word the rule in
   win/console/console.css, because it is the same table. */
tr.can-open { cursor: pointer; }

/* -- the sessions table is the page ----------------------------------------
   Eleven columns and a ⋮, where the note over `table` above assumed four or
   five. At 70rem this table spent its width on wrapping: a track name over two
   lines, "Fastest lap" over two more, and a row two and a half lines tall that
   has to be read rather than scanned.

   The tighter cell padding is the desktop app's answer to the same table --
   `#table th, #table td` in win/console/console.css, which has carried that
   number since the page was drawn.

   `:has()` and not a class the page sets: the condition IS "this page draws the
   sessions table", the same reasoning `.head:has(.bulk)` and the ticked-row
   tint already follow. */
main:has(table[data-sessions]) { max-width: 90rem; }

/* MY SESSIONS HAS NO CEILING AT ALL -- the whole window, like the desktop app's
   copy of this table, which has always been drawn in a frame that IS the
   window. It is the one page where the table is not something on the page, it
   is the page: a driver goes there to narrow a season, sort it and work on
   what is left, and every rem the shell keeps back is a rem the three name
   columns spend wrapping.

   The condition is `form[data-bulk]`, which is to say "the table you WORK on".
   The dashboard's glance sits among tiles and the activity panel, and the
   search pages are a stranger's list to read -- both stay inside the site's
   shell at 90rem, because on those the table is one thing among others. */
main:has(form[data-bulk] table[data-sessions]) { max-width: none; }
table[data-sessions] th,
table[data-sessions] td { padding: .35rem .6rem; }

/* Every column whose contents have a FIXED SHAPE holds its line: a heading, a
   stamp, a tick, a lap time, a count, a size, a length. Not one of them is ever
   too long for its column, so a break in any of them is the table running out
   of room somewhere else -- and "Fastest" over "lap" costs every row on the
   page a second line.

   The three name columns are left to wrap, and they are the only ones that can:
   a car may be called anything, and at a window too narrow to hold them a
   folded name is better than either a cut one or a table that has to be
   scrolled sideways to be read. NO `overflow: hidden` anywhere here -- the ⋮
   menu drops out of the last cell, and a clipped cell would cut it off. */
table[data-sessions] th,
table[data-sessions] td.pick,
table[data-sessions] td.when,
table[data-sessions] td.mark,
table[data-sessions] td.seen,
table[data-sessions] td.num,
table[data-sessions] td.menu-cell { white-space: nowrap; }

/* The session number takes the width of its digits and not one pixel more.
   `width: 1%` on an auto-layout table is "give me my content and hand the rest
   away", which is what `th.menu-cell` below already does for the column of one
   glyph -- and it has to be said, because `.num` on this site does not claim a
   width the way the admin console's copy does.

   The sort button gives back the 1rem `th.num` spends lining a WORD up over
   its column: the heading here is a single `#`, so that padding would be a
   third of the column's width. What is left is room for the caret. */
table[data-sessions] th.sid,
table[data-sessions] td.sid { width: 1%; }
table[data-sessions] th.sid .sortcol { padding-left: .35rem; padding-right: .6rem; }

/* -- one row's own actions --------------------------------------------------
   The ⋮ at the end of a session row: the same `details.menu` the header's
   account menu is, closed by the same assets/menu.js, in a cell one glyph wide.
   The tick column is every session's actions; this is one session's, and asking
   somebody to tick a row to correct the name they are looking at is asking them
   to aim at a bar that just appeared somewhere else. */

th.menu-cell, td.menu-cell { width: 1%; padding-left: .25rem; text-align: right; }
/* EVERY PART OF THE BUTTON COMES OFF. `.menu > summary` is the site's button —
   the header's account menu is a control that happens to be a summary — and
   inherited whole it draws a bordered, gradient-filled pill around one glyph in
   a table cell. This rule sits below that one, which is the only reason it
   wins; see the note at the top of viewer-dev/test_css.mjs on what that costs
   when it is the other way round.

   The glyph is written `&#8942;&#xFE0E;` and `font-variant-emoji: text` says the
   same thing to browsers that implement it — the pair the tick column above
   documents at length. A ⋮ is not an emoji on this machine's fonts, but which
   font a bare glyph resolves through is the platform's business, and this is
   the one column where the answer being wrong would be a colored icon. */
.row-menu > summary {
  padding: .1rem .3rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-dim);
  font-variant-emoji: text;
  font-size: 1.15rem;
  line-height: 1;
}
.row-menu > summary:hover { color: var(--text); background: var(--panel-2); }
/* The caret `.menu > summary::after` draws is the account menu's, where the
   summary is a word that needs one. A ⋮ is already the affordance. */
.row-menu > summary::after { content: none; }
/* Left of the ⋮, not right: this is the last column, and a panel anchored to
   the right edge of the last cell would hang off the table. */
.row-menu .menu-panel { min-width: 11rem; }

/* A menu row that DOES something rather than going somewhere -- the same shape
   as `.menu-nav a`, because the panel it stands in is the same panel and a
   button in it must not read as a different kind of thing. */
.menu-item {
  display: block;
  width: 100%;
  padding: .4rem .55rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: .92rem;
  font-weight: 500;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
.menu-item:hover { background: var(--panel-2); color: var(--text); }

/* A heading with its controls on the same line. */
.head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .75rem 1rem;
  margin: 1.6rem 0 .5rem;
}
.head > h1, .head > h2 { margin: 0; }

/* HIDDEN OUTRIGHT until something is ticked, rather than shown with every
   button greyed. A row of dead controls is a row of things to read past on
   every visit, and one of them is Delete. */
/* The one-press answer to every amber Track mark on the page. Its own line
   above the table, NOT a bar: it is not a selection, and it must exist on the
   dashboard, which has no form and no tick column. The desktop app draws the
   same line over its own copy of the table.

   The 1rem on top is load-bearing: .filters above it ends in a NEGATIVE
   bottom margin that tucks the table up against the bar, and with no top
   margin of its own this line was drawn overlapping the bar's border.

   AND IT IS AMBER -- the second deliberate bend of "nothing amber is
   clickable", after the visibility glyphs' bend of "an icon never appears
   alone". This is the one control whose whole job is answering the amber
   marks in the Track column, so it wears their color the way Delete wears
   red: the pairing is the meaning, and a blue button here would read as
   navigation. Anything else amber remains something you read. */
.fix-all { margin: 1rem 0 .65rem; }
.fix-all button {
  border-color: #7a5a2b;
  background: linear-gradient(180deg, #2c2417 0%, #241d13 100%);
  color: var(--accent);
}
.fix-all button:hover:not(:disabled) { border-color: var(--accent); color: #ffd08e; }

/* An author `display` beats the browser's own [hidden] rule -- the .tooltip
   lesson, relearned here: the bar wore `hidden` faithfully and showed anyway,
   five dead controls and all, on every visit. */
.bulk[hidden] { display: none; }

.bulk {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5rem;
  padding: .5rem .6rem;
  margin: .5rem 0 .75rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
}
.bulk .count { color: var(--muted); font-size: .85rem; margin-right: .25rem; }

/* THE SAME BAR, STANDING IN A HEADING, and that is where it belongs over a
   table somebody is ticking their way down.

   A bar that appears in the flow pushes every row down the instant a box is
   ticked, so the row the pointer is over moves out from under it -- and ticking
   a second one means aiming at a table that just jumped. A heading row is
   already exactly as tall as the buttons in it, so appearing here costs no
   height at all. Its own chrome comes off for the same reason: a border and
   padding would make the row taller the moment something was ticked. */
.head .bulk {
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
}
/* AND IT STAYS PUT. A bar of actions over a table hundreds of rows long is
   useless at the top of it: tick a session forty rows down and the only thing
   that can act on it has scrolled off the screen.

   `:has(.bulk)` and not a class of its own, because that IS the condition --
   a heading carrying actions has to stay, and one carrying only a heading has
   nothing to stay for. The Status page has three `.head`s with no bar between
   them, and three headings glued to the top of a scrolling page would be three
   things covering what you came to read.

   It needs an opaque background of its own: sticky takes it out of the flow's
   painting order but not out of the page, so rows pass underneath it. --bg and
   not --panel, because what it has to hide is the page. */
.head:has(.bulk) {
  position: sticky;
  top: 0;
  /* Over the table and under the tooltips, which leave the page entirely. */
  z-index: 2;
  padding: .75rem 0 .6rem;
  margin: 1rem 0 0;
  background: var(--bg);
}

/* Pushes what follows it to the right. An element rather than a rule on the
   button, because which button ends up last is a layout decision. */
.bulk .spacer, .head .spacer { margin-left: auto; }

/* -- a sortable header ----------------------------------------------------
   The words in a `<th data-sort>` are wrapped in a button by
   assets/session-table.js. A button and not a click handler on the cell: this
   reorders the table, so it is a control, and one that Tab cannot reach is a
   control only a mouse can see.

   It is NOT a second button style. Everything the site's button brings -- the
   border, the fill, the padding -- is taken straight back off, because what is
   wanted here is the header exactly as it already looked, that happens to be
   pressable. */
th[data-sort] { padding: 0; }
.sortcol {
  display: flex;
  align-items: center;
  gap: .35em;
  width: 100%;
  padding: .5rem .75rem;
  border: 0;
  border-radius: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  text-align: inherit;
}
.sortcol:hover:not(:disabled) { border-color: transparent; color: var(--text); }
th.num .sortcol { justify-content: flex-end; padding-right: 1rem; }
th.mark .sortcol, th.seen .sortcol, th.has .sortcol, th.avail .sortcol { justify-content: center; padding-left: .55rem; padding-right: .55rem; }

/* The caret, and it is drawn off `aria-sort` rather than off a class -- so the
   arrow on screen and the state a screen reader announces are the same fact and
   cannot come apart. Faint until the column is the one sorting, so a header row
   is not eight arrows competing with eight words. */
.caret::before { content: '\25B2'; font-size: .7em; opacity: 0; }
.sortcol:hover .caret::before { opacity: .35; }
th[aria-sort] .sortcol { color: var(--text); }
th[aria-sort] .caret::before { opacity: 1; color: var(--blue); }
th[aria-sort="descending"] .caret::before { content: '\25BC'; }

/* -- a row that is ticked -------------------------------------------------
   Selected, not hovered: the pointer moves on and this has to stay. Tinted blue
   because blue is what you act on, and the bulk bar above the table is the
   action -- so the rows it is about and the bar itself read as one thing.

   `:has()` and no class, on purpose. The checkbox IS the state; a class beside
   it would be a second copy of the same fact, kept in step by hand on a website
   that re-renders and in an app that redraws its tbody every second. */
tbody tr:has(input[type="checkbox"]:checked) > td {
  background: color-mix(in srgb, var(--blue) 13%, var(--panel));
}
tbody tr:has(input[type="checkbox"]:checked):hover > td {
  background: color-mix(in srgb, var(--blue) 19%, var(--panel));
}

/* -- the filter bar --------------------------------------------------------
   Above a table that answers "which of my sessions", built by
   assets/session-table.js so the website and the desktop console cannot drift
   apart on what may be filtered or how.

   A row of the site's own controls and nothing invented: two native date
   inputs, three selects, four checkboxes, and buttons. The date presets are
   ordinary buttons rather than a smaller pill, because a second button style is
   the thing the guide's first rule forbids. */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .6rem 1.1rem;
  padding: .75rem .85rem;
  margin: 1rem 0 -.25rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
}

.filters .filter { display: grid; gap: .3rem; }
/* A `display` of its own beats the browser's [hidden], so a bar that takes one
   control off itself needs this to be told. Only the Add Session picker does:
   see showFilter() in assets/session-table.js. */
.filters .filter[hidden] { display: none; }
/* The same word, in the same gray, as the label over a form field -- because
   that is what it is. */
.filters .label { font-size: .72rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
.filters select { min-width: 11rem; }
.filters button { padding: .4rem .7rem; font-size: .85rem; }

/* Date is three things on one line: from, to, and the four shortcuts. They wrap
   together as a group rather than each on their own, so a narrow window never
   leaves "This month" sitting under a driver's name. */
.filters .filter-date { grid-template-columns: auto auto; grid-auto-flow: column; align-items: center; gap: .3rem .5rem; }
.filters .filter-date .label { grid-column: 1 / -1; }
.filters .dates { display: flex; align-items: center; gap: .35rem; }
.filters .dates input { width: 9.5rem; }
.filters .presets { display: flex; flex-wrap: wrap; gap: .35rem; }

/* The yes/no ticks, laid out as a block of labels rather than one per column:
   four of them side by side would be wider than everything else in the bar put
   together. */
.filters .filter-flags {
  grid-template-columns: repeat(2, auto);
  gap: .2rem 1rem;
  align-self: center;
}
.filters .filter-flags label { font-size: .84rem; color: var(--text-dim); }

/* Clear sits at the end and appears only once something is set. */
.filters .filter-clear { margin-left: auto; align-self: center; }

/* A MENU OF TICKS -- assets/tick-menu.js. The site's own `details.menu`
   button, one more block in the bar, opening a panel that is a column of
   checkboxes. There are two: the Columns menu at the bar's end, and the
   Moments filter beside the ticks.

   A panel per row makes these the tallest panels on the site, and the bar they
   hang off wraps down a narrow window -- so that module caps their height to
   whatever is left below the button, and this is what turns the cap into a
   scroll instead of a clip. The number is a measurement and lives there; the
   behaviour is here. */
.filters .filter-columns { align-self: center; }
.filters .filter-moments { align-self: center; }
/* THE MOMENTS BUTTON IS THE SMALL ONE, and it is the only control on the site
   drawn under the site's button size. Two menus sit on this bar and they are
   not the same weight of thing: Columns arranges the table you are reading, and
   this narrows what is in it -- one more filter, standing in a row of filters,
   beside a block of checkboxes set in .84rem. At the site's full button size it
   was the loudest thing in a bar of quiet controls and read as an action rather
   than a setting.

   Small, not a NEW style: this is the size the same control is already drawn at
   in the viewer's copy of this bar (`.tick-menu > summary` in viewer.css),
   where the whole bar sits at that density. The style guide's "no smaller
   button" rule stands for everything that is a button -- the date presets are
   still the site's own button at the site's own size. */
.filters .filter-moments > .menu > summary {
  gap: .25rem;
  /* Half the height of the site's button, which is the size this had to be
     for the hierarchy to read at a glance rather than on measurement. */
  padding: .14rem .4rem;
  font-size: .75rem;
  border-radius: 5px;
  border-color: var(--line);
  /* Flat, where the site's button is a gradient: at this size the gradient is
     a smudge rather than a shape. */
  background: var(--panel-2);
  color: var(--muted);
}
/* Its own hover, because the rule above outranks the site button's. */
.filters .filter-moments > .menu > summary:hover,
.filters .filter-moments > .menu[open] > summary {
  border-color: var(--blue);
  color: var(--text);
}
.tick-panel { display: grid; gap: .1rem; overflow-y: auto; }
.tick-panel .tick-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .3rem .55rem;
  border-radius: 6px;
  font-size: .9rem;
  color: var(--text-dim);
  cursor: pointer;
}
.tick-panel .tick-row:hover { background: var(--panel-2); color: var(--text); }
/* A row that names a mark shows the mark, in the colour the Moments column
   draws it -- so the panel is that column's legend as well as its filter. The
   size is the cell's, and the margin goes because the row's own gap is already
   the space between a shape and its word. */
.tick-panel .tick-row .icon { width: 1.15em; height: 1.15em; margin: 0; }

@media (max-width: 46rem) {
  .filters .filter-date { grid-auto-flow: row; grid-template-columns: 1fr; }
  .filters .filter-clear { margin-left: 0; }
  .filters select { min-width: 0; width: 100%; }
}

/* -- facts ----------------------------------------------------------------- */

.facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0;
  margin: 1rem 0 1.5rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  overflow: hidden;
}
.facts dt {
  padding: .42rem 1rem .42rem .95rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--muted);
  font-size: .8rem;
  font-weight: 600;
}
.facts dd {
  margin: 0;
  padding: .42rem .95rem;
  border-bottom: 1px solid var(--line);
  font-size: .9rem;
  overflow-wrap: anywhere;
}
.facts dt:last-of-type, .facts dd:last-of-type { border-bottom: none; }
.facts dd .muted { font-size: .85em; }

/* -- tiles ----------------------------------------------------------------
   The numbers are amber and the links inside a tile are blue, which is the
   whole rule in one component. */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr));
  gap: 12px;
  margin: 1rem 0 1.5rem;
}

.tile {
  position: relative;
  padding: 14px 16px 15px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: linear-gradient(180deg, var(--panel-2) 0%, var(--panel) 100%);
  overflow: hidden;
}
.tile::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, var(--blue), transparent 70%);
  opacity: .55;
}
.tile .k {
  color: var(--muted);
  font-size: .73rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.tile .v {
  margin-top: 4px;
  color: var(--accent);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.tile .s {
  margin-top: 5px;
  color: var(--faint);
  font-size: .78rem;
  line-height: 1.45;
}
.tile .s a { color: var(--blue); }

/* The dashboard's tiles: the same tile, in a grid that fits four of them on one
   line beside two tables. /account is an inventory and its tiles set a column
   width from the longest of eight; this is a glance under an activity panel,
   and four numbers wrapping onto two rows is the glance costing a scroll. The
   number comes down with the column, or a 13.5rem figure in a 10rem box is a
   tile that reads as broken. */
.tiles.mini { grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); gap: 10px; margin: 1rem 0 1.25rem; }
.tiles.mini .tile { padding: 11px 13px 12px; }
.tiles.mini .v { font-size: 1.35rem; }

.tile.warn { border-color: var(--danger-line); background: linear-gradient(180deg, #20161a 0%, var(--panel) 100%); }
.tile.warn::before { background: linear-gradient(90deg, var(--danger), transparent 70%); }
.tile.warn .v { color: var(--danger); }

/* -- panels ---------------------------------------------------------------- */

.panel {
  margin: 1rem 0 1.25rem;
  padding: 1rem 1.15rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
}
.panel > :first-child { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }
.panel h3 { margin-top: 0; }
.panel h4 { margin: 1.25rem 0 .25rem; }

.panel.danger { border-color: var(--danger-line); background: var(--danger-bg); }
.panel.danger h3 { color: #ffb3b3; }

/* -- tags ------------------------------------------------------------------ */

.tag {
  display: inline-block;
  padding: .05rem .55rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--text-dim);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  vertical-align: middle;
  white-space: nowrap;
}
.tag.bad { border-color: var(--danger-line); background: var(--danger-bg); color: #ff9c9c; }
h1 .tag { vertical-align: .18em; }

/* -- icons -----------------------------------------------------------------
   Inline SVG, one em tall, drawn in currentColor so an icon inside a link is
   blue and one inside a danger button is red without a rule of its own. The set
   is View::icon(); nothing here loads an icon font or a sprite sheet, both of
   which would be a second request for five shapes.

   **An icon never appears on its own.** It sits beside the word it decorates --
   the style guide's rule against icon-only controls is unchanged, and this is
   what makes it affordable to keep: the word is the control, the icon is the
   glance. */

.icon {
  width: 1em;
  height: 1em;
  flex: none;
  /* The icon precedes its word, always, so the space between the two belongs
     here rather than to every caller. */
  margin-right: .3em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -.125em;
}

/* -- a page of results -----------------------------------------------------
   Under a table that holds one page of a larger answer: how much of it you are
   looking at, and one press each way.

   THE COUNT IS THE POINT, more than the buttons are. A search that matched
   eight hundred sessions and one that matched eight look identical without it,
   and the second is the one worth knowing about -- so the line is drawn even
   when there is a single page and nothing to press.

   Previous and Next are GONE at the ends rather than greyed, which is the rule
   the bulk bar states at length: a permanently dead control is one more thing
   to read past on every visit. That is also why they are not laid out in fixed
   positions -- one button on its own sits next to the count, and the count is
   what anybody is reading. */
/* An author `display` beats the browser's own [hidden] rule -- the .tooltip
   lesson that `.bulk[hidden]` above records, relearned here: an empty search
   went on saying "Showing 1-3 of 812" under the sentence that said nothing
   matched. */
.pager[hidden] { display: none; }

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin: .85rem 0 1.5rem;
}
.pager .count { color: var(--muted); font-size: .85rem; font-variant-numeric: tabular-nums; }
.pager button { padding: .4rem .8rem; font-size: .85rem; }
/* The buttons go to the right of the count on a wide window and under it on a
   narrow one, which is what auto on the first of them does. */
.pager button:first-of-type { margin-left: auto; }
@media (max-width: 520px) {
  .pager button:first-of-type { margin-left: 0; }
}

/* -- Compare ---------------------------------------------------------------
   The one press the search pages offer: open what you ticked, together, in the
   telemetry viewer. A link and not a button, because the viewer is a place and
   because these pages post nothing -- there is no form on either of them.

   **GOLD, and it is the THIRD documented bend of "nothing amber is
   clickable"**, after the visibility glyphs' bend of "an icon never appears
   alone" and `.fix-all`. It is the same bend as that one and for the same
   reason: this is the page's single outcome, it appears only once something has
   been chosen, and a blue button in a header full of blue links would read as
   one more place to go. Anything else amber on this site remains something you
   read.

   It is modeled on `.fix-all` line for line, so the two gold controls on the
   site are one control drawn twice. */
.btn.compare {
  border-color: #7a5a2b;
  background: linear-gradient(180deg, #2c2417 0%, #241d13 100%);
  color: var(--accent);
}
.btn.compare:hover { border-color: var(--accent); color: #ffd08e; text-decoration: none; }

/* -- cards -----------------------------------------------------------------
   A grid of things with a picture. The tiles above are a number and a sentence;
   this is for a row that has something to LOOK at -- a rendered clip -- where a
   table cell saying "8.5 MB" is not what anybody came for.

   Same auto-fill grid as .tiles, and for the same reason: a column count set by
   hand is a column count that is wrong on somebody's screen. */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 14px;
  margin: 1rem 0 1.5rem;
}

/* The dashboard's clips: the same card, narrower, so four fit across where
   /clips fits three. Nothing is taken off it but the width -- a card is a
   picture and a name, and a picture that has to be squinted at is not a
   glance. The cards there carry Download, Share, Edit and Delete; these carry
   none, which is a difference in the markup and not in this stylesheet. */
.cards.mini { grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 12px; }

.card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  /* The media is flush to the top edge, so it has to be clipped by the radius
     rather than sit square inside it. */
  overflow: hidden;
  transition: border-color .12s;
}
.card:hover, .card:focus-within { border-color: var(--line-2); }

/* The picture's box exists whether or not there is a picture: a card with
   nothing rendered yet must be the same size as its neighbours, or the grid
   reflows every time an export finishes. */
.card-media {
  aspect-ratio: 16 / 9;
  background: #000;
  border-bottom: 1px solid var(--line);
}
.card-media video {
  display: block;
  width: 100%;
  height: 100%;
  /* contain, not cover: a clip is 16:9 and so is this box, but a source that is
     not must be shown whole rather than cropped. */
  object-fit: contain;
}
/* Nothing to play — the state, centred in the box the video would have filled. */
.card-media .empty {
  display: flex;
  height: 100%;
  align-items: center;
  justify-content: center;
  background: var(--panel-2);
}

.card-body { display: grid; gap: .25rem; padding: .7rem .85rem .75rem; }
.card-title { font-size: .95rem; font-weight: 650; color: var(--text); }

/* Size, length, date: facts about the file, read across rather than down, so
   they are separated by dots instead of being four more columns. */
.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  color: var(--muted);
  font-size: .8rem;
  font-variant-numeric: tabular-nums;
}
.card-meta > * + *::before { content: "·"; margin-right: .5rem; color: var(--faint); }

.card-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem;
  margin-top: .45rem;
  font-size: .85rem;
}
/* Every action is a word with an icon in front of it. inline-flex so the two
   never wrap apart from each other, and so anchors and form buttons sit on one
   baseline without either being nudged. */
.card-actions a, .card-actions button {
  display: inline-flex;
  align-items: center;
}
.card-actions form.inline { display: inline-flex; }

/* Shown when the card is under the pointer or holds focus. Opacity rather than
   `display: none`, deliberately: a hidden-until-hover control that leaves the
   tab order is a control a keyboard user can never reach, and :focus-within
   means tabbing to it is what reveals it. A pointer cannot click one before
   seeing it either -- reaching the button means hovering the card. */
.card .on-hover { opacity: 0; transition: opacity .12s; }
.card:hover .on-hover, .card:focus-within .on-hover { opacity: 1; }
/* A touch screen has no hover, so there would be no way to get at these at all. */
@media (hover: none) {
  .card .on-hover { opacity: 1; }
}

/* -- tooltips --------------------------------------------------------------
   The one thing on the site drawn for a script rather than for markup: there is
   exactly one .tooltip element, it is a child of <body>, and assets/tip.js
   moves it. See that file for why it cannot be a `::after` on the trigger — in
   one sentence, a tooltip drawn inside its trigger is clipped by every ancestor
   with `overflow: hidden`.

   `title` is not the component. The browser waits about a second before showing
   one, which is fine for an afterthought and wrong for a hint somebody is
   hunting for. `data-tip` is what a page writes; nothing here selects on it,
   because the trigger is not what is drawn.

   Nothing about the position is in here except the two rules that make one
   possible -- tip.js writes `left` and `top` and nothing else. */

.tooltip {
  position: fixed;
  z-index: 60;
  /* Long enough for a sentence, short enough that one still reads as a hint
     rather than as a paragraph that happens to float. */
  max-width: 22rem;
  /* A hint is one sentence and stays one; the alert mark on a sessions row is
     the exception, and it is a LIST — one condition per line. `pre-line` keeps
     those newlines and still collapses everything else, so every single-line
     tip on the site is drawn exactly as it was. */
  white-space: pre-line;
  padding: .3rem .55rem;
  border: 1px solid var(--line-2);
  border-radius: 6px;
  background: var(--panel-3);
  color: var(--text);
  font-size: .8rem;
  line-height: 1.35;
  /* It follows the pointer's target, so it must never become the target. */
  pointer-events: none;
  box-shadow: 0 3px 12px rgb(0 0 0 / .45);
}
/* An author `display` here would beat the browser's own [hidden] rule, so the
   hidden state is spelled out rather than left to it.

   `!important`, and it is load-bearing. `.tooltip[hidden]` is one class and one
   attribute; `.tooltip.drawn` below is two classes -- the SAME specificity, and
   it comes later, so its `display: flex` won and a drawn tooltip could not be
   hidden at all. hide() set the attribute, the box stayed on screen, and the
   only thing that ever cleared it was the next PLAIN tooltip, which resets the
   class. The lap panel's moments hint is the only drawn one in the product, so
   it was the only tooltip in either app that behaved that way: hovering a lap
   with no moments left the lap below's oversteers hanging over it, and moving
   the mouse away is what appeared to fix it. Found 2026-09-08. Specificity is
   not the tool here -- a fourth variant added under this one would beat any
   count this rule could hold -- so the hidden state wins outright. */
.tooltip[hidden] { display: none !important; }

/* -- a tooltip with pictures in it -----------------------------------------
   `.drawn` is the class assets/tip.js puts on the box when a trigger hands it
   NODES instead of a sentence, and it comes off again for the next trigger.

   `pre-line` above is for a sentence with newlines in it and is wrong for rows
   that are already elements — it would add a blank line between each pair.

   Nothing on the SITE draws one yet; the telemetry viewer's lap panel does, and
   these rules are here for the reason every other `.tooltip` rule is in all
   three stylesheets: the component is one, and the style guide draws it out of
   this file. See the Moments column. */
.tooltip.drawn {
  display: flex;
  flex-direction: column;
  gap: 2px;
  white-space: nowrap;
}
.tip-moment { display: flex; align-items: center; gap: .4em; }
.tip-moment .icon { width: 1.05em; height: 1.05em; margin: 0; }

/* -- the state tooltip -----------------------------------------------------
   The other kind, and the difference is who asked for it. `.tooltip` above is a
   hint somebody hunted for: it follows the pointer, it is small, and it is gone
   the moment they look away. This one is a STATE — it stands over the control
   that is in that state for as long as the state lasts, whatever the pointer is
   doing, and it says the thing the control cannot say for itself. The telemetry
   viewer's record button is the case it was built for: marking a clip is two
   presses of one button, and nothing said that the second one ends the segment.

   So it is bigger (a hint is .8rem, this is a sentence somebody has to read
   from across the page), it carries a colored edge, and it has an icon.

   It is anchored IN THE PAGE, unlike .tooltip: `.tip-anchor` is a positioned
   box around the control, and the tip is absolute inside it. That costs the
   escape from `overflow: hidden` that .tooltip's <body> child buys — so put one
   on a control whose neighborhood does not clip. What it buys back is that the
   tip moves with the control for free, and that it is still drawn inside a
   fullscreen element, where a <body> child is not painted at all.

   Toggled with the `hidden` attribute, hence the rule below it: an author
   `display: flex` beats the browser's own [hidden], and without that line the
   sentence never goes away. */

.tip-anchor { position: relative; display: inline-flex; align-items: center; }

.state-tip {
  position: absolute;
  bottom: calc(100% + 9px);
  left: -2px;
  z-index: 55;
  display: flex;
  align-items: flex-start;
  gap: .45rem;
  width: max-content;
  max-width: min(30rem, 68vw);
  padding: .5rem .65rem;
  border: 1px solid var(--line-2);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  background: var(--panel-3);
  color: var(--text);
  font-size: .9rem;
  line-height: 1.35;
  box-shadow: 0 6px 18px rgb(0 0 0 / .5);
  /* It hangs over whatever is above the control. Nothing loses a click to it. */
  pointer-events: none;
}
.state-tip[hidden] { display: none; }
.state-tip svg {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  margin-top: 1px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The pointer, drawn on the ANCHOR so `left: 50%` is half the control rather
   than half a sentence — the tip is a fixed distance above it and a variable
   width beside it. The tip covers its top half, so what shows is the triangle.
   `:has` because the state is one element's and the rule is another's. */
.tip-anchor:has(> .state-tip:not([hidden]))::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: calc(100% + 5px);
  width: 8px;
  height: 8px;
  background: var(--panel-3);
  border-right: 1px solid var(--line-2);
  border-bottom: 1px solid var(--line-2);
  transform: translateX(-50%) rotate(45deg);
  pointer-events: none;
}

/* -- toasts ----------------------------------------------------------------
   One line at the top of the window for five seconds, built and removed by
   assets/toast.js. For a message that belongs to a GESTURE rather than to a
   place on the page — the answer to "why did nothing happen when I pressed
   that", said where the eye already is.

   THE WHOLE BOX IS COLORED. It started out as .state-tip's twin, a panel with a
   colored edge, and vanished into a page that is already panels with edges: a
   line that appears for five seconds and was never asked for has one job, and
   blending in is the opposite of it. These are the only solid color BOXES on
   the site — `.panel.danger` and `.tag.bad` are dark with a colored rim, which
   is a state you are looking at rather than a thing that just arrived.

   FIVE: three states, an announcement and a neutral. Red is a failure, green is
   a success, amber is a fact — the site's own three, not colors picked for
   toasts. Red is the default and the unclassed one, because a refusal is what
   this was built for and because a message whose kind nobody set is likelier to
   be bad news than good.

   `.info` IS THE ONE BLUE THING ON THIS SITE YOU DO NOT CLICK, and that is a
   deliberate exception rather than a slip — see the Colour section of the style
   guide, which names it. It costs a little of "blue is what you click", and the
   reason it is affordable here and nowhere else is that a toast cannot mislead
   anybody about it: the box is `pointer-events: none`, the only control in it
   is the ✕, and it is gone in five seconds. **A second blue exception anywhere
   means the rule has gone, not that the rule has another exception.**

   `.plain` is black and is not a state either — it is the absence of one, for a
   message that is simply true. It and `.info` differ by volume rather than by
   meaning: blue announces, black just says.

   The four colored ones are the DARK end of their hue, not the token: --ok,
   --accent and --blue are picked to be read against a dark panel, and white
   text on any of them is illegible. All four carry white at better than 5:1.

   The black one is the one that has to work for its edge. Every other surface
   here is a dark panel with a faint border, and the objection that made this
   component a solid box in the first place was that it vanished into them. So
   .plain takes a border lighter than any other on the site — that rim, not the
   fill, is what keeps it from reading as one more panel. It was briefly darker
   than --bg itself, which read as a hole in the page rather than as a message.

   The color is never the only difference. assets/toast.js draws a different
   glyph for each — see KINDS in that file — so which of the three this is does
   not depend on telling green from red. `--toast-x` is a custom property rather
   than three `.toast.ok .toast-x` rules, so that `.toast-x:hover` still wins
   inside a variant instead of losing to a longer selector.

   There is no hidden state to get wrong: toast.js appends the element to speak
   and removes it to stop, which is also what replays the entrance.

   z-index 70 clears the tooltip's 60, which is the highest thing in this file.
   A modal <dialog> still covers it, and correctly: the browser's top layer is
   above every z-index there is, and a question somebody has to answer outranks
   a line that is already leaving. */

.toast {
  position: fixed;
  top: 14px;
  left: 50%;
  z-index: 70;
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  max-width: min(34rem, 92vw);
  padding: .55rem .6rem .55rem .75rem;
  border: 1px solid var(--toast-line);
  border-radius: 8px;
  background: var(--toast-bg);
  color: #fff;
  /* Red unless a variant says otherwise. Declared here rather than on :root so
     the three live together and a variant is two lines. */
  --toast-bg: #b02a25;
  --toast-line: #d4544e;
  --toast-x: #ffd9d7;
  font-size: .9rem;
  line-height: 1.35;
  box-shadow: 0 8px 22px rgb(0 0 0 / .5);
  /* It hangs over a page that is still being used, so the BOX takes no clicks
     — only its ✕ does, which turns them back on for itself. */
  pointer-events: none;
  /* The centering is repeated in the animation's frames because a transform is
     ONE property: an entrance that animated `transform` alone would throw the
     -50% away and slide the line off to the right. */
  transform: translateX(-50%);
  animation: toast-in .16s ease-out;
}
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, -8px); }
  to { opacity: 1; transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
}
.toast svg {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  margin-top: 1px;
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* It worked. */
.toast.ok { --toast-bg: #1e7a52; --toast-line: #34a374; --toast-x: #cdf1e0; }
/* Something is true and worth a moment. Amber's dark end, which is what carries
   white — the token itself is a color for reading ON a panel, not under text. */
.toast.note { --toast-bg: #8a5a12; --toast-line: #c98a2e; --toast-x: #ffe6c2; }
/* Something is happening. Blue's dark end — and the one blue thing on the site
   that is not a control; see the note above for why that is affordable here. */
.toast.info { --toast-bg: #1c6ea3; --toast-line: #4ba3dc; --toast-x: #cfe9fa; }
/* No state at all. A dark slate box, rimmed brighter than any other border on
   the site — that rim is what keeps it from reading as one more panel, which is
   why it does not need to be black to the point of being a hole. */
.toast.plain { --toast-bg: #1c222e; --toast-line: #4a5769; --toast-x: var(--text-dim); }

.toast-text { flex: 1 1 auto; }
/* A bare glyph on a colored box — the one control on the site with no plate of
   its own, because a plate here would be a second colored thing inside a
   colored thing. */
.toast-x {
  flex: 0 0 auto;
  pointer-events: auto;
  margin: -.15rem -.1rem 0 0;
  padding: 0 .25rem;
  border: 0;
  background: none;
  color: var(--toast-x);
  font: inherit;
  line-height: 1.2;
  cursor: pointer;
}
.toast-x:hover { color: #fff; }
.toast-x:focus-visible { outline: 1px solid #fff; outline-offset: 2px; }

/* -- the confirm modal -----------------------------------------------------
   A real <dialog>, opened with showModal() by assets/confirm.js. The browser
   brings the backdrop, the focus trap, Escape, and inertness of everything
   behind it -- all of which a hand-rolled div has to reimplement, and usually
   only reimplements the parts somebody remembered.

   Like the tooltip there is exactly ONE of these on a page and the script owns
   it. Unlike the tooltip it is not a floating hint: it is the browser's own
   modal, so nothing here positions it.

   window.confirm() would have done the job and looks like Windows in the middle
   of a dark page. That is the whole reason this exists. */

dialog.confirm {
  width: min(26rem, calc(100vw - 2rem));
  padding: 1.05rem 1.2rem 1.15rem;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 18px 48px rgb(0 0 0 / .55);
}
dialog.confirm::backdrop { background: rgb(4 6 10 / .66); }
dialog.confirm h3 { margin: 0; }
dialog.confirm p { margin: .4rem 0 0; color: var(--text-dim); font-size: .92rem; }
/* Right-aligned, and the destructive one last: the button nearest the pointer's
   resting place should not be the one that cannot be undone. */
dialog.confirm .actions { justify-content: flex-end; gap: .8rem; margin-top: 1.15rem; }

/* -- the edit modal --------------------------------------------------------
   The other <dialog>, opened by assets/session-edit.js: the three things about
   a session a person knows better than the logger did.

   Everything the confirm brings is the same -- the box, the backdrop, the
   right-aligned actions -- so this is that dialog with a form in it rather than
   a sentence, and the rules below are only what a form needs. Neither is a
   second modal: there is one of each on a page and its own module owns it. */

dialog.edit {
  width: min(24rem, calc(100vw - 2rem));
  padding: 1.05rem 1.2rem 1.15rem;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 18px 48px rgb(0 0 0 / .55);
}
dialog.edit::backdrop { background: rgb(4 6 10 / .66); }
dialog.edit h3 { margin: 0 0 .9rem; }
dialog.edit .field { margin: 0 0 .75rem; }
/* A field left empty is one the ticked sessions do not agree on, and typing in
   it is what sets them all. Said with the box itself and not with a sentence
   above it -- see the "no explanatory text" rule. */
dialog.edit .error { margin: .2rem 0 0; color: var(--danger); font-size: .88rem; }
dialog.edit .actions { justify-content: flex-end; gap: .8rem; margin-top: 1.15rem; }

/* -- the share modal -------------------------------------------------------
   The third <dialog>, opened by assets/session-share.js: one link that opens
   one session for somebody with no account.

   The edit modal's box again, wider only because it holds a URL that should not
   have to scroll to be read. It is a modal rather than a menu item -- unlike
   Make public / Make private beside it in the same ⋮ -- because a URL has to be
   shown, copied and taken away, and none of those three is a place to go.

   The link box is `readonly` and not `disabled`: a disabled input cannot be
   selected, and selecting the URL is how somebody copies it without the button.
   It is monospaced for the reason every id on this site is -- a link read aloud
   or checked character by character wants unambiguous glyphs. */

dialog.share {
  width: min(32rem, calc(100vw - 2rem));
  padding: 1.05rem 1.2rem 1.15rem;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 18px 48px rgb(0 0 0 / .55);
}
dialog.share::backdrop { background: rgb(4 6 10 / .66); }
dialog.share h3 { margin: 0 0 .9rem; }
/* `.link` and not every input: the email list under it has a box of its own,
   and an address is read as a word, not checked glyph by glyph. */
dialog.share input.link {
  width: 100%;
  font-family: ui-monospace, "Cascadia Mono", SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .85rem;
}
dialog.share .error { margin: .2rem 0 0; color: var(--danger); font-size: .88rem; }
dialog.share .actions { justify-content: flex-end; gap: .8rem; margin-top: 1.15rem; }
/* The people, under the link: one word of heading with a rule over it, then
   the email list. */
dialog.share h4 {
  margin: 1.1rem 0 .5rem;
  padding-top: .9rem;
  border-top: 1px solid var(--line);
  font-size: .88rem;
  color: var(--muted);
}

/* -- the email list --------------------------------------------------------
   Who something is shared with, by address: a list with a Remove beside each
   entry and one box with an Add under it. Drawn by assets/email-list.js in
   two places -- the share dialog above, for one session, and the account
   page, for every session -- and this is the whole of how it looks.

   Remove is a `.link` button and not `.danger`: taking an address off is
   undone by typing it again, and red is for what cannot be brought back. The
   list is `hidden` when empty rather than showing a sentence about being
   empty -- the box under it is the whole answer to "nobody yet". */

.emaillist .emails {
  list-style: none;
  margin: 0 0 .6rem;
  padding: 0;
  display: grid;
  gap: .3rem;
}
.emaillist .emails li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: .3rem 0;
  border-bottom: 1px solid var(--line);
  font-size: .92rem;
}
.emaillist .address { min-width: 0; overflow-wrap: anywhere; }
.emaillist .add {
  display: flex;
  gap: .6rem;
  align-items: center;
  margin: 0;
}
.emaillist .add input { flex: 1 1 12rem; min-width: 0; }
.emaillist .error { margin: .35rem 0 0; color: var(--danger); font-size: .88rem; }

/* -- the activity panel ---------------------------------------------------
   Three lists in one panel: uploads, running jobs, and what just finished.
   `.rows:empty { display: none }` is what lets a group vanish without any JS
   having to hide it -- the server renders three <ul>s whatever is in them. */

.rows { list-style: none; margin: .5rem 0 0; padding: 0; display: grid; gap: .5rem; }
.rows:empty { display: none; }

.row {
  display: grid;
  grid-template-columns: 1fr 10rem 3.5rem;
  align-items: center;
  gap: .75rem;
  padding: .5rem .7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel-2);
}

/* Each child is placed by column rather than by order: a finished row has no
   <progress> in it, and without this its percentage would slide into the bar's
   column and sit under the running rows' bars. */
.row-main { grid-column: 1; min-width: 0; }
.row-name { display: block; font-size: .92rem; max-width: 100%; }
.row-note { display: block; font-size: .8rem; }
.row-pct { grid-column: 3; font-size: .85rem; color: var(--text-dim); }

/* The bar fills blue. Sized here and moved from JS through the `value` IDL
   property -- the CSP covers the style attribute, so a width cannot be set
   inline. A <progress> with no value attribute is indeterminate. */
progress.prog {
  grid-column: 2;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 8px;
  border: 0;
  border-radius: 999px;
  background: var(--panel-3);
  color: var(--blue);
}
progress.prog::-webkit-progress-bar { background: var(--panel-3); border-radius: 999px; }
progress.prog::-webkit-progress-value {
  background: linear-gradient(90deg, var(--blue-2), var(--blue));
  border-radius: 999px;
  transition: width .3s ease;
}
progress.prog::-moz-progress-bar {
  background: linear-gradient(90deg, var(--blue-2), var(--blue));
  border-radius: 999px;
}
/* Indeterminate: no value attribute, so nothing to fill. A faint bar reads as
   "running, no number" rather than as "stuck at zero". */
progress.prog:indeterminate { opacity: .6; }

.row.is-stalled { border-color: var(--line-2); }
.row.is-stalled .row-name, .row.is-stalled .row-pct { color: var(--muted); }
.row.is-stalled progress.prog { opacity: .35; }

.row.is-failed { border-color: var(--danger-line); background: var(--danger-bg); }
.row.is-failed .row-note { color: var(--danger); }

.row.is-done .row-pct { color: var(--ok); }

/* A phone is not going to fit a name, a bar and a number on one line. */
@media (max-width: 40rem) {
  .row { grid-template-columns: 1fr auto; }
  .row-main { grid-column: 1 / -1; }
  progress.prog { grid-column: 1; }
  .row-pct { grid-column: 2; }
}
