/*
 * Car Protect console.
 *
 * THE COLOURS ARE THE ORIGINAL ONES. A pass at a warmer, "classic" palette — cream
 * paper, deep navy — was tried and taken back out: side by side with the blue it
 * replaced, it read as dimmer rather than calmer, and this is a screen people work in
 * rather than look at. What was KEPT from that pass is the structure it came with, which
 * was never the part in question: squared corners instead of pills, hairline rules
 * instead of drop shadows, underlined tabs, lining tabular figures and banded rows.
 *
 * One palette, defined once as variables and flipped by [data-theme]. The dark theme
 * is the default signal (this gets read in a driveway at night as often as at a desk)
 * but the toggle wins in both directions.
 *
 * THE LOOK IS DELIBERATELY CLASSIC RATHER THAN CURRENT.
 * It was a soft modern panel design — bright blue, 14px corners, a drop shadow under
 * everything — and that style says "app". This console is a RECORD: which cars exist,
 * who fitted them, what each one blocks, who was paid. So it is set like a printed one.
 * Warm paper rather than cold grey, ink rather than near-black, a serif for anything
 * that names something, and RULES INSTEAD OF SHADOWS — a shadow suggests a floating
 * surface, a rule states where one column of figures ends and the next begins, which is
 * the only thing a table of thirty vehicles actually needs from its decoration.
 *
 * WHAT DID NOT CHANGE: every class name, every id and every piece of markup. This is
 * one stylesheet edit — the pages and app.js were not touched — so anything that looks
 * wrong here can be corrected here, and nothing about how the console WORKS is resting
 * on how it looks.
 *
 * TYPE IS SYSTEM-ONLY, on purpose. A serif this classic is one webfont away, and a
 * webfont means an external stylesheet and a font file, which means loosening the
 * Content-Security-Policy in public/_headers and in src/index.js for decoration. The
 * stacks below resolve on every platform this is opened on and cost one round trip
 * fewer than the old design did.
 */

:root {
  /* Warm paper, not grey. The old #f6f7f9 is a screen colour; this is a page. */
  --bg: #f6f7f9;
  --panel: #ffffff;
  --panel-2: #f0f2f5;
  --ink: #16181d;
  --ink-dim: #5c6472;
  --line: #dfe3ea;
  /* Deep navy, which is the classic accent for a ledger and — unlike the old bright
     blue — never has to be told apart from --bad at a glance. */
  --accent: #2f6df6;
  --accent-ink: #ffffff;
  --ok: #1f8a4c;
  --warn: #c2670a;
  --bad: #c2352c;
  --radius: 4px;
  /* Kept as a token because rules elsewhere still ask for it, but a hairline rather
     than a lift: the border does the work now. */
  --shadow: 0 1px 0 rgba(26, 24, 21, .04);
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  /* Used by the privacy policy only. Kept as a token so that page and this one cannot
     disagree about which serif they mean. */
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia,
           "Times New Roman", serif;
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    /* Not black. Dark brown-grey, the colour of a room rather than of a void, so the
       warm paper above has somewhere to have come from. */
    --bg: #0e1116;
    --panel: #161b22;
    --panel-2: #1d232c;
    --ink: #e8ecf2;
    --ink-dim: #97a2b2;
    --line: #262d38;
    /* The navy lifted until it carries on this ground. Anything darker reads as a hole
       in the page rather than as a link. */
    --accent: #5b8dff;
    --accent-ink: #08101f;
    --ok: #48c07c;
    --warn: #e0a33c;
    --bad: #f07067;
    --shadow: 0 1px 0 rgba(0, 0, 0, .5);
    color-scheme: dark;
  }
}

:root[data-theme="light"] {
  --bg: #f6f7f9; --panel: #ffffff; --panel-2: #f0f2f5;
  --ink: #16181d; --ink-dim: #5c6472; --line: #dfe3ea;
  --accent: #2f6df6; --accent-ink: #ffffff;
  --ok: #1f8a4c; --warn: #c2670a; --bad: #c2352c;
  --shadow: 0 1px 0 rgba(26, 24, 21, .04);
  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg: #0e1116; --panel: #161b22; --panel-2: #1d232c;
  --ink: #e8ecf2; --ink-dim: #97a2b2; --line: #262d38;
  --accent: #5b8dff; --accent-ink: #08101f;
  --ok: #48c07c; --warn: #e0a33c; --bad: #f07067;
  --shadow: 0 1px 0 rgba(0, 0, 0, .5);
  color-scheme: dark;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* SANS THROUGHOUT THE CONSOLE. The serif headings went back out again: this is a
   screen of package names, device ids and tables, read by glancing rather than by
   reading, and mixing two families across a dense page bought atmosphere at the cost of
   looking settled. The serif is kept for the privacy policy — see privacy.css — which is
   the one page here that is genuinely read at length, and where it earns its place.
   Weight and spacing do the separating instead, which is what they are for. */
h1, h2, h3 {
  margin: 0 0 .35em; line-height: 1.25;
  font-weight: 650;
  letter-spacing: -.005em;
}
h1 { font-size: 1.65rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }
/* The old design leaned on weight to separate a heading from its paragraph. A serif
   already does that, so the weight comes back down and the SPACE does the separating —
   which is what a printed page does. */
h2, h3 { margin-bottom: .5em; }
p { margin: 0 0 .8em; }
.muted { color: var(--ink-dim); font-size: .875rem; }

/*
 * The copyright line, at the foot of every page.
 *
 * SMALLER AND DIMMER THAN .muted, which is the smallest thing on a page that anybody is
 * meant to READ. This is not: it is a line you should be able to find when you look for
 * it and never notice when you are working, so it takes the one step down that .muted
 * deliberately does not.
 *
 * Centred and given room above it rather than tucked under the last card, because on the
 * console the last thing on the page is a table whose height changes with the fleet —
 * a footer hard against it reads as another row.
 */
.site-footer {
  color: var(--ink-dim);
  font-size: .72rem;
  text-align: center;
  padding: 28px 16px 20px;
  opacity: .75;
}
.error { color: var(--bad); font-size: .875rem; }
[hidden] { display: none !important; }

/* --------------------------------- brand -------------------------------- */

/* An <a> now, not a <div> — so it reloads on click and offers open-in-new-tab and
   copy-link on right-click, which a div cannot do at any price. Each one points at its
   OWN page rather than at "/", so the notice keeps having no route into the console.
   Styled back to looking like a wordmark: a link that underlines itself in the corner
   of every page reads as navigation somewhere else, which it is not. */
.brand {
  display: flex; align-items: center; gap: .55rem;
  font-weight: 650; letter-spacing: -.01em;
  color: inherit;
  text-decoration: none;
}

.brand:hover { text-decoration: none; }

/* Focus is NOT removed with the underline. A keyboard user needs to see where they are,
   and this is the first thing in the tab order on every page. */
.brand:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 8px;
}

