/* Mobile-first. The primary client is a phone that has just scanned a QR code.
 *
 * Palette follows www.ladisk.si: brand red #CF1D2D, light band #F8F8F8. The
 * site uses the red strictly as rules and accents — a 2px border under the
 * navbar, 2px bands around headings, the footer's top rule, nav hover/active —
 * and never as a fill; its buttons and body links stay blue. That is copied
 * deliberately rather than tidied up, because here it also keeps saturated red
 * free to mean *overdue*, which is the one colour signal this app cannot
 * afford to blur.
 */

:root {
  color-scheme: light;

  --brand: #cf1d2d;       /* ladisk.si rules and accents */
  --brand-ink: #cf1d2d;   /* brand red as text — darkened in dark mode */
  --bg: #ffffff;
  --band: #f8f8f8;        /* ladisk.si heading/thumbnail band */
  --fg: #1a1d21;
  --muted: #5c6672;
  --line: #dfe3e8;
  --accent: #14507d;      /* interactive fills: buttons, links, selection */
  --accent-ink: #ffffff;  /* text on top of an --accent fill */
  --field: #ffffff;
  --ok: #1f7a4d;
  --ok-bg: #e7f5ee;
  --ok-ink: #ffffff;      /* text on top of an --ok fill */
  --warn: #8a5a00;
  --warn-bg: #fdf3e0;
  --bad: #a02020;
  --bad-bg: #fbeaea;
  --locked-bg: #eef0f3;
  --radius: 8px;
}

/* The dark palette is written twice on purpose: once for an explicit choice,
   once for the system preference when no explicit choice was made. CSS has no
   way to share one declaration block between a selector and a media query, and
   duplicating ten lines beats a build step or a light-dark() dependency. Keep
   the two blocks identical. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --brand: #e8505f;
  --brand-ink: #f08a95;
  --bg: #16181c;
  --band: #1e2126;
  --fg: #e8eaed;
  --muted: #9aa4b0;
  --line: #2c3138;
  --accent: #6fb3e8;
  --accent-ink: #10131a;
  --field: #1e2126;
  --ok: #6ed69f;
  --ok-bg: #1a3326;
  --ok-ink: #10131a;
  --warn: #e8bd6f;
  --warn-bg: #3a2c0f;
  --bad: #f08a8a;
  --bad-bg: #3a1d1d;
  --locked-bg: #262a30;
  /* Dark needs roughly twice the mix for the same apparent strength: any
     colour laid over near-black reads far weaker than over near-white. */
  --lab-tint-mix: 26%;
  /* How far a category's stored colour is lifted toward white here. It was
     chosen on a white page, so unlifted it sits at roughly the contrast of the
     background it is now on. */
  --cat-lift: 45%;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --brand: #e8505f;
    --brand-ink: #f08a95;
    --bg: #16181c;
    --band: #1e2126;
    --fg: #e8eaed;
    --muted: #9aa4b0;
    --line: #2c3138;
    --accent: #6fb3e8;
    --accent-ink: #10131a;
    --field: #1e2126;
    --ok: #6ed69f;
    --ok-bg: #1a3326;
    --ok-ink: #10131a;
    --warn: #e8bd6f;
    --warn-bg: #3a2c0f;
    --bad: #f08a8a;
    --bad-bg: #3a1d1d;
    --locked-bg: #262a30;
    --lab-tint-mix: 26%;
    --cat-lift: 45%;
  }
}

* { box-sizing: border-box; }

/* The UA rule for [hidden] is display:none at low specificity, so any element
   rule that sets display beats it. `button { display: inline-block }` does
   exactly that, and several no-JavaScript fallbacks are buttons hidden by script
   once it has run — the "Save" beside the role dropdown, for one. Without this
   reset they stay on screen, on every viewport. */
[hidden] { display: none !important; }

/* Defaults to the background itself, so mixing it changes nothing until a lab
   supplies a colour. Set as an inline custom property on <body>. */
:root { --lab-tint: var(--band); --lab-tint-mix: 14%; }

/* The brand red the Category field defaults to, written out rather than taken
   from --brand-ink: this one is a *stored* colour that dark lifts, and reading
   the theme's own red here would lift a colour that was already lightened. A
   list row overrides it per category; nothing else uses it. */
