/* The shell every signed-in page wears: tokens, layout, and the controls they
   all use.
 *
 * Shared rather than copied. Four pages each carrying their own copy of this is
 * how the menus drifted apart in the first place — one page gained a link, the
 * others did not, and nothing could notice. There is exactly one definition of
 * the navigation now, in app.js, and exactly one of these styles.
 *
 * ui/deploy.sh serves this with a short cache and invalidates CloudFront on
 * every deploy, so it can change without being renamed. Only the vendored
 * libraries carry versions in their filenames, because those are the ones worth
 * caching for a year. */

:root {
  /* Tells the browser to render native controls — including the popup a
     <select> opens, which no page CSS can reach — in the matching scheme.
     Without it a dark page opens a white list of white-on-white options. */
  color-scheme: light dark;

  --bg: #ffffff;
  --fg: #0b0f14;
  --muted: #5b6672;
  --accent: #2f81f7;
  --ring: rgba(47, 129, 247, .35);
  --rule: rgba(11, 15, 20, .10);
  --panel: rgba(11, 15, 20, .035);
  --raised: #ffffff;
  --danger: #b42318;
  --warn: #9a6700;
  --ok: #1a7f37;
  --shadow: 0 6px 24px rgba(11, 15, 20, .12);

  --sidebar: 14.5rem;
  --topbar: 3.5rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0b0f14;
    --fg: #e6edf3;
    --muted: #8b949e;
    --ring: rgba(47, 129, 247, .28);
    --rule: rgba(230, 237, 243, .12);
    --panel: rgba(230, 237, 243, .04);
    --raised: #11161d;
    --danger: #ff7b72;
    --warn: #d29922;
    --ok: #3fb950;
    --shadow: 0 6px 24px rgba(0, 0, 0, .5);
  }
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

/* The browser hides [hidden] with a UA rule, and ANY author display rule beats
   it — so `.card { display: grid }` silently un-hid every hidden card on the
   site. That is not a nicety: it is what put an empty "copy your new token"
   panel on the tokens page and showed the add-user form to people who are not
   allowed to use it. Author-level, so nothing below can outrank it. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  background: var(--bg);
  color: var(--fg);
  line-height: 1.5;
}

/* --- shell -----------------------------------------------------------------
   Desktop is a two-column grid with the sidebar always present. Below 60rem the
   sidebar leaves the flow entirely and becomes a drawer over the content, which
   is the only way to give a phone the whole width for what it is actually
   showing. */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar) 1fr;
  min-height: 100svh;
}

.sidebar {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  background: var(--panel);
  border-right: 1px solid var(--rule);
  padding: 1rem .75rem
           max(1rem, env(safe-area-inset-bottom))
           max(.75rem, env(safe-area-inset-left));
  /* Stays put while the page beside it scrolls. */
  position: sticky;
  top: 0;
  height: 100svh;
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .35rem .5rem 1rem;
}

.brand svg { width: 1.6rem; height: 1.6rem; flex: none; }
.brand strong { font-size: 1rem; font-weight: 620; }

.sidebar nav { display: flex; flex-direction: column; gap: .15rem; }

.sidebar nav a {
  display: flex;
  align-items: center;
  gap: .6rem;
  color: var(--muted);
  text-decoration: none;
  font-size: .9375rem;
  font-weight: 550;
  /* A 44px row, so it is a target on a phone rather than a line of text. */
  padding: .65rem .65rem;
  border-radius: 9px;
}

.sidebar nav a:hover, .sidebar nav a:focus-visible { color: var(--fg); background: var(--rule); }

.sidebar nav a[aria-current="page"] {
  color: var(--fg);
  background: var(--raised);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .06);
}

.sidebar nav svg { width: 1.05rem; height: 1.05rem; flex: none; opacity: .85; }

/* Identity and sign-out sit at the bottom, out of the way of the navigation. */
.sidebar .foot { margin-top: auto; padding-top: 1rem; display: grid; gap: .5rem; }