/* The app's own icon, at the size the CP tile used to be. It carries no alpha, so it
   draws as a white rounded tile — nearly invisible against a light panel without the
   hairline, which is what the border is for rather than decoration. */
.brand-mark {
  width: 24px; height: 24px; border-radius: 7px;
  object-fit: cover;
  border: 1px solid var(--line);
  flex: none;
  /* The artwork is a shield with a wordmark inside; letting the tile bleed to its own
     edges wastes the few pixels the shield has to read in. */
  background: #fff;
}

/* --------------------------------- login -------------------------------- */

.login { min-height: 100dvh; display: grid; place-items: center; padding: 24px; }

.login-card {
  width: min(400px, 100%);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 28px;
  display: flex; flex-direction: column; gap: .35rem;
}
.login-card h1 { margin-top: 1rem; }
.login-card label { font-size: .8rem; color: var(--ink-dim); margin-top: .7rem; }

input[type="email"], input[type="password"], input[type="search"], input[type="text"],
select, textarea {
  width: 100%;
  padding: .6rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--ink);
  font: inherit;
}
/* The block-order boxes hold package names, one per line, and a proportional face
   makes a column of them impossible to scan for the odd one out. */
textarea { font-family: var(--mono); font-size: .85rem; line-height: 1.5; resize: vertical; }
input:focus-visible, select:focus-visible, button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Reveal toggle. Inside the field rather than beside it, so the form keeps one column.
   MUST come after the generic input rule above: `.password-field input` and
   `input[type="password"]` have identical specificity, so source order is the only
   thing deciding the padding — and losing it puts the text under the button. */
.password-field { position: relative; display: flex; }
.password-field input { padding-right: 2.7rem; }
.reveal {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: 8px;
  background: transparent; color: var(--ink-dim);
}
.reveal:hover { color: var(--ink); background: var(--panel); border-color: transparent; }
.reveal svg { width: 18px; height: 18px; }
.reveal .eye-off { display: none; }
.reveal.on { color: var(--accent); }
.reveal.on .eye { display: none; }
.reveal.on .eye-off { display: block; }

button {
  font: inherit; cursor: pointer;
  border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--panel-2); color: var(--ink);
  padding: .5rem .85rem;
}
button:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
button.primary {
  margin-top: 1.1rem;
  background: var(--accent); color: var(--accent-ink);
  border-color: var(--accent); font-weight: 600; padding: .65rem 1.1rem;
  letter-spacing: .015em;
}
button.ghost { background: transparent; }
/* Reads as "not yet", not as "broken". The sign-in button spends a moment in this
   state while the bot check solves, so it has to look deliberately closed rather than
   merely faded — `not-allowed` is the part that says the click will not land. */
button:disabled { opacity: .55; cursor: not-allowed; }

/* An <a> that has to read as a button. Everything above is scoped to `button`, so a
   link picks up none of it — .ghost on its own would give it no box at all. Kept to
   this one class rather than widening the selectors, which would restyle every link on
   both pages. */
a.ghost.open-vaad {
  display: inline-block;
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .5rem .85rem;
  background: transparent;
  color: var(--ink);
}

a.ghost.open-vaad:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

/* --- A car that has gone quiet ------------------------------------------- */
/* Shared by the console's Vaad tab and the standalone /vaad page. Colour is not the
   only signal — the row carries a marker in its margin too — because "slightly redder
   text" is exactly the difference that disappears on a sunlit windscreen or to a
   red-green colour blind reader. */