:root { --cat-colour: #cf1d2d; --cat-lift: 0%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}

a { color: var(--accent); }

main {
  max-width: 60rem;
  margin: 0 auto;
  padding: 1rem 1rem 3rem;
}

/* --- top bar --------------------------------------------------------- */
/* The 2px red underline is ladisk.si's most recognisable element. */
.topbar {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem .75rem;
  align-items: center;
  justify-content: space-between;
  padding: .6rem 1rem;
  /* Notched phones: keep the bar clear of the rounded corners. */
  padding-left: max(1rem, env(safe-area-inset-left));
  padding-right: max(1rem, env(safe-area-inset-right));
  /* The lab's colour lives here rather than on the page behind it. A whole
     page washed in red read as a warning at any strength that was visible at
     all, which is the one thing this palette cannot afford to say by accident —
     saturated red means overdue. A band can carry more colour and say less,
     and it sits beside the lab's name, which is what it is labelling.

     Computed rather than stored: a tint tuned for the light band is invisible
     on the dark one, so the stylesheet mixes the brand colour into whichever
     band is in force. Browsers without color-mix fall through to the plain band.

     Deliberately not the page: --bg stays exactly what it was. */
  background: var(--band);
  background: color-mix(in srgb, var(--lab-tint) var(--lab-tint-mix), var(--band));
  border-bottom: 2px solid var(--brand);
}
.brand-block { display: flex; align-items: center; gap: .6rem; min-width: 0; }
/* Quiet, like the signed-in name it echoes: a readout, not a navigation item. */
.lab-name {
  color: var(--muted); font-size: .85rem; white-space: nowrap;
  padding-left: .6rem; border-left: 1px solid var(--line);
}

.brand {
  display: inline-flex; align-items: center; gap: .5rem;
  font-weight: 600; text-decoration: none; color: var(--fg);
}
.brand:hover { color: var(--brand-ink); }
.brand-logo { height: 2.2rem; width: auto; display: block; }

/* The wordmark's "www." and ".si" are near-black, so on a dark bar they vanish
   and only the red waveform survives. A light chip keeps the logo exactly as
   drawn; filtering or inverting it would wreck the brand red. Written twice for
   the same reason as the palette above — explicit choice, then system default. */
:root[data-theme="dark"] .brand-logo {
  background: #fff;
  border-radius: 4px;
  padding: 2px 4px;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-logo {
    background: #fff;
    border-radius: 4px;
    padding: 2px 4px;
  }
}
/* On a phone the brand takes the first row and the whole of nav the second, so
   nav must fit 358 px on its own — it did not, and the language select and theme
   toggle were pushed off-screen entirely. */
.topbar nav {
  display: flex; gap: 1rem; align-items: center;
  flex-wrap: wrap; min-width: 0;
}
.topbar nav a, .topbar nav button, .topbar nav summary { white-space: nowrap; }
.topbar nav a { color: var(--fg); text-decoration: none; font-size: .95rem; }
@media (max-width: 40rem) {
  .topbar { gap: .35rem .6rem; }
  /* .55rem, not .75: nav has 358 px on a phone and its contents measured 361
     with the manager's "Manage ①" summary, which is wider than the admin's
     "Admin" — so the theme toggle alone wrapped to a third row. Four gaps at
     .2rem less buys 13 px, which clears it in English, Slovenian and Italian. */
  .topbar nav { gap: .55rem; width: 100%; justify-content: flex-start; }
  /* Truncated to "ad..." at this width — noise, and the name is on Mine. */
  .whoami { display: none; }
  .brand-logo { height: 1.9rem; }
  .brand span { font-size: .95rem; }
}
.topbar nav a:hover { color: var(--brand-ink); text-decoration: underline; }
.inline { display: inline; margin: 0; }
/* Signed-in identity: present, but never competing with the nav links. */
.whoami {
  color: var(--muted); font-size: .85rem;
  max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.linkish {
  background: none; border: 0; padding: 0;
  color: inherit; font: inherit; font-size: .95rem; cursor: pointer;
}
.topbar .linkish:hover { color: var(--brand-ink); text-decoration: underline; }

/* Language picker. Narrow, and it must not inherit the full-width rule that
   applies to every other select on the site. */
.lang-form { display: flex; gap: .3rem; align-items: center; margin: 0; min-width: 0; }
/* Flag plus code, so the control is the same narrow width in every language and
   stays that width as languages are added. No max-width: there is nothing left to
   truncate, and the truncation is what used to make this look broken. */
.lang-form select {
  width: auto; padding: .25rem 1.4rem .25rem .4rem;
  /* 16px, not smaller: iOS Safari zooms the whole page when a control with a
     font under 16px takes focus, and the user has to pinch back out. */
  font-size: 1rem; border-radius: var(--radius);
}
.lang-go { padding: .2rem .5rem; font-size: .85rem; }

/* For labels that screen readers need and sighted users do not. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* Light/dark switch. One button cycling auto -> light -> dark; the icon shows
   the current state, not the state clicking would move to. Which glyph is
   visible is decided in CSS from data-mode, so the button is never blank. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; padding: 0;
  background: none; border: 1px solid var(--line); border-radius: 999px;
  color: var(--muted); cursor: pointer;
}
.theme-toggle:hover { border-color: var(--brand); color: var(--brand-ink); }
.theme-toggle svg {
  display: none; width: 1.15rem; height: 1.15rem;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The help link borrows the toggle's round control so that it reads as chrome
   rather than as another place to go. Its glyph is always shown; the toggle's
   are switched by data-mode below. */
.help-link { text-decoration: none; }
.help-link svg { display: block; }

.theme-toggle[data-mode="auto"] .i-auto,
.theme-toggle[data-mode="light"] .i-light,
.theme-toggle[data-mode="dark"] .i-dark { display: block; }

footer {
  max-width: 60rem;
  margin: 2rem auto 0;
  padding: 1rem;
  color: var(--muted);
  font-size: .85rem;
  border-top: 2px solid var(--brand);
  /* Two items: the provenance note and the link to the guide. They sit on one
     row when there is room and stack on a phone, where the note is long. */
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 1rem;
  justify-content: space-between;
}

/* --- messages -------------------------------------------------------- */
.messages { list-style: none; margin: 0; padding: .5rem 1rem; }
.msg { padding: .6rem .8rem; border-radius: var(--radius); margin-bottom: .4rem; }
.msg.success { background: var(--ok-bg); color: var(--ok); }
.msg.error { background: var(--bad-bg); color: var(--bad); }
.msg.warning { background: var(--warn-bg); color: var(--warn); }

/* "Checking out into Modal test", with an × to end it. One line, in the quiet
   band a notice uses rather than the warning colour it had first: a red strip
   across every page reads as a problem somebody has to solve, and this is a
   convenience somebody switched on. */
.into-experiment { display: flex; align-items: center; gap: .6rem; }
.into-experiment > a { flex: 1 1 auto; color: inherit; }
.into-experiment form { margin: 0; }
/* An × rather than a worded button: it sits on every page, and the word would
   be the widest thing in a strip whose whole point is being one line. The
   accessible name is on the button, so it is only the *visible* label that is a
   symbol. */
.into-experiment-stop {
  background: none; border: 0; color: inherit; cursor: pointer;
  padding: 0 .3rem; font-size: 1.3rem; line-height: 1; opacity: .7;
}
.into-experiment-stop:hover { opacity: 1; }

/* --- status pills ---------------------------------------------------- */
.pill {
  display: inline-block;
  padding: .3rem .7rem;
  border-radius: 999px;
  font-size: .8rem;
  line-height: 1.35;
  font-weight: 600;
  white-space: nowrap;
}
.pill.available { background: var(--ok-bg); color: var(--ok); }
.pill.out { background: var(--warn-bg); color: var(--warn); }
.pill.overdue { background: var(--bad-bg); color: var(--bad); }
.pill.locked { background: var(--locked-bg); color: var(--muted); }
/* A due pill on Mine leads to the date it is showing — that is where somebody
   is looking when they notice it is wrong. Undecorated at rest so a list of
   eight still reads as eight readouts, underlined on hover like the tag pills
   above. The colour is the pill's own and keeps its meaning: red is overdue
   here as everywhere, not "this is a link". */
a.pill { text-decoration: none; }
a.pill.out:hover, a.pill.overdue:hover { text-decoration: underline; }

/* --- forms and buttons ----------------------------------------------- */
label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: .2rem; }
/* Broad, not per-type: allauth renders email and password fields we do not
   author ourselves, and they must not fall back to browser defaults. */
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]),
select, textarea {
  width: 100%;
  padding: .55rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: inherit;
  background: var(--field);
  color: inherit;
}
/* A swatch, not a text box. Excluded from the broad rule above rather than
   overriding it: that selector carries three :not()s and so outranks anything
   written as input[type=color], which is how this first shipped as a
   full-width block of one colour. Tall enough to be a comfortable tap target
   on a phone and no wider than it needs to be. */
input[type=color] { width: 4rem; height: 2.6rem; padding: .2rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--field); }
.field { margin-bottom: .9rem; }
.help { color: var(--muted); font-size: .85rem; font-weight: normal; }
.errorlist { color: var(--bad); font-size: .85rem; margin: .25rem 0 0; padding-left: 1rem; }

button, .button {
  display: inline-block;
  padding: .6rem 1rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
button.secondary, .button.secondary {
  background: transparent; color: var(--accent);
}
/* Brand red is reserved for "overdue", so a destructive button borrows the
   status red instead — and only as text on a transparent field, never a fill. */
button.secondary.danger { color: var(--bad); }
/* .linkish and .theme-toggle are buttons only in the HTML sense. */
button.linkish { padding: 0; font-weight: inherit; }

/* --- equipment detail ------------------------------------------------ */
/* Echoes ladisk.si's .title-heading: a light band ruled in brand red. */
.item-head {
  background: var(--band);
  border-top: 2px solid var(--brand);
  border-bottom: 2px solid var(--brand);
  padding: .6rem .8rem;
  margin-bottom: 1rem;
}
.item-head h1 { margin: .2rem 0; font-size: 1.4rem; }
.pubid { font-family: ui-monospace, "SF Mono", Menlo, monospace; color: var(--muted); }

.status-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .9rem;
  margin-bottom: 1.25rem;
}
/* Checking out is the reason this page exists, so when it is possible the card
   carries a brand-red edge and reads as the page's subject rather than one
   panel among several. */