.sidebar .who {
  color: var(--muted);
  font-size: .75rem;
  padding: 0 .65rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar .org { color: var(--fg); font-weight: 600; font-size: .8125rem; }

.content {
  min-width: 0;   /* lets wide children scroll instead of stretching the grid */
  padding: 1.1rem max(1rem, env(safe-area-inset-right))
           max(1.5rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
}

/* The bar that only exists on a phone. */
.topbar { display: none; }

@media (max-width: 60rem) {
  .shell { grid-template-columns: 1fr; }

  .topbar {
    display: flex;
    align-items: center;
    gap: .6rem;
    position: sticky;
    top: 0;
    z-index: 40;
    height: calc(var(--topbar) + env(safe-area-inset-top));
    padding: env(safe-area-inset-top) max(.75rem, env(safe-area-inset-right)) 0
             max(.75rem, env(safe-area-inset-left));
    background: var(--bg);
    border-bottom: 1px solid var(--rule);
  }

  .topbar .title { font-weight: 620; font-size: 1rem; }

  .sidebar {
    position: fixed;
    z-index: 60;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(17rem, 84vw);
    height: 100svh;
    border-right: 1px solid var(--rule);
    background: var(--bg);
    box-shadow: var(--shadow);
    transform: translateX(-100%);
    transition: transform .22s ease;
  }

  .sidebar.open { transform: none; }

  /* Anyone who asked the system to reduce motion gets the panel without the
     slide. */
  @media (prefers-reduced-motion: reduce) {
    .sidebar { transition: none; }
  }

  .scrim {
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(0, 0, 0, .45);
    border: 0;
    padding: 0;
  }

  .content { padding-top: .9rem; }
}

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

h1 { font-size: 1.25rem; font-weight: 620; margin: 0 0 .25rem; }
h2 { font-size: .9375rem; font-weight: 620; margin: 0; }
.sub { color: var(--muted); font-size: .875rem; margin: 0 0 1rem; }
.note { color: var(--muted); font-size: .75rem; margin: 0; }
.dim { color: var(--muted); }
.mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
.error { color: var(--danger); font-size: .8125rem; margin: 0 0 .5rem; }
.ok { color: var(--ok); font-size: .8125rem; margin: 0 0 .5rem; }

/* For "this answer is incomplete" — not a failure, but not to be read as a
   full result either. Muted grey would be the wrong weight: the whole point is
   that it must be noticed before the result above it is believed. */
.warn { color: var(--warn); font-size: .8125rem; margin: 0; }

button {
  font: inherit;
  font-size: .875rem;
  font-weight: 600;
  color: #fff;
  background: var(--accent);
  border: 0;
  border-radius: 8px;
  padding: .5rem .85rem;
  min-height: 2.5rem;
  cursor: pointer;
}

button.quiet {
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--rule);
}

button:disabled { opacity: .55; cursor: not-allowed; }

/* Icon buttons are square and 44px-ish, so a thumb can hit them. */
.icon {
  display: inline-grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  min-height: 0;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--muted);
  font-size: 1rem;
}

.icon:hover, .icon:focus-visible { color: var(--accent); border-color: var(--rule); }
.icon.danger:hover, .icon.danger:focus-visible { color: var(--danger); }
.icon svg { width: 1.1rem; height: 1.1rem; }

input, select, textarea {
  font: inherit;
  font-size: 1rem;   /* under 16px, iOS zooms the page on focus */
  color: var(--fg);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: .5rem .65rem;
  min-height: 2.5rem;
  width: 100%;
}

input:focus-visible, select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}

/* A native select paints its own background and arrow from the platform, which
   is how a themed page ends up with one control that ignores the theme.
   appearance:none takes that back; the caret below is drawn in currentColor so
   it follows the text rather than needing an asset per scheme. */
select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 1.9rem;
}

/* Honoured on Windows and Linux. macOS draws the popup itself, which the
   color-scheme declaration at the top already points at the right theme. */
select option { background: var(--raised); color: var(--fg); }

.sel { position: relative; display: block; }