.stale {
  color: var(--bad, #ef4444);
  font-weight: 600;
}

.row-stale td:first-child {
  box-shadow: inset 3px 0 0 var(--bad, #ef4444);
}

/* Room under the table for the sentence explaining what a gap means. A fleet view that
   shows "12 days ago" without saying whether that is alarming has reported a number
   rather than answered a question. */
.vaad-note {
  margin: 14px 2px 2px;
  font-size: 0.86rem;
  line-height: 1.5;
}

/* -------------------------------- shell --------------------------------- */

.topbar {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .8rem clamp(12px, 3vw, 28px);
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 20;
}
.spacer { flex: 1 1 auto; }
.device-picker { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.device-picker select { min-width: min(320px, 52vw); }

/* The tab bar sits directly under the topbar, so it has to stick at exactly the
   topbar's height. That was written as a literal 57px, which was never right — the
   topbar measures 70px on a desktop, so the tabs covered its bottom 13px whenever the
   page was scrolled, and on a phone, where it wraps to over 200px, the two bars sat on
   top of each other completely. The height depends on the font, the zoom level and how
   long the signed-in address is, so no literal can be correct; app.js measures it into
   --topbar-h. The fallback preserves the old behaviour if that never runs. */
.tabs {
  display: flex; gap: .25rem; overflow-x: auto;
  padding: .4rem clamp(12px, 3vw, 28px);
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: sticky; top: var(--topbar-h, 57px); z-index: 19;
}
/* UNDERLINED, NOT PILLS. A pill is a button that happens to be selected; an underline
   is a tab, and it is the form every index, ledger and folder has used for a century.
   It also stops the bar looking like eleven buttons competing for the same press. */
.tabs button {
  border: 0; background: transparent; color: var(--ink-dim);
  padding: .45rem .2rem; margin: 0 .55rem; border-radius: 0; white-space: nowrap;
  border-bottom: 2px solid transparent;
  font-size: .92rem; font-weight: 500;
}
.tabs button:hover { color: var(--ink); border-color: var(--line); }
.tabs button.active {
  background: transparent; color: var(--ink); font-weight: 600;
  border-bottom-color: var(--accent);
}

.content { padding: clamp(14px, 3vw, 28px); max-width: 1280px; margin: 0 auto; }
.tab { display: none; }
.tab.active { display: block; }

/* ------------------------------ small screens ----------------------------
   Most of this stylesheet is already fluid — the tables scroll inside
   .table-wrap, the grids collapse to one column, the charts wrap — and measuring
   every tab at 375px found no sideways scrolling anywhere. What it did find is
   the header.

   `.tabs` sticks at `top: 57px`, which is the topbar's height when it fits on one
   line. It does not fit on a phone: at 375px it wraps to about 220px, so the two
   sticky bars overlap as soon as anything scrolls, and between them they take a
   quarter of the screen before a single row of data. A taller magic number would
   only move the problem, because the wrapped height depends on how long the
   signed-in address is.

   So below this width the topbar simply scrolls away and the tabs stick on their
   own at the top. Nothing in the topbar is needed while reading a table — it is a
   brand, a vehicle picker, a theme toggle and sign-out — whereas the tabs are how
   you get anywhere, so those are what stays. */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px 22px;
  margin-bottom: 18px;
}
/* A RULE UNDER THE HEADING, which is the oldest device in the book for saying "this
   heading owns what follows". It replaces the drop shadow that used to say the same
   thing by making the whole card float, and unlike the shadow it survives being
   printed, screenshotted or read at low contrast. */
.card-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin-bottom: .9rem; padding-bottom: .55rem;
  border-bottom: 1px solid var(--line);
}
.card-head h2, .card-head h3 { margin-bottom: 0; }
.filters { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.filters input[type="search"] { width: min(260px, 60vw); }
.check { display: flex; align-items: center; gap: .35rem; font-size: .85rem; color: var(--ink-dim); }

.grid-2 { display: grid; gap: 16px; grid-template-columns: 1fr; }
@media (min-width: 860px) { .grid-2 { grid-template-columns: 1fr 1fr; } }

/* -------------------------------- stats --------------------------------- */

.stat-row {
  display: grid; gap: 12px; margin-bottom: 16px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.stat {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
/* Lining tabular figures, and no negative tracking. The old -.02em was there to make a
   big number look tight; squeezing it only makes 28 and 2B harder to tell apart at the
   glance these are read with. */
.stat-value {
  font-size: 1.8rem; font-weight: 680;
  letter-spacing: 0; font-variant-numeric: lining-nums tabular-nums;
}
.stat-label {
  color: var(--ink-dim); font-size: .74rem;
  text-transform: uppercase; letter-spacing: .07em;
}
.stat.ok .stat-value { color: var(--ok); }
.stat.warn .stat-value { color: var(--warn); }
.stat.bad .stat-value { color: var(--bad); }

/* ------------------------------ access code ------------------------------ */

.code-card { max-width: 720px; }
.code-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

.code {
  font-family: var(--mono);
  font-size: clamp(2.6rem, 11vw, 4.4rem);
  font-weight: 700;
  letter-spacing: .12em;
  text-align: center;
  padding: .35em 0 .2em;
  color: var(--accent);
  word-break: break-all;
}

.countdown { width: 48px; height: 48px; flex: none; }
.countdown-track { fill: none; stroke: var(--line); stroke-width: 12; }
.countdown-arc {
  fill: none; stroke: var(--accent); stroke-width: 12; stroke-linecap: round;
  transform: rotate(-90deg); transform-origin: 60px 60px;
  transition: stroke-dashoffset .9s linear;
}
/* Bigger in the viewBox than it sounds: the ring renders at 48px, so the stroke and
   the number have to grow in SVG units just to stay legible at that size. */
.countdown text { fill: var(--ink-dim); font-size: 40px; font-family: var(--mono); }

.drift { margin-top: .5rem; border-top: 1px solid var(--line); padding-top: .8rem; }

.lookup-card { max-width: 720px; }

/* ------------------------ invite a code-only user ------------------------ */

.invite-card { max-width: 640px; }

.invite-head { display: flex; align-items: flex-start; gap: .9rem; margin-bottom: 1rem; }
.invite-head h3 { margin-bottom: .15em; }
.invite-head p { margin: 0; }
.invite-icon {
  display: grid; place-items: center; flex: none;
  width: 42px; height: 42px; border-radius: var(--radius);
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 18%, transparent),
                                      color-mix(in srgb, var(--accent) 6%, transparent));
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
  color: var(--accent);
}
.invite-icon svg { width: 22px; height: 22px; }

/* What the role is, before you hand it out — the "cannot" half matters as much. */
.capabilities { list-style: none; margin: 0 0 1.2rem; padding: 0; display: grid; gap: .32rem; }
.capabilities li {
  display: flex; align-items: baseline; gap: .55rem;
  font-size: .875rem; color: var(--ink-dim);
}
.capabilities li::before { font-weight: 700; flex: none; }
.capabilities .can::before { content: "✓"; color: var(--ok); }
.capabilities .cannot::before { content: "✕"; color: var(--bad); }

.field-label {
  display: block; margin: .9rem 0 .3rem;
  font-size: .8rem; font-weight: 600; color: var(--ink-dim);
}

/* Radio cards rather than a checkbox: both outcomes are then written down, so
   "what happens if I leave it unticked" is not a question anyone has to ask. */
.choices { display: grid; gap: .5rem; }
@media (min-width: 620px) { .choices { grid-template-columns: 1fr 1fr; } }

.choice {
  display: flex; gap: .55rem; cursor: pointer;
  padding: .7rem .8rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel-2);
  transition: border-color .12s ease, background .12s ease;
}
.choice:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.choice input { margin: .2rem 0 0; accent-color: var(--accent); flex: none; }
.choice:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--panel));
}
.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
.choice-body { display: grid; gap: .15rem; }
.choice-title { font-size: .875rem; font-weight: 600; }
.choice-title em {
  font-style: normal; font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--accent); margin-left: .3rem;
}
.choice-desc { font-size: .8rem; color: var(--ink-dim); line-height: 1.35; }