.status-card.primary {
  border-left: 4px solid var(--brand);
  background: var(--band);
  padding: 1.1rem;
}
.checkout-form { margin-top: 1rem; }
.checkout-fields { display: grid; gap: .9rem; }
@media (min-width: 34rem) {
  .checkout-fields { grid-template-columns: 2fr 1fr; align-items: start; }
}
.checkout-note { margin: .2rem 0 1rem; }
.checkout-note summary {
  cursor: pointer; font-size: .9rem; color: var(--accent);
  width: max-content; padding: .2rem 0;
}
.checkout-note textarea { margin-top: .4rem; }
/* The start day, quieter still than the note: muted rather than accent, because
   accent reads as "there is something for you here" and for almost every
   checkout there is not. */
.checkout-later { margin: .2rem 0 .6rem; }
.checkout-later summary {
  cursor: pointer; font-size: .85rem; color: var(--muted);
  width: max-content; padding: .2rem 0;
}
.checkout-later .field { margin-top: .4rem; }
/* The one action people came here for: full width on a phone, hard to miss. */
.primary-action {
  width: 100%; padding: .85rem 1rem; font-size: 1.05rem;
}
@media (min-width: 34rem) { .primary-action { width: auto; min-width: 14rem; } }

/* Sits above the listing, so it needs to read as the page's one obvious action
   without shoving the results below the fold on a phone. text-align keeps the
   label centred while the anchor is full-width. */
.add-equipment { text-align: center; margin-bottom: 1rem; }


/* A panel: the one block a page is built out of. See "Page structure" in
   UI.md for why there is exactly one of these rather than a section style
   per page.
   
   The whole point is that a panel that folds and a panel that does not are the
   same thing with one property different. So <section class="panel"> and
   <details class="panel"> get identical rules, and the chevron is the only
   thing that tells them apart — never the rule, the spacing or the heading. */
.panel { border-top: 1px solid var(--line); margin-top: 1.5rem; }
.panel-head {
  display: flex; align-items: baseline; gap: .4rem 1rem; flex-wrap: wrap;
  /* The gutter is reserved on every panel, folding or not, so that titles sit
     on one left edge down the page. A chevron that pushed only some of them
     across is the inconsistency this replaced. */
  padding: .8rem .2rem .8rem 1.3rem;
  margin: 0; font-size: 1.1rem; font-weight: 600;
}
.panel-head > :first-child { flex: 1 1 auto; margin: 0; font: inherit; }
/* The section's own action, on its heading row because that is what it belongs
   to. Smaller than a page's primary action and still a comfortable tap. */
/* The button that starts collecting mode wears the mode's own green — the same
   green as the banner that shows while the mode is on, so pressing it visibly
   becomes the state it caused. Green is spent on this and on success notes;
   nothing else fills with it. */
button.go, .button.go {
  background: var(--ok); border-color: var(--ok); color: var(--ok-ink);
}
.panel-head .button,
.panel-head form button,
.head-row .button { font-size: .9rem; padding: .45rem .8rem; font-weight: 600; width: auto; }
.panel-head form { margin: 0; flex: 0 0 auto; }
/* A page header carrying its own act: the meta facts left, the act hard right,
   the same size and weight as an act on a panel's heading row. One treatment,
   whichever row it sits on. */
.head-row { display: flex; align-items: center; gap: .4rem 1rem; flex-wrap: wrap; }
.head-row .meta { flex: 1 1 auto; margin: 0; }
/* The left side takes the slack, whatever it is. On a page header that side is
   a .meta and the rule above already says so; in the loan's detail list it is
   the date itself, which is ordinary text and must not be greyed to earn its
   place in the row. */
.head-row > :first-child { flex: 1 1 auto; }

details.panel > summary.panel-head {
  cursor: pointer; list-style: none; position: relative;
}
details.panel > summary.panel-head::-webkit-details-marker { display: none; }
details.panel > summary.panel-head::marker { content: ""; }
/* Our own triangle, pointing the same way in every browser and matching the
   sidebar's. */
details.panel > summary.panel-head::before {
  content: "▾"; position: absolute; left: .3rem;
  font-size: .7rem; color: var(--muted);
}
details.panel:not([open]) > summary.panel-head::before { content: "▸"; }
details.panel > summary.panel-head:hover { color: var(--brand-ink); }

/* One setting: what it is, the control, and the line explaining it. Repeated
   identically however many there are — two settings drawn two different ways,
   one of them boxed, was the page telling the reader about a distinction that
   does not exist. */
.setting { padding: .2rem .2rem 1rem; }
.setting + .setting { border-top: 1px solid var(--line); padding-top: 1rem; }
.setting .row { display: flex; gap: .6rem; align-items: flex-end; flex-wrap: wrap; }
.setting .row .field { flex: 1 1 14rem; margin: 0; }
.setting .help { margin: .3rem 0 0; }
/* Each section draws its own rule on top, so the list above it must not draw
   one underneath: two lines with a gap between them read as a section whose
   heading is missing. Worst with an empty list, where the gap is the 2rem of
   `.empty` — "You have nothing checked out" then a band of nothing. */
.mine-list li:last-child, .feed li:last-child { border-bottom: none; }


/* Staff menu. <details> so it opens without JavaScript; the script in base.html
   only adds close-on-outside-click, which is a nicety rather than the mechanism. */