.sel::after {
  content: "";
  position: absolute;
  right: .75rem;
  top: 50%;
  width: .4rem;
  height: .4rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}

/* --- Tom Select ---------------------------------------------------------------
   Themed by feeding it variables, not by restyling it.
   Tom Select has no dark theme of its own, but its Bootstrap 5 build is written
   against --bs-* custom properties instead of fixed colours — which is exactly
   how Bootstrap's own dark mode works. Defining those from our tokens themes
   the whole control: frame, dropdown, borders, radii, focus ring. No Bootstrap
   is loaded and none of its internals are overridden, so a library upgrade
   cannot silently walk past a selector we happened to be reaching for.
   The short block after this is the remainder — the handful of things that
   build still hard-codes. */

:root {
  --bs-body-bg: var(--panel);
  --bs-body-color: var(--fg);
  --bs-secondary-bg: var(--rule);
  --bs-secondary-color: var(--muted);
  --bs-tertiary-bg: var(--panel);
  --bs-border-color: var(--rule);
  --bs-border-color-translucent: var(--rule);
  --bs-border-width: 1px;
  --bs-border-radius: 8px;
  --bs-border-radius-sm: 6px;
  --bs-border-radius-lg: 10px;
  --bs-box-shadow-inset: none;
  --bs-form-invalid-color: var(--danger);
  --bs-form-valid-color: var(--ok);
}

/* The dropdown is rendered inside the wrapper, so it inherits the block above.
   Its surface is the raised one rather than the field's. */
.ts-dropdown {
  --bs-body-bg: var(--raised);
  z-index: 70;            /* above the map, which Leaflet puts at 0–600 */
  box-shadow: var(--shadow);
}

/* The build sets colour on all three of these in one rule, hard-coded to
   #343a40 — so the text you type and every resting option in the list came out
   near-black on a near-black panel. The options had no rule of their own: they
   inherit it from .ts-dropdown. Same three selectors, so this replaces it
   rather than fighting it. */
.ts-dropdown, .ts-control, .ts-control input { color: var(--fg); }

.ts-wrapper .ts-control { min-height: 2.5rem; font-size: 1rem; }  /* under 16px, iOS zooms on focus */
.ts-wrapper.focus .ts-control { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }

/* 44px rows, so a thumb hits one rather than the one beside it. */
.ts-dropdown .option { min-height: 2.75rem; display: flex; align-items: center; }
.ts-dropdown .active { color: var(--fg); background: var(--ring); }

/* --- what the Bootstrap build still hard-codes ------------------------------- */

/* The chips: #efefef on #343a40, invisible either way in dark. */
.ts-wrapper.multi .ts-control > .item {
  color: var(--fg);
  background: var(--rule);
  border-radius: 999px;
}

.ts-wrapper.multi .ts-control > .item.active {
  color: #fff;
  background: var(--accent);
}

/* Its divider between the label and the ×. */
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove { border-left-color: var(--rule); }
.ts-wrapper.plugin-remove_button .item .remove:hover { background: rgba(127, 127, 127, .28); }

/* The clear-all ×. The build hides it at opacity 0 until the control is focused
   or hovered, which on a touch screen means never — there is no hover, and a
   control you have not focused is exactly when you want to see that there is
   something to clear. Shown whenever there is anything to clear. */
.ts-wrapper.plugin-clear_button.has-items .clear-button {
  opacity: 1;
  transition: none;   /* it either applies or it does not; a half-second fade on
                         a control you are looking at is just a delay */
  font-size: 1.5rem;
  line-height: 1;
  color: var(--muted);
  background: transparent;
}

.ts-wrapper.plugin-clear_button.has-items .clear-button:hover { color: var(--fg); }

/* The placeholder is an attribute the library sets once and never clears, so
   "All groups" went on being painted beside the chips once groups were chosen —
   saying the opposite of what the control showed. Hidden rather than removed,
   because the attribute is also what a screen reader announces for the box. */
.ts-wrapper.has-items .ts-control > input::placeholder { color: transparent; }
.ts-wrapper.has-items .ts-control > input::-ms-input-placeholder { color: transparent; }