#newUserPasswordRow { margin-top: .55rem; }
#addUserForm .primary { margin-top: 1rem; }

/* Shown once, so it has to be unmissable and easy to copy correctly. */
.credentials {
  margin-top: 1.2rem; padding: 16px;
  border: 1px solid color-mix(in srgb, var(--ok) 45%, var(--line));
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ok) 8%, var(--panel));
}
.credentials-head { display: flex; align-items: flex-start; gap: .7rem; margin-bottom: .9rem; }
.credentials-head p { margin: 0; }
.credentials h4 { margin: 0 0 .2em; font-size: 1rem; }
.credentials .tick {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--ok); color: #fff; font-size: .85rem; font-weight: 700;
}
.credential-row {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .3rem 0;
}
.credential-row > span { color: var(--ink-dim); font-size: .78rem; min-width: 74px; }
.credential-row code {
  font-family: var(--mono); font-size: .95rem; letter-spacing: .04em;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 8px; padding: .28rem .5rem; word-break: break-all;
  flex: 1 1 auto;
}
.credential-row.password code {
  font-size: 1.15rem; font-weight: 700; letter-spacing: .12em; color: var(--ink);
}
.credential-row .copy { padding: .28rem .6rem; font-size: .78rem; flex: none; }
.credentials-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .9rem; }
.manual-row { display: flex; gap: .5rem; flex-wrap: wrap; margin: .5rem 0; }
.manual-row input {
  flex: 0 1 180px; width: auto;
  font-family: var(--mono); font-size: 1.15rem; letter-spacing: .14em; text-align: center;
}
.drift summary { cursor: pointer; color: var(--ink-dim); font-size: .875rem; }
/* Who to ring about this car, under the access code. Deliberately quieter than the
   code itself: the code is what the screen is FOR, and a phone number competing with
   it for attention would make the thing people came for harder to find. */
/* A switch that is not about the same thing as the ones above it. A rule and a little
   air is enough to say so without a second card and a second Save button. */
.switch-row-apart {
  border-top: 1px solid var(--line);
  margin-top: .5rem;
  padding-top: .9rem;
}

.code-owner {
  display: grid; gap: 6px; margin: .2rem 0 .8rem;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .5rem .7rem;
}
.code-owner-row { display: flex; justify-content: space-between; align-items: baseline; gap: .8rem; }
.code-owner-label { color: var(--ink-dim); font-size: .78rem; }
.code-owner-value { font-size: .95rem; word-break: break-word; text-align: right; }
/* The number is a link and should look like one you can tap - a car park on a phone is
   where this gets used. */
a.code-owner-value { color: var(--accent); text-decoration: none; font-family: var(--mono); }
a.code-owner-value:hover { text-decoration: underline; }
/* The installer's address is an anchor whether or not it has an href - it carries a
   dash when there is nobody to mail. Without this the dash would still be painted as a
   link, which invites a tap that does nothing. */
a.code-owner-value:not([href]) { color: var(--ink-dim); cursor: default; }

/* The car's own report — rows INSIDE the owner box, not a second panel.

   One box, because it is one question: what car is this, who do I ring, and is it all
   right. Two grey panels stacked on a card read as two unrelated things, and the health
   half would then need a visibility rule of its own — which is exactly how a panel ends
   up on screen at a moment nobody asked for it. */

/* The timestamp stacks under itself so the exact date needs no second row, and stays
   right-aligned to line up with the phone number and address above it. */
.code-status-value { display: grid; gap: 1px; line-height: 1.25; }
#codeStatusDate { font-size: .72rem; }
/* A hairline between "who to ring" and "how it is doing". Added by showCodeAbout() only
   when a contact row is actually above it, so it can never be a stray line at the top
   of the box. */
.code-owner-row.top-rule { border-top: 1px solid var(--line); padding-top: .45rem; margin-top: .15rem; }
/* Wrapping beats shrinking. Three self-describing pills do not fit across a phone, and
   a pill squeezed until its label truncates says less than no pill at all. */
.code-status-pills { display: flex; flex-wrap: wrap; gap: .35rem; }
.code-status-vehicle, .code-status-none { font-size: .78rem; margin: 0; }
.code-status-none { line-height: 1.45; }

.neighbours { display: grid; gap: 6px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); margin-top: .6rem; }
.neighbour {
  display: flex; justify-content: space-between; gap: .6rem;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .4rem .6rem; font-family: var(--mono); font-size: .95rem;
}
.neighbour.now { border-color: var(--accent); }
.neighbour span { color: var(--ink-dim); font-size: .78rem; font-family: system-ui, sans-serif; }

/* ------------------------------- charts ---------------------------------- */

.chart svg { width: 100%; height: auto; display: block; }

/* A donut is a key, not a picture. Sized to be read at a glance beside its legend
   rather than stretched to the width of the card — at full card width the ring was
   bigger than everything it was explaining. */
.donut { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; }
.donut svg { width: 120px; flex: none; }
.donut .legend { flex: 1 1 160px; margin-top: 0; }

.legend { display: grid; gap: .3rem; margin-top: .6rem; }
.legend div { display: flex; align-items: center; gap: .5rem; font-size: .85rem; }
.swatch { width: 11px; height: 11px; border-radius: 2px; flex: none; }
.bar-row { display: grid; grid-template-columns: minmax(90px, 34%) 1fr auto; gap: .6rem; align-items: center; margin-bottom: .35rem; font-size: .85rem; }
.bar-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-dim); }
.bar-track { background: var(--panel-2); border-radius: 2px; height: 10px; overflow: hidden; border: 1px solid var(--line); }
.bar-fill { height: 100%; background: var(--accent); border-radius: 1px; }
.chart-empty { color: var(--ink-dim); font-size: .875rem; padding: .5rem 0; }