.menu { position: relative; }
.menu > summary {
  cursor: pointer; list-style: none; font-size: .95rem;
  display: inline-flex; align-items: center; gap: .35rem;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::after { content: "▾"; font-size: .7rem; color: var(--muted); }
.menu > summary:hover { color: var(--brand-ink); }
.menu > ul {
  position: absolute; right: 0; top: calc(100% + .45rem); z-index: 20;
  min-width: 12rem; max-width: calc(100vw - 2rem); margin: 0; padding: .25rem;
  list-style: none; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 6px 20px rgb(0 0 0 / .18);
}
/* On a phone the button sits at the left of its row, so anchoring the panel's
   right edge to it threw the panel off the left of the screen. Position it
   against the bar instead and let it span the width — the containing block is
   whichever ancestor is positioned, so .menu has to go static for that. */
@media (max-width: 40rem) {
  .topbar { position: relative; }
  .menu { position: static; }
  .menu > ul {
    left: 1rem; right: 1rem; max-width: none;
    top: calc(100% + .3rem);
  }
}
/* flex-start, not space-between: with an icon there are three children, and
   space-between would strand the label in the middle. The badge takes the right
   edge for itself instead. */
.menu > ul a {
  display: flex; justify-content: flex-start; align-items: center; gap: .5rem;
  padding: .45rem .6rem; border-radius: var(--radius);
  text-decoration: none; color: var(--fg); font-size: .9rem; white-space: nowrap;
}
.menu > ul a .badge { margin-left: auto; }
.menu > ul a:hover { background: var(--band); color: var(--brand-ink); }

/* Drawn, not fetched: eight glyphs do not justify an icon font, and they
   inherit currentColor so both themes and the hover colour work unchanged.
   Slightly faded, so the menu still reads as a list of words. */
.menu-icon {
  width: 1.05rem; height: 1.05rem; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round; opacity: .7;
}
.menu > ul a:hover .menu-icon { opacity: 1; }

.badge {
  display: inline-block; min-width: 1.25rem; padding: 0 .35rem;
  border-radius: 999px; background: var(--brand); color: #fff;
  font-size: .75rem; font-weight: 700; text-align: center; line-height: 1.35;
}

.approvals li {
  display: flex; gap: .5rem .8rem; align-items: center;
  justify-content: space-between; flex-wrap: wrap;
}
/* Role dropdown and, for an admin, the Block button beside it. Wraps rather
   than overflowing: three controls plus a long name do not fit 390 px. */
/* flex-end so the Block button sits on the baseline of the captioned selects
   beside it rather than floating level with their captions. */
.user-actions { display: flex; gap: .5rem; align-items: flex-end; flex-wrap: wrap; }
/* Each control captioned, rather than one header row above the list. A header
   would have to line its columns up with flex children of varying width at every
   viewport, and the list reflows to a stack on a phone where a header row means
   nothing at all. The captions cost a small grey word per control and are right
   at every width — they are also the <label> the select needs regardless, so
   this replaces a visually-hidden duplicate rather than adding markup. */
.auto-submit { display: flex; flex-direction: column; gap: .15rem; margin: 0; }
/* Quiet on purpose. These repeat on every row, so at heading weight they read
   as a column of shouting rather than as labels — the values themselves ("LADISK",
   "Manager") already say most of what they are. */
.auto-submit label { font-size: .7rem; font-weight: 400; color: var(--muted); }
/* 1rem, not smaller: anything below 16px in a select makes iOS Safari zoom in
   on focus and never zoom back out. */
.auto-submit select { font-size: 1rem; padding: .35rem 1.6rem .35rem .5rem; width: 100%; }
/* Fixed per-column widths, so the same control is the same size on every row.
   Left to size themselves, selects take the width of their widest *option*, and
   the mentor list differs from row to row — the row belonging to a mentor with
   a long name came out visibly narrower than the rest, for a reason nobody
   reading the page could see. Widths are per column rather than one shared
   value because a lab name is two words and a full name is not. */
.auto-submit.col-lab, .auto-submit.col-role { width: 8rem; }
.auto-submit.col-mentor { width: 10.5rem; }
.auto-submit .auto-go { align-self: flex-start; }

/* The lab's own answer-or-not choice: radios stacked, each a full-width thumb
   target rather than a dot to hit. */
.policy-form ul { list-style: none; margin: .5rem 0; padding: 0; }
.policy-form li { margin: 0; }
.policy-form label {
  display: flex; gap: .5rem; align-items: center;
  padding: .55rem .2rem; cursor: pointer;
}
.policy-form input[type="radio"] { width: 1.15rem; height: 1.15rem; accent-color: var(--accent); }

/* The owning lab, on an item's head line and in a listing row. Neutral, not a
   status: it says whose a thing is, not whether you may have it. */
.pill.lab { background: var(--locked-bg); color: var(--muted); }

/* The legend under the list. Tooltips do not exist on a phone, and the tiers
   differ by one sentence each, so the sentences are on the page as well. */
dl.roles { margin: .5rem 0 1rem; }
dl.roles dt { font-weight: 600; margin-top: .6rem; }
dl.roles dd { margin: .1rem 0 0; color: var(--muted); font-size: .92rem; }
/* :not(.pill) matters. This targets the name/meta wrapper, but a pill is also a
   direct span child of the same li, and `.approvals li > span` outranks `.pill`
   — so the pills silently inherited .7rem/.2rem and looked squeezed sideways
   however much padding .pill asked for. */
.approvals li > span:not(.pill) { padding: .7rem .2rem; }
.approvals button { padding: .35rem .8rem; font-size: .9rem; }

/* --- the approved-users list ------------------------------------------- */
/* A card per person. As a flat list the controls sat below their own name and
   above the next one with equal space either side, so on a phone every group of
   dropdowns appeared to belong to the wrong person. Whitespace cannot fix that;
   only a boundary can say "these belong together". */
/* An experiment's plan. The unchosen entries are the same rows in a lighter
   voice: they are a to-do list beside a list of real equipment, and the
   difference has to be visible without reading the words. Dashed, because a
   dashed edge already means "not filled in" everywhere else people have seen
   one. */
/* Down the left edge rather than as a different row separator: it is a second
   fact about a row, and the same vocabulary the notification feed already uses
   for "not dealt with yet". Dashed because a dashed edge reads as "not filled
   in" without a word being spent on it. */
.plan-list li.unchosen { border-left: 3px dashed var(--line); padding-left: .6rem; }
.plan-list li.unchosen .name { color: var(--muted); font-weight: 500; }
/* Chosen but not in hands yet — booked, waiting on a blessing, or simply not
   collected. The same dashed edge as the placeholders, because it means the
   same thing ("not filled by a loan yet"), but at full colour: the choice
   itself is made, and greying a real sensor's name would say it was not. */
.plan-list li.pending { border-left: 3px dashed var(--line); padding-left: .6rem; }
/* The identity fills the line and the actions sit at the end of it. .mine-list
   does this for rows that begin with a link; an unchosen entry has nothing to
   link to yet, so it begins with a span and needs saying separately. */
.plan-list li > span:first-child { flex: 1 1 auto; }
/* The row link fills the line and the actions sit beside it — that is what
   .mine-list does. But this list's actions include anchors ("Take it out",
   "Choose one") as well as buttons, and .mine-list styles *every* anchor in a
   row as the row link, so without this they stretch the full width and swallow
   the row. Anything wearing .button is an action, whatever tag it is. */
.plan-list li a.button {
  flex: 0 0 auto; display: inline-block; width: auto;
  padding: .45rem .9rem; white-space: nowrap;
}

/* The Manage panel's rows: what will happen on the left, the button that does
   it on the right — the shape a repo's settings page uses, and the reason a
   row of bare buttons is not enough for verbs this consequential. */
.manage-list li { display: flex; gap: .6rem 1.2rem; align-items: center; padding: .55rem .2rem; }
.manage-list li > .meta { flex: 1 1 auto; min-width: 0; }
.manage-list form { margin: 0; flex: 0 0 auto; }
.manage-list .button, .manage-list button {
  flex: 0 0 auto; width: auto; white-space: nowrap;
  font-size: .9rem; padding: .45rem .8rem; font-weight: 600;
}

/* "We need three of these", under the plan it adds to. A row on a desk and a
   stack on a phone, like every other short form here. */
.plan-add {
  display: flex; flex-wrap: wrap; gap: .6rem .8rem; align-items: flex-end;
  margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--line);
}
.plan-add .field { margin: 0; }
.plan-add label { display: block; font-size: .7rem; color: var(--muted); }
.plan-add input[type=number] { width: 5rem; }

/* Experiments sit above the disclosures on Mine, so the heading has to separate
   itself from the list of loans above without looking like one of them. */
.mine-heading { margin-top: 1.8rem; }