/* A yellow highlight on the matched text, which reads as a smear on a dark row. */
.ts-dropdown [data-selectable] .highlight {
  background: transparent;
  color: var(--accent);
  font-weight: 700;
}

.ts-dropdown .optgroup-header { color: var(--muted); background: transparent; }
.ts-dropdown .create, .ts-dropdown .active.create { color: var(--accent); }
.ts-dropdown .create strong { color: var(--fg); }
.ts-dropdown .spinner::after { border-color: var(--accent) transparent var(--accent) transparent; }

label { display: block; font-size: .6875rem; font-weight: 600; color: var(--muted); margin-bottom: .25rem; }

.card {
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--raised);
  box-shadow: var(--shadow);
  padding: .9rem;
  display: grid;
  gap: .6rem;
}

/* A label-and-value list: who you are signed in as, what a beacon's fixed
   details are. The pairs are spans rather than a <dl> because the label column
   is one grid on the list itself and each .fact only groups its two cells —
   display:contents hands them straight to that grid, so every value on the list
   starts at the same x no matter how long its own label is. A per-row grid
   would size each label column to its own row and leave the values ragged.

   Account rendered these as bare spans for want of this rule, which put
   "Emailppatel@..." on the page: two spans with nothing between them are one
   run of inline text. */
.facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: .3rem .9rem;
  font-size: .8125rem;
}

.facts .fact { display: contents; }

/* Standing on its own, outside a .facts list — the beacon cards build one row
   at a time straight into the card. Each row is then its own grid and cannot
   see its neighbours' labels, so the label column gets a floor to align on
   instead: without one, four rows size four different columns and the values
   come out ragged. Longer than the floor and that row alone gives way. */
.fact {
  display: grid;
  grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
  gap: .3rem .9rem;
  font-size: .8125rem;
}

.fact > span:first-child { color: var(--muted); }

/* An email or an id has no spaces to break at and must not widen the column. */
.fact > span + span { overflow-wrap: anywhere; }

.pill {
  display: inline-block;
  font-size: .6875rem;
  padding: .05rem .45rem;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--panel);
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}

.pill.active { color: var(--ok); border-color: currentColor; }

.rows { border: 1px solid var(--rule); border-radius: 12px; overflow: hidden; }
.empty { padding: 2rem 1rem; text-align: center; color: var(--muted); font-size: .875rem; }

/* --- creating things -------------------------------------------------------
   A list ends with the way to add to it, rather than opening with a form for
   something you are usually not there to do. Dashed and quiet so it reads as an
   invitation at the end of the list and not as a row of it; full width and 44px
   tall so it is a thumb target. */

.addrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  width: 100%;
  margin-top: .6rem;
  min-height: 2.75rem;
  color: var(--accent);
  background: transparent;
  border: 1px dashed var(--rule);
  border-radius: 10px;
}

.addrow:hover, .addrow:focus-visible { border-color: var(--accent); background: var(--panel); }
.addrow svg { width: 1.05rem; height: 1.05rem; }

/* Where that form went. A dialog is only on the page while it is open, so it
   cannot be left behind empty or scrolled past — and Escape, the backdrop and
   the focus trap are the platform's rather than ours. */
dialog.modal {
  width: min(30rem, calc(100vw - 2rem));
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--raised);
  color: var(--fg);
  box-shadow: var(--shadow);
  padding: 1.1rem;
  display: grid;
  gap: .7rem;
}

/* A closed dialog is display:none by UA rule, which the display:grid above
   would otherwise outrank — the same trap [hidden] falls into at the top. */
dialog.modal:not([open]) { display: none; }

dialog.modal::backdrop { background: rgba(0, 0, 0, .55); }
dialog.modal h2 { font-size: 1.0625rem; }
dialog.modal form { display: grid; gap: .7rem; }

.modal-actions { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: flex-end; }
.modal-actions button { flex: 1 1 auto; }