/* ------------------------------- tables ---------------------------------- */

/* NO BOX OF ITS OWN TO SCROLL.
   This capped itself at 62vh and scrolled inside a frame, which meant a console holding
   forty cars showed about twelve of them and put the rest behind a scrollbar nested
   inside the page's own — so a flick moved the list or the page depending on where the
   pointer happened to be, and "how many cars are there" could not be answered by
   looking. The whole list is laid out now and the page scrolls it.

   The horizontal scroll is gone too, and that one is load-bearing: `overflow-x: auto`
   makes overflow-y compute to auto as well, which makes this a scroll container, which
   is what the sticky header below resolves against. With no height cap that container
   never scrolls, so the header would have scrolled away with the page. Removing it
   entirely hands the header back to the viewport — which is also why the cells below
   must wrap rather than overflow. */
.table-wrap { border: 1px solid var(--line); border-radius: var(--radius); }

/* The corners used to be cut by that overflow. Only the top two need doing by hand: the
   header has a background that would square them off, while the last row's cells are
   transparent and let the border's own curve show through. */
.table-wrap thead th:first-child { border-top-left-radius: 3px; }
.table-wrap thead th:last-child { border-top-right-radius: 3px; }
.table-wrap tbody tr:last-child td { border-bottom: 0; }

/* Lining tabular figures throughout. Every table here has a column of counts, dates or
   revisions in it, and figures that do not share a width do not form a column — which
   is the one thing a table is for. */
table {
  border-collapse: collapse; width: 100%; font-size: .875rem;
  font-variant-numeric: lining-nums tabular-nums;
}

/* CELLS WRAP; THEY DO NOT SET THE WIDTH.
   `white-space: nowrap` made every column as wide as its longest value, so the eleven
   columns of the vehicle list came to about 1100px whatever screen was asked to show
   them. Letting cells wrap is what frees the browser to size columns to the screen it
   actually has — and because automatic table layout still prefers the natural width
   when there is room, nothing wraps on a desktop that did not wrap before.

   `break-word`, NOT `anywhere`. The two look alike and differ in the one place that
   matters: `anywhere` also drops the cell's MINIMUM width to a single character, which
   tells the layout it may squeeze a column to any width at all. It did — the Type
   column came out narrower than the word "system" and split it as "syste/m", and Icon
   split "no" into two lines. `break-word` breaks a word only when that word alone
   cannot fit its line, and leaves the minimum at the longest word, so a column is never
   sized smaller than the words it has to hold. */
th, td { text-align: left; padding: .52rem .75rem; border-bottom: 1px solid var(--line); overflow-wrap: break-word; }

/* THE EXCEPTION, and the reason `anywhere` was reached for in the first place. A
   package name is one unbreakable token —
   `com.android.car.settings.common.CarSettingActivities$ResetOptionsActivity` is
   seventy characters with no space in it — so a minimum of "the longest word" would let
   a single cell set the width of the whole table and push it off the side of the
   screen. These columns are the ones where breaking mid-token is the correct answer,
   and they are also the ones where nobody reads the value as a word. */
td.mono, td.wrap, td.tok, .table-wrap .mono { overflow-wrap: anywhere; }
th {
  /* Sticky against the viewport now, so it has to clear whatever is pinned above it —
     the topbar and the tab bar on a desktop, nothing at all on a phone where both
     scroll away. app.js measures the real stack into --sticky-h; the 0px fallback
     leaves the header at the top of the screen if that never runs. */
  position: sticky; top: var(--sticky-h, 0px); z-index: 1;
  background: var(--panel-2); color: var(--ink-dim);
  font-weight: 600; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase;
  cursor: pointer; user-select: none;
  /* THE HEAVY RULE UNDER THE HEADINGS is what makes this read as a ledger rather than
     as a list of rows that happens to start with bold text. It is the one place in the
     stylesheet that uses a thicker line than a hairline, and it earns it: it is the
     only rule a reader needs to find without looking for it. */
  border-bottom: 2px solid var(--ink-dim);
}
th[data-sorted]::after { content: " ▲"; font-size: .7em; }
th[data-sorted="desc"]::after { content: " ▼"; }
tbody tr:hover { background: var(--panel-2); }
/* Every other row tinted, faintly. On a thirty-car list the eye loses its place between
   the first column and the last, and banding is how printed tables have fixed that
   since long before any of this. Kept well under the hover tint so the two never
   compete for the same row. */
tbody tr:nth-child(even):not(.group):not(:hover) {
  background: color-mix(in srgb, var(--panel-2) 45%, transparent);
}
/* A floor, not a demand: 220px is well over half a 375px screen, and a column that
   insists on that much is a column that pushes the table off the side of it. */
td.wrap { min-width: min(220px, 45vw); }
td.mono, .mono { font-family: var(--mono); font-size: .82rem; color: var(--ink-dim); }
tr.group td {
  background: var(--panel-2); font-weight: 650; color: var(--ink);
  /* Directly under the column headings, wherever those have come to rest. */
  position: sticky; top: calc(var(--sticky-h, 0px) + 30px); z-index: 1;
}

/* Squared off with the rest of the page. A fully round pill is the single most
   recognisable shape of the style this design moved away from, and left rounded it was
   the one thing on screen still saying "app". */
.pill {
  display: inline-block; padding: .1rem .45rem; border-radius: var(--radius);
  font-size: .74rem; font-weight: 600; border: 1px solid transparent;
  letter-spacing: .02em;
}
.pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.pill.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
.pill.bad { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
.pill.dim { color: var(--ink-dim); border-color: var(--line); }

.row-actions button { padding: .25rem .55rem; font-size: .8rem; }

/* --------------------------------- log ----------------------------------- */

.log {
  font-family: var(--mono); font-size: .8rem; line-height: 1.45;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px; max-height: 60vh; overflow: auto; white-space: pre-wrap; word-break: break-word;
  margin: 0;
}

/* -------------------------------- toast ---------------------------------- */

.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow);
  border-radius: var(--radius); padding: .55rem 1rem; font-size: .875rem; z-index: 50; margin: 0;
}