/* How many people a folded lab holds, next to its name — so closing a section
   never hides the fact that it has contents. */
.panel-head .count { color: var(--muted); font-weight: 400; font-size: .85rem; }

.user-cards { list-style: none; margin: 0; padding: 0; }
.user-cards > li {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .8rem .9rem;
  /* The gap is most of what makes fifteen of these legible: it has to be
     plainly wider than any spacing inside a card, or the boundary between two
     people is guesswork. */
  margin-bottom: 1.25rem;
  background: var(--bg);
}
.user-cards > li.empty { border: 0; padding: .7rem .2rem; }
/* The band echoes the item-detail heading: it ties the name to the card rather
   than leaving it floating above the controls. */
.user-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .6rem;
  padding-bottom: .5rem; margin-bottom: .6rem;
  border-bottom: 1px solid var(--line);
}
.user-head .name { font-weight: 600; }
/* The name leads to the person's own page, and has to look as though it does:
   blue says link where nothing else on the card is one. Undecorated at rest so
   fifteen cards do not become fifteen underlines, underlined on hover — the
   same treatment the top bar gives its own entries. */
.user-head a.name { color: var(--accent); text-decoration: none; }
.user-head a.name:hover { text-decoration: underline; }
/* The tier, beside the name and before the address. Quiet — it is a readout
   somebody scans a column of, not a thing to press — and pushed hard against
   the name so the two read as one phrase.

   Not scoped to the user card: the person's own page carries the same pill
   under its own heading, and scoped here it fell back to the bare .pill and
   came out bolder than the name it was qualifying. One treatment, wherever
   the tier is printed. */
.pill.tier {
  font-size: .7rem; padding: .1rem .45rem; text-transform: uppercase;
  letter-spacing: .03em; color: var(--muted);
}
/* The address and the rest onto their own line, so the name and its tier are
   a heading rather than the start of a sentence that runs on. */
.user-head .meta { flex-basis: 100%; }
.user-cards .user-actions { align-items: flex-end; }
.user-cards button { padding: .35rem .8rem; font-size: .9rem; }

/* One person, one line, once the width is there for it. The card is stacked —
   name over controls — because on a phone the controls have nowhere else to go,
   and that stacking is what makes eight people a page to scroll rather than a
   list to read. Given room, the name goes beside its own controls instead and
   the internal rule is unnecessary: the card's own border is already saying
   what belongs to whom.

   Two columns were tried here first and made it worse. The problem was never
   how many cards fit across; it was how tall one is. */
@media (min-width: 64rem) {
  .user-cards > li { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem 1.2rem; }
  /* A floor, not a preference. The controls are a different width on different
     installations — the password-manager toggle is only drawn where a vault is
     configured — and with nothing but `min-width: 0` here the name was squeezed
     to about seven characters and broken across six lines, because the actions
     beside it would not give any of their width back. Below this the row wraps
     and the card stacks, which is the phone layout and is fine. */
  .user-head {
    flex: 1 1 16rem; min-width: 16rem;
    padding-bottom: 0; margin-bottom: 0; border-bottom: 0;
  }
  /* Shrinkable, so a long set of controls wraps onto a second line inside the
     card rather than eating the name. */
  .user-actions { flex: 1 1 20rem; min-width: 0; }
  /* The address is the one thing here with no length limit; wrap it rather than
     let it set the width of the column it is in. */
  .user-head .meta { overflow-wrap: anywhere; }
}

/* The "?" beside the password-manager checkbox. Quiet enough not to compete
   with the controls, big enough to be a tap target on a phone. */
.help-q {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; flex: none;
  border: 1px solid var(--line); border-radius: 999px;
  color: var(--muted); text-decoration: none; font-size: .8rem; line-height: 1;
}
.help-q:hover { border-color: var(--brand); color: var(--brand-ink); }
/* A row, against .auto-submit's column above: that stacks a caption over a
   select, and this form is a checkbox with its "?" beside it. Without the
   direction the icon dropped onto its own line under the label it explains.
   nowrap keeps them together; the label wraps its own text instead. */
.col-vault {
  display: flex; flex-direction: row; align-items: center;
  gap: .4rem; flex-wrap: nowrap;
}
.col-vault .tag-choice { min-width: 0; font-size: 1rem; color: var(--fg); }

/* The nightly-import switch, under the button that does it now. Set apart by a
   rule and its own space: it is a standing decision sitting beneath a one-off
   action, and without the separation the Save button below reads as belonging
   to Sync now. */
.sync-schedule {
  margin: 1.4rem 0 0; padding-top: 1rem; border-top: 1px solid var(--line);
}
.sync-schedule .tag-choice { font-size: 1rem; }
.sync-schedule button { margin-top: .6rem; }

/* Categories: name on the left, its Edit on the right. Same shape as the user
   queue above, which is the other list-of-records-with-one-action in the app. */
.categories li {
  display: flex; gap: .5rem .8rem; align-items: center;
  justify-content: space-between; flex-wrap: wrap;
}
.categories li > span { padding: .7rem .2rem; }
.categories .button { padding: .35rem .8rem; font-size: .9rem; }
/* The heading a group of categories sits under, on the management list and in
   the label picker. Quiet on purpose: it is the same word the sidebar puts over
   the same categories, so it has to read as a divider and not as a second level
   of navigation competing with the page's own title. */
.category-heading {
  margin: 1.4rem 0 .2rem; font-size: .85rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  border-bottom: 1px solid var(--line); padding-bottom: .25rem;
}
.category-heading:first-child { margin-top: .4rem; }

/* .add-category retired: "Add a category" is now class="panel" (see
   categories.html), and .panel / .panel-head already supply the spacing,
   cursor and weight this used to draw. */

/* Approve, decline, and the line that goes in the email either way. The input
   grows into whatever the row has left and drops onto its own line on a phone,
   where the two buttons alone fill the width. */