/* Side by side once there is room for both to stay comfortable. */
@media (min-width: 24rem) {
  .modal-actions button { flex: 0 0 auto; min-width: 6rem; }
}

/* Wide content — tables above all — scrolls inside its own box so the page
   itself never scrolls sideways on a phone. */
.scroll { overflow-x: auto; border: 1px solid var(--rule); border-radius: 12px; }
table { border-collapse: collapse; width: 100%; font-size: .875rem; }

th, td {
  text-align: left;
  padding: .55rem .7rem;
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}

th {
  font-size: .6875rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  background: var(--panel);
  white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }

/* Tables become cards on a phone. One DOM, two layouts: each cell carries its
   column name in data-label, which the card layout shows as a caption, so the
   narrow view needs no second render path and cannot drift from the wide one. */
@media (max-width: 55rem) {
  .scroll.cards { border: 0; border-radius: 0; overflow: visible; }
  .scroll.cards table, .scroll.cards thead, .scroll.cards tbody,
  .scroll.cards tr, .scroll.cards th, .scroll.cards td { display: block; }
  .scroll.cards thead { display: none; }

  .scroll.cards tbody tr {
    border: 1px solid var(--rule);
    border-radius: 12px;
    padding: .3rem .2rem;
    margin: 0 0 .6rem;
  }

  .scroll.cards td {
    display: grid;
    grid-template-columns: 7rem 1fr;
    gap: .75rem;
    align-items: center;
    padding: .4rem .6rem;
    border-bottom: 0;
  }

  .scroll.cards td::before {
    content: attr(data-label);
    font-size: .6875rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 600;
  }

  .scroll.cards td.actions-cell { grid-template-columns: 1fr; }
  .scroll.cards td.actions-cell::before { content: none; }
  .scroll.cards td.empty { display: block; }
  .scroll.cards td.empty::before { content: none; }
}

/* --- indoor mapping ----------------------------------------------------------
   OpenStreetMap indoor data, rendered by indoorequal's own viewer. Almost nothing
   is needed here: the geometry is drawn by maplibre from indoorequal's style, and
   the level control is theirs. What is left is making their control look like it
   belongs on this page, and the one thing that is genuinely ours — how a beacon on
   another storey is drawn. */

/* The maplibre canvas is a Leaflet layer, so it inherits the map's rounded corner
   and must not intercept the gestures Leaflet is listening for. */
.leaflet-maplibre-gl-layer canvas { border-radius: 14px; }
.leaflet-maplibre-gl-layer { pointer-events: none; }

/* indoorequal's level control, with our caption above it. Its own stylesheet sizes
   the buttons and marks the active one; this themes the frame so it does not sit on
   the map as a white slab in dark mode. Their class names, so a library upgrade that
   renames them shows up as an unstyled control rather than as a silently wrong one. */

/* The caption sits in the same card as the buttons, so the two read as one control
   rather than as a stray word next to a stray column of numbers. */
.lvl-caption {
  padding: .25rem .4rem .2rem;
  background: var(--raised);
  border: 1px solid var(--rule);
  border-bottom: 0;
  border-radius: 8px 8px 0 0;
  color: var(--muted);
  font-size: .625rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: center;
}