/* ==========================================================================
   SMALL SCREENS

   LAST IN THE FILE ON PURPOSE. A media query adds no specificity, so a rule
   inside one only beats a base rule of equal specificity by coming after it.
   This block used to sit near the top, above `.stat-row` and `.table-wrap`,
   and every override that collided with those at equal specificity was
   silently discarded — the table-to-card rules worked only because
   `.table-wrap td` outranks `th, td`, while `.table-wrap`'s own overrides did
   nothing at all. Keep new small-screen rules here, at the bottom.
   ========================================================================== */
@media (max-width: 760px) {
  .topbar {
    position: static;
    gap: .5rem .7rem;
    padding: .6rem 14px;
  }

  /* THE TABS WRAP HERE — they do not scroll sideways.
     Nine tabs come to about 900px of content. At 375px that put six of them past the
     right edge, reachable only by dragging the bar sideways, with nothing on screen to
     suggest they were there — Debug logs and everything after it simply looked absent.
     (An earlier version of this block hid the scrollbar too, which removed the last
     hint that anything was off-screen.) A gesture nobody knows to try is not a
     navigation control, so every tab is now on screen and one tap away, and the type
     shrinks a little so nine of them still come to two rows rather than three. */
  .tabs {
    /* Not sticky here either. Wrapped, nine tabs come to three rows — about 118px, a
       seventh of a phone screen, held permanently against a table you are trying to
       read. Sticky nav is worth that on a desktop and not on a phone, so the whole
       header scrolls away as one block and a flick to the top brings it back. */
    position: static;
    padding: .35rem 8px .5rem;
    flex-wrap: wrap;
    overflow-x: visible;
    row-gap: .3rem;
  }
  .tabs button {
    /* 44px is the usual floor for something you hit with a thumb; the desktop tabs are
       32px, which is fine for a pointer and not for a moving car. */
    min-height: 42px;
    padding: .4rem .7rem;
    font-size: .84rem;
  }

  /* The vehicle picker gets its own full-width line — it is the one control here
     worth room, and at 52vw it was squeezing everything else onto extra rows. */
  .device-picker { order: 5; flex: 1 0 100%; }
  .device-picker select { min-width: 0; width: 100%; }

  /* A long address is what pushed the header to three lines. Truncated rather than
     hidden: knowing WHICH account is signed in matters on a shared phone, and the
     first part of an address is enough to tell two of them apart. */
  #whoami {
    max-width: 40vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The spacer earns nothing once things wrap, and its flex-grow was creating a
     line of its own. */
  .spacer { display: none; }

  /* Two across at 320px rather than one, which is what minmax(150px) gives once
     padding is taken off. */
  .stat-row { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; }
  .stat { padding: 11px 12px; }
  .stat-value { font-size: 1.45rem; }

  /* Long values — a generated password, a device id — must wrap rather than push
     the card wider than the screen. */
  .credential-row { flex-wrap: wrap; }
  .credential-row code { min-width: 0; overflow-wrap: anywhere; }

  /* A LIST, NOT CARDS.
     Below this width every row used to become its own card, one labelled value per
     line. It fit, and it stopped being a list: seven cars came to seven tall blocks, so
     comparing two of them meant scrolling from one to the other — and scanning a column
     for the one car that has gone quiet is the entire job of this page.

     It stays a table at every width now, and is made to fit rather than reshaped. The
     work is done by the wrapping in the base rules, which lets the browser size columns
     to the screen it actually has; everything here is the type and spacing shrinking to
     match. Nothing is hidden, nothing is behind a sideways drag, and the
     column headings stay where they can be read and tapped to sort.

     Eleven columns on a 375px screen is dense — the vehicle list is genuinely at the
     limit of what fits. It is legible, which two columns and a horizontal scrollbar
     were not. */
  table { font-size: .78rem; }
  th, td { padding: .38rem .4rem; }
  th { font-size: .66rem; letter-spacing: 0; }
  td.mono, .mono { font-size: .72rem; }
  .pill { padding: .05rem .32rem; font-size: .68rem; }
  .row-actions button { padding: .2rem .4rem; font-size: .72rem; }

  /* The wide-column floor is the one thing that can still overflow a phone. */
  .table-wrap td.wrap { min-width: 0; }

  /* NOTHING IS PINNED AT THIS WIDTH, so the heading rests at the top of the screen.
     This is the media query's call to make, not the measurement's: the same rules above
     are what turn both bars static, so asking JS whether anything is covered duplicates
     a decision already made here — and gets it wrong for as long as it takes an event
     to arrive. Without this, a --sticky-h measured on a wide window and not yet updated
     leaves the column headings hovering a hundred pixels down the page. */
  th { top: 0; }
  tr.group td { top: 30px; }
}

/* ==========================================================================
   COLUMNS THAT GIVE UP THEIR PLACE

   Shrinking the type is only half of making a table fit. Eleven columns on a
   375px screen came to 21px each — three characters, words broken mid-syllable
   — which fits in the sense that nothing hangs off the edge and in no other
   sense at all. A column has a width below which it stops carrying its value,
   and past that point the honest move is to spend the screen on fewer of them.

   So each column declares what it is worth: unset is kept everywhere, 2 goes
   below 560px, 3 goes below 760px. renderTable writes it to every heading and
   every cell as data-pri; the priorities themselves live beside the column
   definitions in app.js and vaad.js, because which of eleven columns matters
   is a question about the data, not about CSS.

   Nothing is lost, only deferred — every column returns the moment the window
   is wide enough to hold it, and rotating a phone to landscape is usually
   enough. What survives at every width is what each table is opened to answer:
   which car, when it was last heard from, and whether it is protected.
   ========================================================================== */