.decide { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
.decide input[name="message"] {
  flex: 1 1 12rem; min-width: 0;
  /* 16px: anything smaller makes iOS Safari zoom the page on focus. */
  font-size: 1rem; padding: .3rem .5rem;
}

/* .section-head retired: locations.html's two heading-plus-button rows are now
   real .panel-head rows (see "Page structure" in UI.md), which already
   supply this layout. */

/* nowrap lives with the table rules below, where it can outrank them. */
.row-actions a { margin-right: .7rem; font-size: .9rem; }
/* Label, name, and the button that hands over to the tracker's own app. Wraps
   on a phone rather than squeezing the name to nothing. */
.tracker {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: .3rem .7rem;
  margin: 1rem 0 0;
}
.tracker .meta { flex: 0 0 100%; }
/* The one link on this page that is worth interrupting for. Ruled in brand red
   as an accent, never filled — solid red still has to mean overdue. */
/* The clear link sits with the Filter button rather than under the fields, so
   it appears in the same place at both widths — the fields reflow, this row
   does not. */
.filter-actions { display: flex; align-items: center; gap: .8rem; }

/* A full-width row of its own beneath the fields, in both the grid the index
   uses and the flex panel the browse page uses. Collapses when it holds nothing,
   so an unfiltered page gets no empty gap. */
.filter-links {
  grid-column: 1 / -1; flex: 1 1 100%;
  display: flex; flex-wrap: wrap; gap: .3rem 1.1rem;
  margin: 0 0 .3rem; font-size: .9rem;
}
.filter-links:empty { display: none; }

/* Tags. Neutral like the lab pill — they say what a thing is for, not how it
   is doing, so they must not compete with available/in use/overdue. */
/* The owning-lab chooser under the tags is the same control in a different
   column of the data, so it is the same pill. Not .pill.lab — that one labels an
   item in the listing and has to stay legible as a label, not as a switch. */
.pill.tag, .pill.lab-choice { background: var(--locked-bg); color: var(--muted); text-decoration: none; }
a.pill.tag:hover, a.pill.lab-choice:hover { color: var(--brand-ink); }
.tag-cloud { display: flex; flex-wrap: wrap; gap: .35rem; margin: 1rem 0 0; }

/* The filter panel. One column on a phone, wrapping into several on a desktop —
   a category can put a dozen filters on screen and a single column would push
   the results a screen and a half down. */
.facets { display: flex; flex-wrap: wrap; gap: .8rem 1.2rem; align-items: end; }
.facets .field { flex: 1 1 12rem; margin: 0; }
.facets .tag-field { flex: 1 1 100%; }
.tag-choices { display: flex; flex-wrap: wrap; gap: .3rem .9rem; }
.tag-choice {
  display: inline-flex; align-items: center; gap: .35rem;
  font-weight: 400; padding: .35rem 0; cursor: pointer;
}
.tag-choice input { width: 1.1rem; height: 1.1rem; accent-color: var(--accent); }
/* Ruled off from the tags themselves: it is not one of them, it is the absence
   of all of them. */
.untagged-choice {
  border-left: 1px solid var(--line); padding-left: .9rem; margin-left: .2rem;
  color: var(--muted);
}

.bulk-tag {
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: end;
  margin: 1rem 0; padding: .8rem;
  border: 1px solid var(--line); border-radius: var(--radius);
}
.bulk-tag label { flex: 0 0 100%; font-size: .9rem; color: var(--muted); }
.bulk-tag input { flex: 1 1 12rem; }

.doc-link {
  border-left: 3px solid var(--brand);
  padding: .1rem 0 .1rem .7rem;
}
.tracker .button { padding: .35rem .8rem; font-size: .9rem; }

.status-card dl { margin: .6rem 0 0; display: grid; grid-template-columns: auto 1fr; gap: .3rem .8rem; }
.status-card dt { color: var(--muted); font-size: .9rem; }
.status-card dd { margin: 0; }

table.specs { width: 100%; border-collapse: collapse; }
table.specs th, table.specs td {
  text-align: left;
  padding: .45rem .5rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
  font-size: .95rem;
}
table.specs th { width: 40%; color: var(--muted); font-weight: 600; }
/* pre-line keeps the line breaks a spreadsheet cell wrote. It also turns the
   newline between two action links into a visible one, and `table.specs td`
   outranks `.row-actions`, so the exception has to be spelled out here. */
table.specs td { white-space: pre-line; }
table.specs td.row-actions { white-space: nowrap; }

/* --- allauth pages --------------------------------------------------- */
main form p { margin: 0 0 .9rem; }
main form p label { display: block; }
.socialaccount_providers { list-style: none; padding: 0; margin: 1rem 0; }
.socialaccount_providers a { font-weight: 600; }

/* --- index: sidebar + results ---------------------------------------- */
main.wide { max-width: 76rem; }

/* Phone order: search first, then a compact category list, then the results.
   The sidebar used to sit above everything and eat 45vh before you could reach
   the search box — on the page whose whole purpose is finding something. */
.layout { display: flex; flex-direction: column; gap: 1rem; }
/* Search first — most arrivals are looking for something, not adding it — then
   the add button, which is why it comes before the category list rather than
   after it, where a phone would put it below the fold. */
.filters { order: 1; }
.add-equipment { order: 2; }
.sidebar { order: 3; }
.results { order: 4; }
@media (min-width: 52rem) {
  .layout {
    display: grid;
    grid-template-columns: 17rem 1fr;
    grid-template-areas: "side add" "side filters" "side results";
    /* The 1fr is load-bearing. The sidebar spans all three rows, and grid hands
       a spanning item's surplus height to every auto row it crosses, in equal
       shares — so a search matching one item used to push the Add button, the
       filter bar and the result apart down the page, each stranded in the
       middle of an over-tall row. Naming the last row 1fr sends the whole
       surplus there instead, and align-items:start keeps the results at its
       top. Only the results row may grow; the two above it are content-sized. */
    grid-template-rows: auto auto 1fr;
    gap: 0 2rem;
    align-items: start;
  }
  .sidebar { grid-area: side; }
  .add-equipment { grid-area: add; justify-self: start; }
  .filters { grid-area: filters; }
  .results { grid-area: results; }
}

/* The category list is always expanded. On a phone it is capped and scrolls in
   place rather than pushing the results off the bottom of the screen. */
.sidebar {
  margin-bottom: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .4rem;
}
/* The cap is on the category list, not the panel. It exists to stop 48
   categories pushing the results off a phone screen — but with the tag cloud
   underneath, capping the whole panel buried the tags below twenty rows of
   scrolling, which is the opposite of a shortcut. */
.cat-list {
  max-height: 14rem;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
@media (min-width: 52rem) {
  .sidebar {
    position: sticky; top: 1rem;
    border: 0; padding: 0;
  }
  .cat-list { max-height: calc(100vh - 10rem); }
}

.sidebar summary {
  cursor: pointer; padding: .45rem .5rem .45rem 1.2rem; border-radius: var(--radius);
  font-weight: 600; font-size: .9rem; position: relative;
  display: flex; justify-content: space-between; gap: .5rem; align-items: baseline;
}
.sidebar summary::-webkit-details-marker { display: none; }
.sidebar summary::marker { content: ""; }
/* Own triangle, so it points the same way in every browser. */
.sidebar summary::before {
  content: "▾"; position: absolute; left: .35rem;
  font-size: .7rem; color: var(--muted);
}
.sidebar details:not([open]) > summary::before { content: "▸"; }
.sidebar summary:hover { background: var(--band); color: var(--brand-ink); }

.sidebar ul { list-style: none; margin: 0; padding: 0; }
.sidebar details ul { margin-left: .6rem; border-left: 1px solid var(--line); padding-left: .4rem; }

/* One student's name above their own rows. Quieter than an <h2> because the
   section already has one — this divides it, it does not open a new topic. */
/* An unread notice keeps a brand-red rule until it has been opened. A rule and
   not a fill: a page of red blocks would read as a page of problems. */
.bookings { margin-top: 1.5rem; }
/* .book-ahead and .cal-wrap are both now class="panel" details (see
   detail.html) — cursor, weight, padding and the chevron come from .panel /
   .panel-head in the block above, so the per-element rules that used to draw
   them here are gone. */
.book-ahead form { margin-top: .5rem; }

/* The booking calendar. Seven equal columns whatever the width, because a month
   that reflows into ragged columns stops being readable as a month. --accent
   for your own days and a neutral fill for everybody else's: red is spoken for
   here, and a booking is not overdue, it is just spoken for. */
.cal { margin: .5rem 0 1rem; }
.cal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem; margin-bottom: .4rem;
}
.cal-head .button { padding: .15rem .6rem; line-height: 1.4; }
.cal-grid { width: 100%; table-layout: fixed; border-collapse: collapse; }
.cal-grid th {
  padding: .2rem 0; font-size: .75rem; font-weight: 600;
  color: var(--muted); text-align: center;
}
.cal-grid td { padding: 1px; text-align: center; }
/* The day itself is the target, so a fat finger lands on a day and not between
   two. 2rem square is above the 44px-ish tap target once padding is counted. */
.cal-day {
  display: block; padding: .45rem 0; border-radius: var(--radius);
  font-size: .9rem; font-variant-numeric: tabular-nums;
  border: 1px solid transparent; text-decoration: none; color: inherit;
}
.cal-grid td.out .cal-day { color: var(--muted); opacity: .55; }
.cal-grid td.past .cal-day { opacity: .7; }
.cal-grid td.today .cal-day { border-color: var(--brand); font-weight: 700; }
.cal-day.taken { background: var(--locked-bg); color: var(--fg); }
.cal-day.mine { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
/* A day somebody actually has the equipment, rather than has claimed. Hatched
   rather than a third colour: it is a second fact about a day that is already
   either yours or somebody else's, and the palette has no fourth meaning to
   spare — red is overdue and blue is yours. */
.cal-day.in-use {
  background-image: repeating-linear-gradient(
    -45deg, rgba(127, 127, 127, .45) 0 2px, transparent 2px 6px
  );
}
.cal-day.mine:hover { text-decoration: underline; }
.cal-key { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.cal-key .cal-day { display: inline-block; width: 1.6rem; padding: .25rem 0; }

.danger-form { margin-top: 1.5rem; display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.help.error { color: var(--bad); }

.mail-prefs { margin: 0 0 1.2rem; }
.mail-prefs .help { margin: .1rem 0 .5rem; }

/* --accent, not --brand. Saturated red has one job in this palette and it is
   *overdue* — spending it on "you have not read this yet" costs the meaning, and
   these two now sit on the same page. Blue is what every other chosen or new
   thing here uses. */
.feed li.fresh { border-left: 3px solid var(--accent); padding-left: .6rem; }
.feed li > span:first-child { display: block; padding: .6rem .2rem; }

.student-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .6rem;
  font-size: 1rem; margin: 1.2rem 0 .2rem;
}
.student-head .meta { font-weight: 400; }
.student-head .overdue-note { color: var(--bad); }

.sidebar-heading {
  font-size: .78rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); margin: .8rem .5rem .3rem; padding-top: .6rem;
  border-top: 1px solid var(--line);
}
.sidebar .tag-cloud { margin: 0 .3rem .3rem; gap: .3rem; }
/* A selected tag reads as chosen, not as a status: --accent, the colour every
   other chosen thing here uses, never the brand red that means overdue. */
.pill.tag.on, .pill.lab-choice.on { background: var(--accent); color: var(--accent-ink); }

a.cat {
  display: flex; justify-content: space-between; gap: .5rem; align-items: baseline;
  padding: .4rem .5rem; border-radius: var(--radius);
  text-decoration: none; color: var(--fg); font-size: .9rem;
}
a.cat:hover { background: var(--band); color: var(--brand-ink); }
a.cat.active { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
a.cat.all { font-weight: 600; }
a.cat.sub { font-size: .87rem; }
.count { color: var(--muted); font-size: .8rem; font-variant-numeric: tabular-nums; }
a.cat.active .count { color: inherit; opacity: .8; }

.filters { display: grid; gap: .6rem; margin-bottom: 1rem; }
@media (min-width: 40rem) { .filters { grid-template-columns: 2fr 1fr 1fr 1fr auto; align-items: end; } }
@media (min-width: 52rem) and (max-width: 68rem) {
  .filters { grid-template-columns: 1fr 1fr; }
}

.item-list { list-style: none; margin: 0; padding: 0; }
.item-list li { border-bottom: 1px solid var(--line); }
.item-list a {
  display: flex; gap: .6rem; justify-content: space-between; align-items: baseline;
  padding: .7rem .2rem; text-decoration: none; color: inherit;
}
.item-list a:hover { background: var(--band); }
/* The text takes the leftover width, so the pills sit in columns instead of
   trailing whichever of the two text lines happens to be longest on that row —
   which is what made a column of lab pills read as a ragged edge once several
   items had an owner. min-width:0 because a flex item will not shrink below its
   content otherwise, and these lines are long. */
.item-list a > span:first-child { flex: 1 1 auto; min-width: 0; }
.item-list a > .pill { flex: 0 0 auto; text-align: center; }
/* Floors, not fixed widths: "available" and "in use" then line up with each
   other, and the two lab names with each other, while a status pill naming a
   long room is still allowed to be as wide as the room is called. */
.item-list a > .pill.lab { min-width: 4rem; }
.item-list a > .pill:last-child { min-width: 5.5rem; }
/* The same floor for a pill that *is* the link rather than sitting inside one —
   the due pill on Mine. Without it that one alone shrank to its text, so "due
   1 Sep" read as though somebody had squeezed it, beside a column of pills that
   all had room. Centred, because a floor plus left alignment leaves a short
   label adrift at one end of its own oval. */
.item-list li > a.pill { min-width: 5.5rem; text-align: center; }

/* "Mine" rows carry an action, so the link cannot wrap the whole row. On a
   phone the three parts wrap onto two lines instead of being squeezed flat. */
.mine-list li { display: flex; gap: .5rem .7rem; align-items: center; flex-wrap: wrap; }
.mine-list li a { flex: 1 1 100%; display: block; }
/* The same trap the plan list hit above: this rule styles *every* anchor in a
   row as the row link, and the due pill is an anchor because it opens the date
   it shows. Without this it stretched the full width of the row and pushed the
   button onto a line of its own. A pill is a readout that happens to lead
   somewhere, never the row link. */
.mine-list li a.pill { flex: 0 0 auto; display: inline-block; width: auto; }
.mine-list form { margin-left: auto; }
.mine-list button { padding: .45rem .9rem; white-space: nowrap; }
@media (min-width: 40rem) {
  .mine-list li a { flex: 1 1 auto; }
}
.item-list .name { font-weight: 600; }
.item-list .meta { display: block; color: var(--muted); font-size: .85rem; font-weight: normal; }
/* The category leads the meta line: with 404 items in one list, "which kind of
   thing is this" is the first question, and colour separates it from the grey
   identifiers that follow without looking like a link.

   Its own colour, set as an inline custom property from the category record —
   the same shape as the lab tint on the top bar, and for the same reason: one
   stored value, mixed by the stylesheet into whichever theme is in force. A
   colour picked to read on white is invisible on the dark background, so dark
   lifts it toward white rather than storing a second value nobody would keep
   in step. Browsers without color-mix fall through to the stored colour. */
.cat-tag {
  color: var(--cat-colour);
  color: color-mix(in srgb, #fff var(--cat-lift), var(--cat-colour));
  font-weight: 600;
}

.empty { color: var(--muted); padding: 2rem 0; text-align: center; }


/* --- phone sizing ------------------------------------------------------ */
/* The primary client is a phone that has just scanned a QR code, so these are
   corrections measured against a 390 px viewport rather than guesses. */
@media (max-width: 40rem) {
  main { padding: .75rem .9rem 3rem; }
  h1 { font-size: 1.45rem; margin: .3rem 0 .6rem; }
  h2 { font-size: 1.15rem; }

  /* Comfortable thumb targets. 44 px is the figure both platforms publish. */
  button, .button { min-height: 2.75rem; }
  .item-list a { padding: .85rem .2rem; }
  .menu > ul a { padding: .7rem .6rem; }

  /* A spec table with a long value must scroll inside itself rather than
     making the whole page scroll sideways. */
  table.specs { display: block; overflow-x: auto; white-space: normal; }
  table.specs th { width: 40%; }

  /* Nothing may push the document wider than the screen. */
  body { overflow-x: hidden; }
}

/* Never let a control fall below 16px on a touch screen: iOS zooms the page in
   on focus and does not zoom back out. */
@media (max-width: 40rem) and (pointer: coarse) {
  input:not([type=checkbox]):not([type=radio]):not([type=submit]),
  select, textarea { font-size: 16px; }
}

/* --- API tokens ------------------------------------------------------------
   The secret block exists to be selected, on a phone, once, by somebody who
   will not get a second chance at it. So it wraps rather than scrolls: a
   horizontal scroller hides half a credential behind a gesture, and a
   half-copied token fails in a way that looks like a server problem. */
.token-secret {
  border: 1px solid var(--brand);
  border-radius: var(--radius);
  padding: .9rem 1rem;
  margin: 1rem 0 1.5rem;
  background: var(--band);
}
.token-secret h2 { margin-top: 0; }
.token-secret .secret {
  display: block;
  margin: .6rem 0;
  padding: .6rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field);
  font-size: .95rem;
  /* anywhere, not break-word: the string has no spaces to break at */
  overflow-wrap: anywhere;
  user-select: all;
}
.item-list.tokens li {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  padding: .6rem 0;
}
.item-list.tokens li > span:first-child { flex: 1 1 12rem; min-width: 0; }
.item-list.tokens form { margin: 0; }
.token-state { font-size: .8rem; color: var(--muted); white-space: nowrap; }
.token-state.state-active { color: var(--ok); }
.token-state.state-rotated { color: var(--warn); }
.token-state.state-revoked, .token-state.state-expired { color: var(--bad); }

/* Three documents, three audiences — laid out so the difference between them is
   readable at a glance rather than after opening all three. */
.doc-links { list-style: none; margin: 0 0 1.5rem; padding: 0; }
.doc-links li { padding: .35rem 0; border-bottom: 1px solid var(--line); }
.doc-links li:last-child { border-bottom: 0; }
.doc-links .meta { display: block; color: var(--muted); font-size: .85rem; }

/* Custom label geometry. Five boxes that wrap on a phone rather than scroll —
   somebody typing these is holding a packet of labels in the other hand. */
.custom-size { margin: .6rem 0 .2rem; padding-left: 1.6rem; }
.measurements { display: flex; flex-wrap: wrap; gap: .6rem; margin: .5rem 0; }
.measurements label { display: flex; flex-direction: column; font-size: .85rem; gap: .2rem; }
.measurements input { width: 6.5rem; }

/* The audit log. One row per entry, and a table because the questions asked of
   it are comparisons down a column — who, when, what — which is the one thing a
   list of stacked cards cannot answer. Seven columns do not fit a phone, so the
   table scrolls inside its own box; the page itself never moves sideways. */
.log-retention {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem;
  margin: 1rem 0;
  padding: .7rem .9rem;
  background: var(--band);
  border-radius: 6px;
}
/* The base form rule carries three :not() clauses, so a plain `.log-retention
   input` loses to it on specificity and the box stretches the whole row. Same
   shape, one class deeper. A number of days is four digits. */
.log-retention input:not([type=checkbox]):not([type=radio]):not([type=submit]) {
  width: 6rem;
}
.log-retention .unit { color: var(--muted); font-size: .85rem; }
.log-retention .meta { color: var(--muted); font-size: .85rem; margin-left: auto; }
.log-filters { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; margin: 1rem 0; }
.log-filters label { display: flex; flex-direction: column; font-size: .85rem; gap: .2rem; }
.log-filters select, .log-filters input { max-width: 14rem; }

.table-scroll { overflow-x: auto; }
.log-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.log-table th, .log-table td {
  text-align: left;
  padding: .35rem .6rem .35rem 0;
  border-bottom: 1px solid var(--line);
  /* One line each, which is the whole point. Anything longer than its column
     is cut rather than wrapped, and the full text is on the row's title. */
  white-space: nowrap;
}
.log-table thead th { border-bottom: 2px solid var(--line); font-weight: 600; }
.log-table thead a { text-decoration: none; color: inherit; }
.log-table thead a:hover { text-decoration: underline; }
/* The arrow marks the column in force and which way. Only one ever shows. */
.log-table th.sorted-asc .arrow::after { content: " \2191"; }
.log-table th.sorted-desc .arrow::after { content: " \2193"; }
.log-table tbody tr:hover { background: var(--band); }
.log-table .col-details {
  max-width: 24rem;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--muted);
}
.log-table .col-action code { font-size: .85rem; }
/* The outcome left the table, so a refusal has to be visible without one. A
   rule down the edge rather than a fill: solid red still means overdue here. */
.log-table tbody tr.log-refused td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
.log-table tbody tr.log-failed td:first-child { box-shadow: inset 3px 0 0 var(--bad); }
.log-table tbody tr.log-refused td:first-child,
.log-table tbody tr.log-failed td:first-child { padding-left: .6rem; }

/* One entry in full. */
/* A monospace action name is wider than a phone; break it rather than let it
   push the page sideways. */
.log-title code { overflow-wrap: anywhere; }
.log-headline { font-weight: 600; }
.log-headline.state-done { color: var(--ok); }
.log-headline.state-refused { color: var(--warn); }
.log-headline.state-failed { color: var(--bad); }
.log-detail { display: grid; grid-template-columns: minmax(6rem, 10rem) 1fr; gap: .4rem 1rem; }
.log-detail dt { font-weight: 600; color: var(--muted); }
.log-detail dd { margin: 0; overflow-wrap: anywhere; }
.log-detail ul.plain { list-style: none; margin: 0; padding: 0; }
.log-detail ul.plain code { margin-right: .4rem; }
@media (max-width: 520px) {
  /* Two columns of six characters each is not a layout. */
  .log-detail { grid-template-columns: 1fr; gap: .1rem; }
  .log-detail dd { margin-bottom: .6rem; }
}
.log-table .why { color: var(--muted); margin-left: .4rem; }
.log-table .state-done { color: var(--ok); }
.log-table .state-refused { color: var(--warn); }
.log-table .state-failed { color: var(--bad); }
.pager { display: flex; gap: 1rem; align-items: baseline; margin: 1rem 0; }

/* A card was tried, then a table, then the card again — the full story is in
   the template. What survives of the table experiment is only this file being
   shorter than it was. */