/* Square off the top of the button group, since the caption now caps it. */
.lvl-wrap .maplibregl-ctrl-group.maplibregl-ctrl-indoorequal {
  margin: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.maplibregl-ctrl-group.maplibregl-ctrl-indoorequal {
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: var(--shadow);
  overflow: hidden;
}

.maplibregl-ctrl-indoorequal button {
  color: var(--fg);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  min-height: 2rem;
  font: inherit;
  font-size: .8125rem;
  font-weight: 650;
  /* Tabular, so a column of storey numbers lines up rather than shimmering. */
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.maplibregl-ctrl-indoorequal button:last-child { border-bottom: 0; }
.maplibregl-ctrl-indoorequal button:hover { background: var(--panel); }

/* Their active class, whose own rule is a fixed grey that reads as disabled on a
   dark page. */
.maplibregl-ctrl-indoorequal .maplibregl-ctrl-active {
  color: #fff;
  background: var(--accent);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* --- authenticator enrolment ------------------------------------------------
   Used by the sign-in, invitation and recovery pages as well as by Account, so
   it lives here rather than in any one of them. */

/* The QR is given a white plate of its own in both themes. A dark-mode page
   that inverted it would produce a symbol most scanners refuse, and one drawn
   flush to a coloured background often will not read either — the quiet zone is
   part of the code. */
.qr {
  display: flex;
  justify-content: center;
  margin: 0 0 1rem;
}

.qr svg {
  width: min(14rem, 60vw);
  height: auto;
  border-radius: 8px;
  border: 1px solid var(--rule);
}

.manual { margin: 0 0 1.25rem; font-size: .875rem; }
.manual summary { cursor: pointer; color: var(--muted); }
.manual summary:hover { color: var(--fg); }

/* Wide enough to be read aloud, spaced enough to keep your place while typing
   it into a phone. Wrapping is allowed: thirty-two characters do not fit on a
   narrow screen and a horizontal scrollbar on a value someone is transcribing
   is worse than two lines. */
.key {
  margin: .6rem 0 .35rem;
  padding: .55rem .7rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--panel);
  letter-spacing: .06em;
  overflow-wrap: anywhere;
}

/* Two columns of five, which is how a printed sheet of ten reads. */
.codes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: .35rem 1rem;
  margin: 0 0 1rem;
  padding: .9rem 1rem .9rem 2.5rem;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--panel);
  font-size: .9375rem;
  letter-spacing: .04em;
}

.confirm {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 0 0 .75rem;
  font-size: .875rem;
  font-weight: 400;
  color: var(--fg);
  text-transform: none;
  letter-spacing: 0;
}

.confirm input { width: auto; margin: 0; }

/* A second table under its own heading on a page that already has one — the
   pending invitations under the member list. Enough space above it to read as a
   separate thing, without the weight of another <h1>. */
h2.section {
  font-size: 1.0625rem;
  margin: 1.75rem 0 .35rem;
}

/* --- telemetry ---------------------------------------------------------------
 *
 * Battery and die temperature, on the beacon card and in the trend dialog.
 *
 * The series colours are the one place in this stylesheet that names a palette
 * rather than a role, and both modes are chosen rather than derived: the dark
 * steps are the same two hues re-stepped for the dark surface, because a light
 * hue dimmed algorithmically lands outside the band where it stays legible on
 * #11161d. Blue is always battery and orange is always temperature — the hue
 * follows the measure, so a chart that loses one does not repaint the other. */
:root {
  --series-batt: #2a78d6;
  --series-temp: #eb6834;

  /* Grid and axis rules sit one shade off the surface, solid and hairline.
     Dashing them would read as "threshold" when it is only a grid. */
  --viz-grid: rgba(11, 15, 20, .10);
}

@media (prefers-color-scheme: dark) {
  :root {
    --series-batt: #3987e5;
    --series-temp: #d95926;
    --viz-grid: rgba(230, 237, 243, .13);
  }
}

/* A fact whose value opens something. It has to read as activatable without
   becoming a button-shaped object in a list of plain rows, so it borrows the
   text colour and earns an underline on hover — and it carries a real focus
   ring, because this is reachable by keyboard and is the only way in. */
.fact-link {
  font: inherit;
  font-size: inherit;
  font-weight: inherit;
  color: var(--fg);
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  border-radius: 4px;
}

.fact-link:hover .fact-value { text-decoration: underline; }
.fact-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fact-link[disabled] { cursor: default; color: var(--muted); }
.fact-link[disabled]:hover .fact-value { text-decoration: none; }

/* The dot is the second encoding: it ties the value on the card to the line in
   the dialog without either of them relying on colour alone to say which
   measure it is — the label beside it already does that. */
.fact-dot { width: .5rem; height: .5rem; border-radius: 50%; flex: none; }
.fact-dot.batt { background: var(--series-batt); }
.fact-dot.temp { background: var(--series-temp); }