@media (max-width: 760px) {
  [data-pri="3"] { display: none; }
}

@media (max-width: 560px) {
  [data-pri="2"] { display: none; }
}

/* Tier 4, and it exists because of one table. Not breaking words gave every
   column a real minimum, and the eleven columns of the vehicle list add up to
   about 970px of them — so between 760px and 1000px that table was pushing the
   whole PAGE sideways, which is the complaint this all started with. Measured:
   spills 217px at 768, 93px at 900, fits at 1000.

   A fourth tier rather than moving tier 3 up, because tier 3 is shared: the
   Apps table fits all eight of its columns at 768px comfortably, and raising
   the shared breakpoint would have stripped three columns off every table to
   solve a problem only the widest one has. 1080px leaves ~80px of headroom over
   the measured minimum for a vehicle name longer than any in hand, and sits
   below every common laptop width, so a real desktop still shows everything. */
@media (max-width: 1080px) {
  [data-pri="4"] { display: none; }
}

/* ==========================================================================
   THE NARROWEST PHONES (320px and the like)

   Not breaking words mid-syllable costs something: a column can no longer be
   squeezed below its longest word, so the table has a real minimum width now.
   At 360px and up that minimum fits with room to spare — measured at zero spill
   — but at 320px the vehicle list wanted 273px inside a 252px wrapper and stuck
   21px out through its own border.

   Paying for it in padding and heading size rather than by breaking words is
   the right trade: the four columns still there are the ones the table exists
   to answer, and a tighter cell is legible in a way "Infotain / ment" is not.
   ========================================================================== */
@media (max-width: 360px) {
  th, td { padding: .36rem .25rem; }
  th { font-size: .62rem; }
}

/* --------------------------- the sign-in switches -------------------------
   Three checkboxes that each need a sentence of explanation, because two of
   them refuse people at the door and the difference between them and the
   cosmetic one is not guessable from a four-word label. So the label is a
   block, not a line: heading, then the consequence underneath it. */

.switch-list {
  display: grid;
  gap: 2px;
  margin: 1.1rem 0 .4rem;
}

.switch-row {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  padding: .85rem .9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  cursor: pointer;
}

.switch-row + .switch-row {
  margin-top: 8px;
}

.switch-row input[type="checkbox"] {
  /* Nudged onto the cap-height of the heading beside it rather than the top of the
     line box, which is where a default checkbox sits and always looks a shade high. */
  margin-top: .18rem;
  flex: none;
}

.switch-row > span {
  display: grid;
  gap: .2rem;
  min-width: 0;
}

.switch-note {
  font-size: .84rem;
  line-height: 1.45;
}

/* The privacy link under the sign-in form. Quiet on purpose: it is there for the person
   who came looking for it, not an invitation to leave the page. */
.login-privacy {
  margin: 1rem 0 0;
  text-align: center;
  font-size: .85rem;
}

.login-privacy a {
  color: inherit;
}

/* --- Payments -------------------------------------------------------------

   Two prices side by side, wrapping to one column when there is no room for two. The
   dollar sign sits INSIDE the field rather than in the label, so what is being typed is
   unambiguous at a glance — these are the numbers that decide what a card is charged,
   and "50" meaning cents rather than dollars is the mistake worth designing out. */
.price-grid {
  display: grid; gap: .8rem; margin: .8rem 0 .3rem;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.price-field { display: grid; gap: .25rem; font-size: .82rem; }
.price-input {
  display: flex; align-items: center; gap: .35rem;
  border: 1px solid var(--line); border-radius: 8px; padding: 0 .55rem;
  background: var(--panel-2);
}
.price-input span { color: var(--ink-dim); font-family: var(--mono); }
/* The border lives on the wrapper, so the input inside gives up its own rather than
   drawing a second one a pixel in from the first. */
.price-input input {
  border: 0; background: transparent; padding: .45rem 0; width: 100%;
  font-family: var(--mono); font-size: .95rem; color: var(--ink);
}
.price-input input:focus { outline: none; }
.price-input:focus-within { border-color: var(--accent); }
.price-note { font-size: .78rem; margin: .1rem 0 0; }

.pay-row {
  display: grid; gap: .8rem; margin: .8rem 0;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.pay-email input, .price-field select {
  border: 1px solid var(--line); border-radius: 8px; padding: .45rem .55rem;
  background: var(--panel-2); color: var(--ink); font-size: .9rem; width: 100%;
}
/* Reserves its own space. The widget is mounted when the tab opens, and without a
   minimum the Save row jumps down the page as it appears. */
.pay-bot { min-height: 68px; margin: .2rem 0 .6rem; }

/* A test payment is not money. It is labelled everywhere it appears, because a ledger
   that shows test rows as though they were takings is worse than no ledger. */
.pill.test { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
.pay-amount { font-family: var(--mono); white-space: nowrap; }

/* The fallback link to the Stripe page. Sits under the Pay button and stays put once a
   session exists, because the case it is there for is the one where the automatic
   redirect silently did nothing. */
.pay-link-row { margin: .4rem 0 0; font-size: .82rem; }

/* The bulk waiver card. Marked out from the cards above it, because it is the one
   control on the page that changes many vehicles in a single press and it should not
   look like the ones that change none. */
.danger-card { border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); }
.danger-card h3 { color: var(--warn); }

/* ---------------------------------------------------------------------------
   Account permissions
   ---------------------------------------------------------------------------
   Built on the same shape as .choice, because they are the same kind of control read
   the same way — a title, a sentence saying what it costs, and a box. They are not the
   same rule because these are checkboxes rather than radios and stack in one column:
   two per row would put "Set up cars" beside "Manage cars remotely" and invite reading
   them as alternatives. */
.perm-set { border: 0; margin: 0 0 1rem; padding: 0; display: grid; gap: .5rem; }
.perm-set legend { padding: 0; margin-bottom: .1rem; }

.perm {
  display: flex; gap: .55rem; cursor: pointer;
  padding: .7rem .8rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel-2);
  transition: border-color .12s ease, background .12s ease;
}
.perm:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.perm input { margin: .2rem 0 0; accent-color: var(--accent); flex: none; }
.perm:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--panel));
}
.perm:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
.perm-body { display: grid; gap: .15rem; }
.perm-title { font-size: .875rem; font-weight: 600; }
.perm-desc { font-size: .8rem; color: var(--ink-dim); line-height: 1.35; }

/* The table cell holding an account's set. Wraps rather than truncating: four pills is
   the most there can ever be, and an account whose permissions are cut off is exactly
   the row somebody opened this page to check.

   NOT display:flex. The pills are already inline-block and wrap on their own, and a
   table cell told to be a flex container stops being a table-cell — which on a desktop
   table means this column no longer lines up with its own header. */
.perm-cell { line-height: 1.7; }

/* An account still being read through its old role. Dim rather than a warning colour:
   nothing is wrong with it and nothing needs doing today. */
.pill.pill-legacy { color: var(--ink-dim); border-color: var(--line); font-style: italic; }

/* Editing one account's permissions. A dialog rather than an inline row, because the
   list is a table and four labelled tick boxes do not fit in a cell. */
.perm-dialog { display: grid; gap: .9rem; min-width: min(30rem, 82vw); }
.perm-dialog h3 { margin: 0; font-size: 1rem; word-break: break-all; }
dialog.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 1.3rem; }
dialog.card::backdrop { background: rgb(0 0 0 / 45%); }

/* The attestation log, inside the Sign-in tab's card rather than in one of its own:
   it is evidence for the switch directly above it, and a card would read as a separate
   subject. Ruled off at the top so it does not look like part of that switch's label. */
.appcheck-log {
  margin-top: 1.2rem; padding-top: 1rem;
  border-top: 1px solid var(--line);
}
.appcheck-log .card-head { margin-bottom: .5rem; }

/* The result of a payment, on the page rather than in a toast. Sized and coloured to be
   the first thing read on arrival back from Stripe — this is the one moment in the flow
   where somebody has just parted with money and is looking for confirmation. */
.paid-banner {
  margin: 0 0 16px;
  padding: .85rem 1rem;
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: var(--panel);
  font-size: .95rem;
}
.paid-banner.good {
  border-left-color: var(--ok);
  color: var(--ink);
  background: color-mix(in srgb, var(--ok) 8%, var(--panel));
}
.paid-banner.bad {
  border-left-color: var(--warn);
  color: var(--ink);
  background: color-mix(in srgb, var(--warn) 8%, var(--panel));
}

/* ---- The access-code lock ----------------------------------------------------

   "Code restricted" takes the place of the code, instead of sitting under a row of dashes
   that look like a code failed to load. The contact rows reuse the owner box, so a name, an
   address and a number read the same wherever this console shows them. */
.code-restricted { display: grid; gap: .45rem; margin: .1rem 0 .8rem; }
.code-restricted > .muted { margin: 0; }
.code-restricted .code-owner { margin: 0; }

/* Name and phone side by side, being one answer to "who is this"; stacked on a phone, where
   two half-width boxes would each be too narrow for a number. */
.contact-pair { display: grid; gap: .6rem; grid-template-columns: 1fr 1fr; margin: .6rem 0; }
@media (max-width: 560px) { .contact-pair { grid-template-columns: 1fr; } }

/* Groups are picked by ticking a name. Wrapping, so a console with a dozen groups is a
   paragraph of names rather than a column pushing Save off the screen. */
.group-picks { display: flex; flex-wrap: wrap; gap: .35rem .9rem; margin-top: .45rem; }
.group-pick { display: inline-flex; align-items: center; gap: .35rem; font-size: .875rem; }
.group-pick input { accent-color: var(--accent); margin: 0; }
.sees-block { margin-bottom: .8rem; }

/* A field in an edit dialog: its label above it, as on the create form. */
.dialog-field { display: grid; gap: .3rem; }

/* A group's members as chips, each with its own remove button, under the dropdown they were
   picked from — so the dialog shows exactly who was chosen and nothing that was typed. The API
   site is marked apart, being the one member that is not a person. */
.member-chips { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .45rem; }
.member-chip {
  display: inline-flex; align-items: center; gap: .25rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2);
  padding: .15rem .2rem .15rem .5rem; font-size: .85rem;
}
.member-chip button {
  border: 0; background: none; color: var(--ink-dim); cursor: pointer;
  padding: 0 .3rem; font-size: 1rem; line-height: 1;
}
.member-chip button:hover { color: var(--bad); }
.member-chip-site, .pill.pill-site {
  color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

/* A dialog never grows past the window, however many members a group is given. The member
   list scrolls inside it first, and the dialog itself scrolls after that, so the group's name
   and the Save button stay within reach. Long labels wrap instead of widening the dialog off
   the side of a phone. */
dialog.card { max-height: 88vh; overflow-y: auto; }
.perm-dialog { max-width: min(38rem, 92vw); }
.member-chips { max-height: 12rem; overflow-y: auto; align-content: flex-start; }
.member-chip span { overflow-wrap: anywhere; }
.perm-dialog .group-picks { max-height: 10rem; overflow-y: auto; }
.groups-block { margin: 0 0 .8rem; }

/* "Code restricted" where the digits would be. Smaller than a code and in the warning colour:
   at code size, in the code's own accent and spacing, two words wrap across three lines and
   read as a code that failed to render. */
.code.restricted {
  font-family: inherit;
  font-size: clamp(1.6rem, 7vw, 2.4rem);
  letter-spacing: normal;
  word-break: normal;
  color: var(--warn);
}
.code-card.is-restricted .countdown { visibility: hidden; }

/* Where Show code scrolls to: clear of the topbar and tabs, which --sticky-h measures, with a
   little air so the card's top edge shows rather than sitting flush against the bars. */
.code-card { scroll-margin-top: calc(var(--sticky-h, 0px) + 12px); }