/* A reading nobody has refreshed in a while. Dimmed rather than hidden: the
   number is still the last thing the beacon said. */
.fact-stale { color: var(--muted); }

/* The trend dialog is wider than the rest — two stacked plots need the width,
   and squeezing them into the 30rem the other modals use would put the x-axis
   ticks on top of each other. */
dialog.modal.wide { width: min(46rem, calc(100vw - 2rem)); }

.viz-head {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: center;
  justify-content: space-between;
}

/* Range and view controls in one row above the plots, not beside each one. */
.viz-controls { display: flex; flex-wrap: wrap; gap: .35rem; }

.viz-controls button {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--rule);
  padding: .3rem .6rem;
  font-size: .8125rem;
  font-weight: 500;
}

.viz-controls button[aria-pressed="true"] {
  color: var(--fg);
  border-color: var(--accent);
  background: var(--panel);
}

.viz-chart { position: relative; }

/* Sized to include the x-axis band. A fixed height that fits only the plot is
   how a card ends up with a tiny nested scrollbar under the ticks. */
.viz-chart svg { display: block; width: 100%; height: auto; }

.viz-title {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  font-size: .8125rem;
  color: var(--muted);
  margin-bottom: .15rem;
}

.viz-title b { color: var(--fg); font-size: .875rem; font-weight: 600; }

/* Follows the crosshair. Pointer-events off so it can never eat the move
   events that position it. */
.viz-tip {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: var(--shadow);
  padding: .35rem .5rem;
  font-size: .75rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  transform: translate(-50%, -130%);
}

.viz-tip[hidden] { display: none; }
.viz-tip b { font-weight: 600; }
.viz-tip span { color: var(--muted); }

.viz-empty {
  color: var(--muted);
  font-size: .8125rem;
  padding: 1.2rem 0;
  text-align: center;
}

/* The table view. Present for every chart, because a tooltip may enhance a
   value but must never be the only way to read one. */
.viz-table { max-height: 16rem; overflow: auto; }
.viz-table table { font-variant-numeric: tabular-nums; }

/* Axis and grid text. Tabular figures here, where numbers stack vertically. */
.viz-axis {
  font-size: 10px;
  fill: var(--muted);
  font-variant-numeric: tabular-nums;
}

.viz-gridline { stroke: var(--viz-grid); stroke-width: 1; }
.viz-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.viz-crosshair { stroke: var(--muted); stroke-width: 1; }

/* A 2px surface ring on the hovered point, rather than a border drawn round
   every mark. */
.viz-dot { stroke: var(--raised); stroke-width: 2; }

.viz-plot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }


/* --- place search -------------------------------------------------------------
 *
 * Finding the building, inside the beacon editor. The last few metres are still
 * a drag on the map — a geocoder does not know which corridor a device is in —
 * so this is deliberately a way to get near, not a way to finish. */
.place { display: grid; gap: .35rem; }

.place-row { display: flex; gap: .4rem; }
.place-row input { flex: 1 1 auto; min-width: 0; }
.place-row button { flex: 0 0 auto; }

/* Results are a list of choices, not a dropdown: they sit in the card, push the
   buttons down, and stay until the search is run again. An overlay would cover
   the coordinate readout, which is the thing somebody is checking against. */
.place-results {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .2rem;
  max-height: 11rem;
  overflow-y: auto;
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: .25rem;
}

.place-result {
  font: inherit;
  font-size: .8125rem;
  font-weight: 400;
  text-align: left;
  width: 100%;
  color: var(--fg);
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: .4rem .5rem;
  cursor: pointer;

  /* A display_name runs to several lines of administrative hierarchy. Two lines
     is enough to tell candidates apart; the rest is in the title tooltip. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.place-result:hover { background: var(--panel); }
.place-result:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Required by the geocoder's terms, and it belongs with the results rather than
   in a corner of the page — it credits these rows, not the basemap, which
   carries its own attribution on the map itself. */
.attrib { font-size: .6875rem; color: var(--muted); margin: 0; }
