/* Field Day planner — one stylesheet, mobile first. */

:root {
  /* The greys carry a green cast rather than a blue one. The mark this is all
     drawn around is moss, and a cold slate under a warm green reads as two
     palettes that met on the page rather than one that was chosen. */
  /* What the browser draws for us: the calendar button inside a date field,
     the clock inside a time field, a scrollbar, the panel a picker opens. Those
     follow this, and this only — not the ground we paint. Without it, a planner
     asked for night on a machine set to day keeps its near-black calendar icon
     on our near-black field, and a desktop, where that icon is the only way in,
     has no date picker at all. A phone opens the picker from anywhere in the
     field, which is why it looked fine there and only there. */
  color-scheme: light;
  --bg: #f4f5f0;
  --surface: #ffffff;
  --surface-2: #eaece4;
  --text: #151a12;
  --muted: #626b58;
  --line: #e0e3d8;
  --line-strong: #9aa38c;
  --accent: #4f6c3d;
  --accent-text: #ffffff;
  --tint: #eef1e7;
  --todo: #97a08b;
  --progress: #4f46e5;
  --done: #1a7f4b;
  --warn: #a8501c;
  --danger: #b3261e;
  /* Depth is a lift off the page, not a line drawn round the edge. A hairline
     is what a document does; a raised surface is what a phone does, and in the
     dark a border reads as noise while a lighter ground reads as "on top". */
  --raise: 0 1px 2px rgb(20 26 15 / .06), 0 6px 18px rgb(20 26 15 / .07);
  --radius: 14px;
  --radius-field: 12px;
  --radius-pill: 999px;
  --tap: 46px;
  /* Four pixels, seven steps. Before this there were twenty spacings in the
     stylesheet and no rhythm anybody could see. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;
  --gap: .75rem;
  --topbar-h: 3.3rem;
  --quick-h: 2.9rem;
  --bottomnav-h: 60px;
}

/* Night, two ways in. The phone decides unless somebody has said otherwise, and
   what they said wins in both directions — a phone that has gone dark at sunset
   must still show day to somebody who asked for day.

   The two lists below are the same palette written twice, because a declaration
   block cannot be shared across a media query. planner/tests/test_theme.py holds
   them to each other, so changing one and forgetting the other is a failing test
   rather than a page that is half dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #0f1310;
    --surface: #1a201a;
    --surface-2: #232a22;
    --text: #e9ede4;
    --muted: #98a28d;
    --line: #2a3228;
    --line-strong: #5c6656;
    --accent: #8fb073;
    --accent-text: #0c1016;
    --tint: #222a20;
    --todo: #79836f;
    --progress: #8b83ff;
    --done: #33b06a;
    --warn: #d99154;
    --danger: #e5715f;
    /* Almost nothing: a shadow on a near-black ground is a smudge, and the
       lift is done by the surface being lighter than what it sits on. */
    --raise: 0 1px 2px rgb(0 0 0 / .5), 0 8px 24px rgb(0 0 0 / .34);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0f1310;
  --surface: #1a201a;
  --surface-2: #232a22;
  --text: #e9ede4;
  --muted: #98a28d;
  --line: #2a3228;
  --line-strong: #5c6656;
  --accent: #8fb073;
  --accent-text: #0c1016;
  --tint: #222a20;
  --todo: #79836f;
  --progress: #8b83ff;
  --done: #33b06a;
  --warn: #d99154;
  --danger: #e5715f;
  /* Almost nothing: a shadow on a near-black ground is a smudge, and the
     lift is done by the surface being lighter than what it sits on. */
  --raise: 0 1px 2px rgb(0 0 0 / .5), 0 8px 24px rgb(0 0 0 / .34);
}

/* The three-way choice in the More menu. The one in force is pressed. */
.theme-choice { display: flex; gap: .3rem; padding: .2rem 0; }
.theme-choice button {
  flex: 1; min-height: var(--tap); padding: .3rem .5rem;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius);
  font: inherit; font-size: .82rem; cursor: pointer;
}
.theme-choice button[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-text); border-color: var(--accent);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding-bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom));
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
  overflow-x: hidden;
}

a { color: var(--accent); }
h1 { font-size: 1.35rem; margin: 0; letter-spacing: -.01em; }
/* Big, because on a phone the only thing above it is a brand bar, and what
   somebody needs to read first is which page they are on — they already know
   which app they opened. */
.page-head h1 {
  font-size: clamp(1.7rem, 6.5vw, 1.9rem);
  font-weight: 700; letter-spacing: -.028em; line-height: 1.08;
  text-wrap: balance;
}
h2 { font-size: 1rem; margin: 1.5rem 0 .5rem; letter-spacing: .01em; }

.wrap { max-width: 62rem; margin: 0 auto; padding: 1rem .75rem 2.5rem; }
/* For the timeline, which is the one page whose job is width. */
.wrap.wide { max-width: 108rem; }
.muted { color: var(--muted); }
.small { font-size: .82rem; }
.nowrap { white-space: nowrap; }
/* Nothing here yet, said as a place rather than as a missing paragraph. A
   single grey line at the top left of a blank page reads as a page that failed
   to load; a bordered field of nothing reads as a list with nothing in it. */
.empty {
  color: var(--muted); list-style: none; text-align: center;
  padding: var(--s7) var(--s4); font-size: .92rem;
  border: 1px dashed var(--line); border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 55%, transparent);
}
.page-head {
  display: flex; flex-wrap: wrap; gap: var(--s1) var(--gap);
  align-items: baseline; justify-content: space-between; margin-bottom: var(--s5);
}
.page-head p { margin: 0; }
/* A heading with a line about the page under it, rather than a heading with a
   button beside it: the sentence belongs below the title, not out at the right
   margin where it reads as a caption. */
.page-head.stacked { display: block; }
.page-head.stacked p { margin-top: var(--s1); max-width: 46ch; }

/* --- Navigation --- */
.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--gap);
  padding: .55rem .75rem; min-height: var(--topbar-h);
  background: var(--surface); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 30;
}
.brand { font-weight: 600; text-decoration: none; color: var(--text); }
.topnav { display: none; align-items: center; gap: .9rem; }
.topnav > a:not(.button) { text-decoration: none; color: var(--muted); }
.topnav > a:not(.button):hover { color: var(--text); }
.topnav .current { color: var(--text); font-weight: 600; }

.menu { position: relative; }
.menu > summary {
  list-style: none; cursor: pointer; color: var(--muted);
  padding: .4rem 0; min-height: var(--tap);
  display: flex; align-items: center; justify-content: center; gap: .15rem;
  white-space: nowrap;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::after { content: "▾"; font-size: .85em; }
.menu-panel {
  position: absolute; right: 0; top: 100%; z-index: 40;
  display: grid; min-width: 12rem; padding: var(--s1);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 12px 32px rgb(0 0 0 / .18);
}
.menu-panel a {
  padding: .6rem .7rem; text-decoration: none; color: var(--text);
  border-radius: 6px; min-height: var(--tap); display: flex; align-items: center;
}
.menu-panel a:hover { background: var(--surface-2); }
/* A count of somebody waiting: a coloured badge, so it reads as news rather
   than as a digit stuck to the end of a word. The margin does the spacing,
   because a flex container drops the whitespace between text and span. */
.tally {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.6em; height: 1.6em; padding: 0 .45em; margin-left: .45em;
  border-radius: 999px; font-size: .78em; font-weight: 700; line-height: 1;
  background: var(--accent); color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}
.menu > summary .tally { margin-left: .3em; }
.menu.up .menu-panel { top: auto; bottom: 100%; margin-bottom: .3rem; }
.menu-title { display: none; }

/* Tapping anywhere else closes the menu. Without a backdrop the only way out
   is to hit the same small target again. */
.menu[open]::before {
  content: ""; position: fixed; inset: 0; z-index: 1;
  background: rgb(0 0 0 / .45);
}

/* On a phone it is a sheet across the foot of the screen, not a small tray
   hanging off one nav item. */
@media (max-width: 759px) {
  .bottomnav .menu > summary { min-width: 0; padding: 0 .2rem; }
  .bottomnav .menu[open] > summary { color: var(--accent); font-weight: 600; }
  /* Outranks the desktop .menu.up rule, which pins it to the summary. */
  .bottomnav .menu.up .menu-panel {
    position: fixed; left: var(--s2); right: var(--s2); top: auto;
    bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom) + var(--s2)); z-index: 2;
    min-width: 0; margin: 0; padding: var(--s2);
    border: 0;
    border-radius: 22px;
    box-shadow: 0 -8px 40px rgb(0 0 0 / .4);
  }
  /* The grab handle every sheet on a phone has. It is not draggable — this one
     is a details element — but it is what says "this came up from the bottom
     and goes back down", which is the whole reason it reads as a sheet. */
  .bottomnav .menu.up .menu-panel::before {
    content: ""; display: block; justify-self: center;
    width: 2.25rem; height: .25rem; border-radius: 999px;
    background: var(--line-strong); opacity: .55;
    margin: var(--s1) 0 var(--s2);
  }
  /* The sheet's own header: what this is on the left, the way out of the
     planner on the right, and a line under both. The button keeps its whole
     tap height rather than being shrunk to match the label beside it — a
     target you have to aim at is how a thumb misses it. */
  .menu-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s3); margin: .1rem .7rem .35rem;
    border-bottom: 1px solid var(--line);
  }
  .menu-title {
    display: block; margin: 0;
    font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
    color: var(--muted); font-weight: 600;
  }
  .menu-out button { padding: .4rem 0; font-size: .85rem; }
  .bottomnav .menu.up .menu-panel a { min-height: var(--bottomnav-h); font-size: 1rem; }
}

/* Set apart from the page rather than blended into it: on a phone this is the
   only way around the planner, and drawn in --surface it read as more of the
   page. A tone up from the background, and a line across the top strong enough
   to see in sunlight, so it is findable without being looked for. */
.bottomnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: flex; background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: 0 -2px 14px rgb(0 0 0 / .10);
  padding: var(--s1) var(--s1) 0;
  padding-bottom: calc(var(--s1) + env(safe-area-inset-bottom));
}
.bottomnav > a, .bottomnav > .menu {
  flex: 1; text-align: center; font-size: .66rem;
}
/* Icon over word. Five words in a row is a menu on a web page; a drawing with
   its name under it is what every phone in the tent already has at the foot of
   every other app on it. */
.bottomnav > a, .bottomnav .menu > summary {
  padding: var(--s1) 2px; text-decoration: none; color: var(--muted);
  min-height: var(--bottomnav-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; font-size: .66rem; font-weight: 500; letter-spacing: .01em;
  -webkit-tap-highlight-color: transparent;
}
.bottomnav .icon {
  width: 46px; height: 28px; border-radius: var(--radius-pill);
  display: flex; align-items: center; justify-content: center;
  transition: background-color .15s ease;
}
.bottomnav .icon svg { width: 21px; height: 21px; display: block; }
/* Where you are, said with a shape as well as a colour: a tent in daylight is
   not where a hue on its own carries. */
.bottomnav .current { color: var(--accent); font-weight: 600; box-shadow: none; }
.bottomnav .current .icon { background: var(--tint); }
.bottomnav .menu > summary::after { content: none; }
.bottomnav .menu[open] > summary .icon { background: var(--tint); }
@media (prefers-reduced-motion: reduce) {
  .bottomnav .icon { transition: none; }
}

@media (min-width: 760px) {
  .topnav { display: flex; }
  .bottomnav { display: none; }
  body { padding-bottom: 0; }
}

/* --- Controls --- */
button, input, select, textarea { font: inherit; color: inherit; }

/* :not(.linkish) because a linkish button is a link that happens to submit, and
   the fill rules must not reach it. Without it the delete button ended up with
   background and colour both var(--danger) — red on red, and the word "Slett"
   was invisible on every page that offered it. Wrapped in :where() so the
   exclusion adds no specificity: a bare :not() outranked .button.ok,
   .button.ghost and .task-check .check, which painted the row tick
   accent-on-accent — the same invisible-glyph failure in a new place. */
.button, button[type="submit"]:where(:not(.linkish)), .statusbar button {
  display: inline-flex; align-items: center; justify-content: center; gap: .35rem;
  min-height: var(--tap); padding: .4rem .85rem;
  border: 1px solid transparent; border-radius: var(--radius-pill);
  background: var(--accent); color: var(--accent-text);
  text-decoration: none; cursor: pointer; font-weight: 600;
  box-shadow: var(--raise);
  -webkit-tap-highlight-color: transparent;
  transition: transform .12s ease;
}
/* The button gives under the thumb. Nothing else in the planner moves on
   press, and the one control that commits something should. */
.button:active, button[type="submit"]:where(:not(.linkish)):active,
.statusbar button:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
  .button, button[type="submit"]:where(:not(.linkish)), .statusbar button {
    transition: none;
  }
  .button:active, button[type="submit"]:where(:not(.linkish)):active,
  .statusbar button:active { transform: none; }
}
/* A button that cannot be pressed has to look like it. There was no style for
   this at all, so "disabled" was a thing only the mouse found out — and the one
   place it matters most is a letter that cannot be sent, where the grey is the
   whole explanation. Also catches the submit buttons app.js switches off after
   a tap, which is the feedback that one press was one request. */
.button:disabled, button:disabled {
  background: var(--surface-2); color: var(--muted);
  border-color: var(--line); cursor: not-allowed; font-weight: 400;
  box-shadow: none;
}
.button.small, button.small { min-height: 36px; padding: .25rem .6rem; font-size: .85rem; }
.button.ghost, button.ghost {
  background: transparent; color: var(--text); border-color: var(--line-strong);
  box-shadow: none;
}
.button.ok { background: var(--done); color: var(--accent-text); }
.button.danger, button.danger[type="submit"]:where(:not(.linkish)) {
  background: var(--danger); color: var(--accent-text);
}
/* Element-qualified so the shared button fill does not outrank it. */
button.linkish {
  background: none; border: none; color: var(--muted); cursor: pointer;
  min-height: var(--tap); padding: .4rem .2rem;
  text-decoration: underline; font-size: .85rem; font-weight: 400;
}
button.linkish:hover { color: var(--text); }
button.linkish.danger { color: var(--danger); }
.inline { display: inline; }
html.js .js-hide { display: none; }
/* Read by a screen reader, seen by nobody. */
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* One ring for everything, thick enough to find against a filled button. */
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.button:not(.ghost):focus-visible, button[type="submit"]:where(:not(.linkish)):focus-visible {
  outline-color: var(--text);
}

input[type="text"], input[type="search"], input[type="email"], input[type="password"],
input[type="datetime-local"], input[type="date"], input[type="time"],
input[type="number"], input[type="file"], select, textarea {
  width: 100%; padding: var(--s3); min-height: var(--tap); font-size: 16px;
  border: 1px solid var(--line); border-radius: var(--radius-field);
  background: var(--surface); color: var(--text);
  box-shadow: var(--raise);
}
input:focus, select:focus, textarea:focus { border-color: var(--line-strong); }
textarea { min-height: 5.5rem; }
/* The platform's own chevron is grey-on-grey at the far edge of a field that
   is otherwise ours. Drawn here instead, in the field's own muted colour, so a
   select reads as one of our controls and not as a hole in the page. */
select {
  appearance: none;
  padding-right: 2.4rem;
  background-image: linear-gradient(45deg, transparent 49%, currentColor 50%),
                    linear-gradient(-45deg, transparent 49%, currentColor 50%);
  background-size: 7px 7px, 7px 7px;
  background-position: right 1.15rem center, right .75rem center;
  background-repeat: no-repeat;
}
select::-ms-expand { display: none; }

.messages { list-style: none; margin: 0; padding: .5rem .75rem 0; max-width: 62rem; margin-inline: auto; }
.messages li {
  background: var(--surface); border: 0; border-left: 3px solid var(--done);
  box-shadow: var(--raise);
  padding: var(--s3) var(--s4); border-radius: var(--radius); margin-bottom: var(--s2);
}
.messages li.error { border-left-color: var(--danger); }
.messages li.warning { border-left-color: var(--warn); }
/* Read, and then out of the way, the same departure a ticked-off task makes.
   The height goes with it so the page closes over the gap instead of jumping
   under a thumb. Form errors are a div with this class, not this list, so they
   stay until somebody fixes them. */
.messages li.is-leaving {
  transition: opacity .3s ease, transform .3s ease, height .3s ease,
              margin .3s ease, padding .3s ease, border-width .3s ease;
  opacity: 0; transform: translateX(1.25rem);
  height: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0;
  border-top-width: 0; border-bottom-width: 0; overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .messages li.is-leaving { transition: none; }
}

/* --- Filters --- */
/* Search sits out on the page above the drawer, not in it: it is the thing
   reached for most, and a drawer is a thing nobody opens. One field, the width
   of the list it narrows. */
.find { margin-bottom: var(--s2); }
.find input[type="search"] { box-shadow: var(--raise); }

.filters {
  background: var(--surface); border: 0; box-shadow: var(--raise);
  border-radius: var(--radius); margin-bottom: var(--s4);
}
.filters > summary {
  padding: .6rem .75rem; cursor: pointer; min-height: var(--tap);
  display: flex; align-items: center; gap: .4rem; color: var(--muted);
}
.filters .dot-accent { width: .45rem; height: .45rem; border-radius: 50%; background: var(--accent); }
.filter-form { display: grid; gap: .55rem; padding: 0 .75rem .75rem; }
.filter-form label { display: grid; gap: .15rem; font-size: .8rem; color: var(--muted); }
.filter-form label.check { display: flex; align-items: center; gap: .5rem; min-height: var(--tap); }
.filter-form label.check input { width: 1.05rem; height: 1.05rem; min-height: 0; }
.filter-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }

@media (min-width: 760px) {
  .filter-form { grid-template-columns: repeat(3, 1fr); align-items: end; }
}

/* --- Quick filters --- */
.quick {
  position: sticky; top: var(--topbar-h); z-index: 20;
  display: flex; gap: .35rem; padding: .45rem 0;
  background: var(--bg); margin: -.25rem 0 .6rem;
  overflow-x: auto; scrollbar-width: none;
}
.quick::-webkit-scrollbar { display: none; }
.quick a {
  flex: 1 0 auto; text-align: center; white-space: nowrap;
  padding: .5rem .9rem; min-height: 44px; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-strong); border-radius: 999px;
  background: var(--surface); color: var(--muted); text-decoration: none; font-size: .9rem;
}
.quick a.on {
  background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 600;
}

/* --- Schedule --- */
/* A long schedule is a hundred rows, and on a phone most of them are below the
   fold. This lets the browser skip laying out and painting a day until it comes
   into view. contain-intrinsic-size keeps the scrollbar honest in the meantime,
   so the thumb does not jump as days are reached. Nothing is hidden: find in
   page still works, and a browser without the property just does the work. */
.day, .tl-day {
  content-visibility: auto;
  contain-intrinsic-size: auto 20rem;
}
.day-head {
  position: sticky; top: calc(var(--topbar-h) + var(--quick-h)); z-index: 10;
  margin: 1.5rem 0 .4rem; padding: .35rem 0;
  background: var(--bg);
  border-bottom: 1px solid var(--line); font-size: .9rem; color: var(--muted);
}
.day.is-today .day-head { color: var(--accent); }
/* My page has no quick-filter bar above it, so the day headings stick straight
   under the top bar, and the first one sits close to the block heading it
   belongs under rather than a page-width gap away. */
.block .day-head { top: var(--topbar-h); font-size: .82rem; }
.block .day:first-child .day-head { margin-top: .5rem; }

.tasks { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
/* Searched away. Said here because the row's own display outranks the one the
   browser gives [hidden], and a row that stays put is a search that does not
   work. The day heading above it is a plain section and needs no such help. */
.task[hidden] { display: none; }
/* The whole row opens the task. A title is a small target for a thumb in a
   glove, and everything else on the row — the time, the chips, the empty space
   to the right of a short name — was dead to the touch. The title's own link is
   stretched over the row instead of a click handler, so it is still a link: it
   opens in a new tab, it can be copied, and it works with nothing running.

   Two things stand above it and keep their own jobs: the box that ticks the
   task off, which must never open anything, and the handle that sets who is on
   it. */
.task {
  position: relative;
  scroll-margin-top: calc(var(--topbar-h) + var(--quick-h) + 2.5rem);
  display: grid;
  /* Two columns on a phone, and each one answers one question. Down the left:
     when it runs, and the box that says whether it is done. Down the right:
     what it is, what it is waiting for, and who is on it, one name to a line.

     Before this the check, the clock and the title shared the top line and the
     names hung off a second line on the right, which left every row with a
     ragged edge on both sides and the names floating somewhere in the middle of
     the list. Two columns cost about a sixteenth of an inch of height per row
     and give the eye two straight edges to run down. */
  grid-template-columns: 3.6rem minmax(0, 1fr);
  grid-template-areas:
    "time  body"
    "check assign";
  gap: .3rem .6rem; align-items: start;
  background: var(--surface); border: 0; box-shadow: var(--raise);
  border-left: 3px solid var(--todo);
  border-radius: var(--radius); padding: var(--s2) var(--s3);
}
.task.status-in_progress { border-left-color: var(--progress); }
.task.status-done { border-left-color: var(--done); }
/* Past its deadline, or its time came and went with nobody on it. Different
   questions, same answer for the person reading the row: this one is adrift. */
.task.overdue, .task.late { border-left-color: var(--warn); }
/* Finished is finished, whatever the deadline said. Last word on the border,
   and it takes the lateness chip down with it, so a row that is ticked off in
   place stops claiming to be adrift without waiting for a page load. */
.task.status-done { border-left-color: var(--done); }
.task.status-done .chip.late { display: none; }
/* Ticked off, and on its way out of a list that does not keep finished work. */
.task.is-leaving {
  transition: opacity .3s ease, transform .3s ease;
  opacity: 0; transform: translateX(1.25rem);
}
@media (prefers-reduced-motion: reduce) {
  .task.is-leaving { transition: none; }
}
/* Ticking a task off is switching it off: the whole row steps back, not just
   the title. What is left on the plan should be what the eye lands on. */
.task.status-done { opacity: .62; }
.task.status-done .task-title { text-decoration: line-through; }
.task.status-done .task-time,
.task.status-done .chip,
.task.status-done .task-assign { color: var(--muted); }

/* A square, and square it stays: stretched down a two-line row it stopped
   reading as a box to tick and became an empty panel. It sits directly under
   the clock rather than centred in its row, so that on a task four people are
   on it does not drift half an inch down the side of their names. */
.task-check { grid-area: check; align-self: start; display: flex; }
.task-check .check {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-top: 0; padding: 0;
  border: 1px solid var(--line-strong); border-radius: 6px;
  background: var(--surface); color: var(--done);
  font-size: 1.4rem; font-weight: 700; line-height: 1; cursor: pointer;
}
.task-check button.check { min-height: 0; }
.status-done .task-check .check {
  border-color: var(--done);
  background: color-mix(in srgb, var(--done) 9%, transparent);
}
/* Read access: the same square, but printed on the row rather than offered on
   it. The row partial has said from the start that a tick box which answers
   with a refusal is worse than a printed one, because it invites the tap — and
   the stylesheet had it saying so with a cursor, which is a thing a phone does
   not have. An empty white box the size of a thumb is an invitation whoever is
   holding it will accept. So: the weak line rather than the strong one, and
   filled rather than empty, which is the difference between a control and a
   mark. Last, so it takes on a finished row too. */
.task-check .check.static {
  cursor: default;
  border-color: var(--line);
  background: var(--surface-2);
  color: var(--muted);
}
/* Finished still says finished. It is the one thing this square has left to
   say to somebody who may not change it, so it keeps the colour and gives up
   only the strength of it. */
.status-done .task-check .check.static {
  background: color-mix(in srgb, var(--done) 10%, transparent);
  color: color-mix(in srgb, var(--done) 70%, var(--muted));
}

.task-time { grid-area: time; font-variant-numeric: tabular-nums; line-height: 1.25; font-size: .85rem; }
.task-time .from { display: block; font-weight: 600; }
.task-time .to { display: block; font-size: .75rem; color: var(--muted); }

.task-body { grid-area: body; min-width: 0; }
.task-title { display: block; font-weight: 500; text-decoration: none; color: var(--text); }
.task-title::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
/* Above the sheet the title lays over the row — but only the controls in them
   stand on it. A grid item fills its column whether or not anything is drawn
   there, so left alone these two would put a dead patch the width of the names
   over the link that opens the task, and the row would be tappable everywhere
   except the half of it holding four names. Only what can actually be pressed
   takes the press. */
.task-check, .task-assign { position: relative; z-index: 1; pointer-events: none; }
.task-check button,
.task-assign a, .task-assign button, .task-assign select { pointer-events: auto; }
/* Something has to happen under the finger, or a row that is all one target
   still reads as a row where only the words are one. */
.task:active { background: var(--surface-2); }
@media (hover: hover) {
  .task:hover { background: color-mix(in srgb, var(--surface-2) 55%, var(--surface)); }
}
.chips { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .25rem; }

/* Everybody who is on the task. On a phone they stack under the title, one to
   a line and all starting where the title starts: two of them wrapped across
   that column put half a name at the end of one line and half at the start of
   the next, which is the arrangement that has to be read twice. A taller row is
   cheaper than that, and a list where every name begins at the same place is
   one an eye can run down. */
.task-assign {
  grid-area: assign;
  display: flex; flex-direction: column; gap: .25rem; align-items: flex-start;
}
.assign-many { display: contents; }
.task-assign form, .assign-form { display: flex; align-items: center; gap: .4rem; }
.task-assign select {
  min-height: 44px; width: auto; min-width: 7rem; max-width: 11rem;
  padding: .3rem .4rem;
}

@media (min-width: 760px) {
  .quick { top: var(--topbar-h); }
  /* The names column takes what the names need rather than a flat 12rem: four
     people on one task were stacked one per line and the row came out five
     times the height of its neighbours. Capped, so a shared task cannot push
     the title into a column an inch wide. */
  .task {
    grid-template-columns: auto 3.6rem minmax(0, 1fr) auto;
    grid-template-areas: "check time body assign";
    align-items: center;
  }
  /* One line again, so the box goes back to the middle of it: pinned to the top
     it would sit above a title that has a chip under it. */
  .task-check { align-self: center; }
  /* Room for a row again, and back to its own column at the end of it: the
     stack that made a phone readable would put four names down the side of a
     screen with the width to hold them side by side. Hard right, because a
     column of names that starts at a different place on every line is one
     nobody can run an eye down. */
  .task-assign {
    flex-direction: row; flex-wrap: wrap;
    align-items: center; justify-content: flex-end; max-width: 26rem;
  }
  .task-assign select { max-width: 10rem; }
}

/* --- Chips and dots --- */
.chip {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .78rem; padding: .12rem .5rem; border-radius: 999px;
  background: var(--surface-2); color: var(--text); white-space: nowrap;
}
/* On the screen a name is not a footnote: it is who has the job. A wall is read
   from across a tent, where a faint tint carries less than it does on a phone,
   so the colour is laid on harder here than in the lists. */
.screen-people .chip { font-size: calc(clamp(.85rem, 1.3vw, 1.5rem) * var(--wall, 1)); padding: .18rem .6rem; }
.screen-people .chip.person { background: color-mix(in srgb, var(--chip, var(--muted)) 34%, transparent); }
.screen-people .dot { width: .6em; height: .6em; }
.dot {
  width: .55rem; height: .55rem; border-radius: 50%; flex: none;
  background: var(--chip, var(--muted));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .12);
}
.chip.tag { background: color-mix(in srgb, var(--chip, var(--muted)) 18%, transparent); }
.chip.person { background: color-mix(in srgb, var(--chip, var(--muted)) 20%, transparent); }
.chip.blocked { background: color-mix(in srgb, var(--warn) 20%, transparent); }
.chip.cat { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.chip.cat-next_year { background: color-mix(in srgb, var(--progress) 18%, transparent); }
.chip.cat-remember { background: color-mix(in srgb, var(--warn) 20%, transparent); }
/* Archived work is set aside, not flagged: it should read quieter than the rest. */
.chip.archived { background: color-mix(in srgb, var(--muted) 22%, transparent); color: var(--muted); }
.chip.late { background: color-mix(in srgb, var(--warn) 26%, transparent); font-weight: 600; }
/* In progress was said by a 3px border colour alone, which is nothing to a
   colour-blind eye. The word goes on the row as well. */
.chip.progress { background: color-mix(in srgb, var(--progress) 16%, transparent); color: var(--progress); font-weight: 600; }
.chip.unsaved { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); font-weight: 600; }

/* --- The document archive --- */
.shelf {
  margin: var(--s2) 0; border: 0; box-shadow: var(--raise);
  border-radius: var(--radius); background: var(--surface);
}
.shelf > summary {
  padding: var(--s3) var(--s4); cursor: pointer; font-weight: 600;
  display: flex; align-items: center; gap: .6rem; min-height: var(--tap);
  list-style: none; -webkit-tap-highlight-color: transparent;
}
.shelf > summary::-webkit-details-marker { display: none; }
/* The chevron every row on a phone that opens something has, turned down when
   it is open. Drawn from two borders rather than an image, so it takes the
   colour of the text beside it. */
.shelf > summary::after {
  content: ""; margin-left: auto; flex: none;
  width: .5rem; height: .5rem; margin-right: .2rem;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(-45deg); transition: transform .15s ease;
}
.shelf[open] > summary::after { transform: rotate(45deg) translate(-2px, -2px); }
@media (prefers-reduced-motion: reduce) {
  .shelf > summary::after { transition: none; }
}
/* The two or three characters that name a group, in a box of its own. A column
   of these is read down the left edge at a glance, which is what a list of
   visitors is for. */
.shelf > summary .sign {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 2.3rem; height: 2.3rem; border-radius: var(--radius-field);
  background: var(--tint); color: var(--accent);
  font-size: .78rem; font-weight: 700; letter-spacing: .02em;
}
.shelf > summary .tally {
  font-size: .72rem; font-weight: 700; color: var(--accent);
  background: var(--tint); border-radius: 999px; padding: .1rem .5rem;
  font-variant-numeric: tabular-nums;
}
.shelf > summary .when { color: var(--muted); font-weight: 400; font-size: .8rem; }
.shelf > .cards, .shelf-in { padding: 0 var(--s3) var(--s3); }
.shelf-in h3 { font-size: .8rem; color: var(--muted); margin: .5rem 0 .3rem; font-weight: 600; }
.doc-kind { margin-right: .35rem; }
/* What the native field means, in words. Empty until a date is chosen. */
.date-echo { display: block; font-size: .75rem; color: var(--muted); margin-top: .15rem; }
.date-echo:empty { display: none; }
.card-actions { display: flex; gap: .8rem; margin-top: .3rem; }

/* --- Task detail --- */
.crumbs { color: var(--muted); font-size: .8rem; margin: 0 0 .25rem; }
.detail-status { display: grid; gap: .5rem; margin: .9rem 0; }
.detail-status > form:first-child button { width: 100%; min-height: 52px; font-size: 1.05rem; }
.detail-status .assign-form select { flex: 1; }
@media (min-width: 760px) {
  .detail-status { display: flex; flex-wrap: wrap; align-items: center; }
  .detail-status > form:first-child button { width: auto; min-width: 12rem; }
  .detail-status .assign-form select { flex: 0 0 14rem; }
}
/* Well clear of the comment box above it: the last thing on the page, and not
   something to hit on the way to anything else. */
.task-copy { display: grid; gap: .15rem; margin-top: 3.5rem; }

.statusbar { display: flex; gap: .3rem; flex-wrap: wrap; }
.statusbar .pill {
  background: var(--surface); color: var(--muted); border: 1px solid var(--line);
  min-height: 36px; padding: .2rem .7rem; font-size: .85rem; font-weight: 400;
}
.statusbar .pill.active { color: var(--accent-text); border-color: transparent; font-weight: 500; }
.statusbar .pill.active.status-todo { background: var(--muted); }
.statusbar .pill.active.status-in_progress { background: var(--progress); }
.statusbar .pill.active.status-done { background: var(--done); }

.warn {
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  border-left: 3px solid var(--warn);
  padding: .55rem .75rem; border-radius: var(--radius);
}
.meta { display: grid; gap: .4rem; margin: 1rem 0; }
.meta div { display: grid; grid-template-columns: 7rem 1fr; gap: .5rem; align-items: baseline; }
.meta dt { color: var(--muted); font-size: .82rem; }
.meta dd { margin: 0; display: flex; flex-wrap: wrap; gap: .25rem; }
.description {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .1rem .75rem;
}
.description a { overflow-wrap: anywhere; }
/* Prose stops at a readable measure; the wrap is wider than a line should be. */
.description, .note-body, .comments li { max-width: 65ch; }
.block { margin-top: 1.75rem; }
.plain, .comments, .activity { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.comments li, .activity li {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .5rem .7rem;
}
.comments p { margin: .15rem 0; }
.comment-meta { font-size: .78rem; color: var(--muted); }
.inline-form { display: flex; gap: .5rem; margin-top: .6rem; align-items: flex-start; }
.inline-form.stacked { display: grid; gap: .4rem; }
.inline-form input, .inline-form textarea { flex: 1; }
/* The same button as "Mark as finished", because it is the same kind of thing:
   what you came to this page to do. Full width under the box on a phone, and
   its own width on a screen that has room for the box to be wider than it. */
.comment-form button { width: 100%; min-height: 52px; font-size: 1.05rem; }
@media (min-width: 760px) {
  .comment-form button { width: auto; min-width: 12rem; justify-self: start; }
}
/* Air between the form that files something and what has already been filed.
   The submit button and the first row of the list must not read as one stack:
   they are a thing you do and a thing you read. */
.stacked-form + .cards, .stacked-form + .quick, .stacked-form + .shelf,
.stacked-form + .empty { margin-top: 4.5rem; }
.stacked-form { display: grid; gap: .15rem; max-width: 40rem; }
.stacked-form.narrow { max-width: 24rem; }
.stacked-form p { margin: .35rem 0; }
.stacked-form ul { padding-left: 1rem; }
.stacked-form label { font-weight: 500; font-size: .85rem; }
.stacked-form .errorlist { color: var(--danger); list-style: none; padding: 0; font-size: .85rem; }
.stacked-form .helptext { color: var(--muted); font-size: .8rem; }

/* One field, drawn once. The label is above the control with no colon after
   it — the colon is the single mark that made every form here read as the
   Django admin — and the help and the error belong to the field rather than
   floating after it. */
.field { display: grid; gap: 5px; margin-bottom: var(--s3); }
.field > .lab {
  font-size: .78rem; font-weight: 600; letter-spacing: .01em;
  color: var(--muted); line-height: 1.3;
}
.field .help { margin: 0; font-size: .78rem; color: var(--muted); line-height: 1.35; }
.field .field-error { margin: 0; font-size: .8rem; font-weight: 500; color: var(--danger); }
.field.has-error input, .field.has-error select, .field.has-error textarea {
  border-color: var(--danger);
}
.form-errors {
  background: color-mix(in srgb, var(--danger) 10%, var(--surface));
  border-radius: var(--radius-field); padding: var(--s3);
  margin-bottom: var(--s4); color: var(--text);
}
.form-errors p { margin: 0; font-size: .9rem; }
/* The control is the subject of the sentence, so the words go after it. */
.field.check .box-line {
  display: flex; align-items: center; gap: .6rem;
  min-height: var(--tap); cursor: pointer;
}
.field.check input {
  width: 1.2rem; height: 1.2rem; min-height: 0; flex: none;
  accent-color: var(--accent); box-shadow: none;
}
/* A choice you can see the whole of is a switch, not a drop-down that hides
   two thirds of itself. Django wraps each radio in a div of its own, which is
   what these two selectors are reaching through. */
.field.radioselect > div {
  display: flex; gap: 3px; padding: 3px;
  background: var(--surface-2); border-radius: var(--radius-pill);
  /* Stretch, not centre: a label that wraps to two lines is taller than its
     neighbours, and with each one sized to its own text the chosen segment
     came out a different shape depending on which one was chosen. */
  align-items: stretch;
}
.field.radioselect > div > div { flex: 1; min-width: 0; display: flex; }
.field.radioselect label {
  flex: 1;
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: 42px; padding: .3rem .6rem; border-radius: var(--radius-pill);
  font-size: .88rem; line-height: 1.25; color: var(--muted); cursor: pointer;
  text-wrap: balance;
}
/* Only where :has works, because hiding the radio without it would leave a
   switch with nothing to say which half is on. */
@supports selector(:has(*)) {
  .field.radioselect input {
    position: absolute; opacity: 0; width: 1px; height: 1px; min-height: 0;
  }
  .field.radioselect label:has(input:checked) {
    background: var(--surface); color: var(--text);
    font-weight: 600; box-shadow: var(--raise);
  }
  .field.radioselect label:has(input:focus-visible) {
    outline: 3px solid var(--accent); outline-offset: 2px;
  }
}
/* A list of checkboxes — the categories somebody works in — is a list, not a
   paragraph with bullets hanging off the side of it. */
.field.checkboxselectmultiple ul { list-style: none; margin: 0; padding: 0; display: grid; }
.field.checkboxselectmultiple li label {
  display: flex; align-items: center; gap: .6rem; min-height: var(--tap); cursor: pointer;
}
.field.checkboxselectmultiple input {
  width: 1.2rem; height: 1.2rem; min-height: 0; accent-color: var(--accent); box-shadow: none;
}
/* A date and a time are one answer, so they sit on one line: the phone opens
   its own calendar for the first and its own wheel for the second. */
.field.dateandtime { display: flex; flex-wrap: wrap; gap: 5px; }
.field.dateandtime > .lab { flex: 0 0 100%; }
.field.dateandtime input[type="date"] { flex: 1 1 9rem; width: auto; }
.field.dateandtime input[type="time"] { flex: 0 1 7.5rem; width: auto; }
.field.dateandtime .help, .field.dateandtime .field-error,
.field.dateandtime .date-echo { flex: 0 0 100%; }

/* --- Tables --- */
.table-scroll {
  overflow-x: auto; border: 0; box-shadow: var(--raise);
  border-radius: var(--radius); background: var(--surface);
}
.grid { border-collapse: collapse; width: 100%; font-size: .88rem; font-variant-numeric: tabular-nums; }
.grid th, .grid td { padding: .55rem .7rem; text-align: left; border-bottom: 1px solid var(--line); }
.grid th { font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 600; }
.grid tbody tr:last-child td { border-bottom: 0; }
.grid td .dot { margin-right: .4rem; vertical-align: baseline; display: inline-block; }
/* Somebody off the roster should look off it: weaker than "muted", which is
   what ordinary secondary text already is. Put on the cells rather than the
   row, because colour does not reliably inherit into a cell and opacity on a
   <tr> is not dependable in a border-collapse table. */
.grid tr.is-inactive td { color: color-mix(in srgb, var(--muted) 80%, transparent); }
.grid tr.is-inactive td a { color: inherit; }
.grid tr.is-inactive td .dot { opacity: .35; }
.grid tr.is-inactive td .chip { opacity: .6; }
.grid td.access { white-space: normal; }
.grid td.access .chip { margin: .1rem .15rem .1rem 0; }
.chip.admin { background: var(--tint); color: var(--accent); font-weight: 700; }
/* Nobody gave them a category, so they see no work at all. That is a thing
   to notice on the roster, not a quiet blank. */
.chip.cat.warn-chip { background: color-mix(in srgb, var(--warn) 20%, transparent); color: var(--warn); }
/* Read but not write: the outline of the chip somebody who works there wears.
   Said with the shape as well as the colour, because the roster is read in a
   tent in daylight. */
.chip.cat.reads {
  background: none; box-shadow: inset 0 0 0 1px var(--line-strong); color: var(--muted);
}
.grid .num { text-align: right; white-space: nowrap; }

/* --- Cards (documents, attendance, notes) --- */
.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.card {
  background: var(--surface); border: 0;
  border-radius: var(--radius); padding: var(--s3) var(--s4);
  display: grid; gap: var(--s2); box-shadow: var(--raise);
}
.card-head { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: space-between; }
.card-title { font-weight: 600; text-decoration: none; color: var(--text); }
.card-actions { display: flex; gap: .75rem; align-items: center; }
/* The handle that stands in for the roster until somebody asks for it. */
/* The link is the tap target; the chip inside it is the same size a person
   wears on the task page and on the wall. Keeping the colour on the link meant
   one person drew a bigger box than four did, for no reason but the 46px a
   thumb needs — which the link can hold on its own. */
a.assign {
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  color: inherit; text-decoration: none;
}
/* Nobody has it yet: no colour to wear, so it is an outline — which is what it
   is, an empty slot asking to be filled. */
a.assign.nobody .chip {
  background: none; box-shadow: inset 0 0 0 1px var(--line); color: var(--muted);
}
a.assign:hover .chip { box-shadow: inset 0 0 0 1px var(--line-strong); }
.card p { margin: 0; }
.card-title.big { font-size: 1.1rem; font-weight: 700; letter-spacing: -.02em; }
/* Who wrote the row down, under a rule of its own rather than as one more grey
   line among the facts: it answers a different question than the card does. */
.card-foot { border-top: 1px solid var(--line); padding-top: var(--s2); }
.shelf > summary .who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (min-width: 760px) {
  .cards { grid-template-columns: repeat(2, 1fr); }
  .cards.notes { grid-template-columns: 1fr; }
}

.card.me { border-left: 3px solid var(--line-strong); margin-bottom: .5rem; }
.card.me > form > button, .card.me .check-out > summary {
  width: 100%; min-height: 52px; font-size: 1.05rem;
}
/* Which way the button goes is said twice: by colour, and by which side of the
   card it sits on. Scanning a column of cards for who is still out then needs
   no reading at all. Still 12rem and 52px, so it is a thumb-sized target on a
   phone; full width bought nothing but a wider red rectangle. */
.attendance .card > form,
.attendance .card > .check-out { justify-self: start; }
.attendance .card.present > .check-out { justify-self: end; }
/* Opened, the reason and the note need the whole card, not the right half. */
.attendance .card > .check-out[open] { justify-self: stretch; }
.attendance .card > form > button, .attendance .check-out > summary {
  width: auto; min-width: 12rem; min-height: 52px; font-size: 1.05rem; font-weight: 600;
  justify-content: center;
}
.attendance .check-out[open] > summary { width: 100%; }
@media (min-width: 760px) {
  .card.me > form > button, .card.me .check-out > summary { width: auto; min-width: 12rem; }
}
.card.me.present { border-left-color: var(--done); }
.card.me .who { display: flex; align-items: center; gap: .45rem; font-weight: 600; }
.card.me .hours { font-variant-numeric: tabular-nums; color: var(--muted); font-size: .85rem; }
.card.me details > summary { cursor: pointer; width: fit-content; }
.card.me .check-out > summary { display: inline-flex; }

/* Two things carry this page across a tent: whose card it is, and the one
   button on it. Everything else is detail, and detail is off by default. */
.attendance .card { border-left: 4px solid var(--line-strong); }
.attendance .card.present {
  border-left-color: var(--done);
  background: color-mix(in srgb, var(--done) 8%, var(--surface));
}
.attendance .who {
  display: flex; align-items: center; gap: .5rem;
  font-weight: 700; font-size: 1.15rem; line-height: 1.2;
}
.attendance .who .dot { width: .7rem; height: .7rem; }
.attendance .state { margin: 0; }
.attendance details > summary { cursor: pointer; min-height: 36px; display: flex; align-items: center; }
.attendance .check-out > summary { width: fit-content; justify-content: center; }
.attendance .spells ul { margin-top: .35rem; gap: .15rem; }

/* Off unless asked for, by the checkbox above the list. Driven by :has() so
   the toggle works on its own; the script only remembers the choice. */
.attendance-wrap .detail { display: none; }
.attendance-wrap:has(.detail-toggle input:checked) .detail { display: grid; gap: .3rem; }
.detail-toggle {
  display: inline-flex; align-items: center; gap: .45rem;
  margin: 0 0 .6rem; color: var(--muted); font-size: .85rem; cursor: pointer;
}

.notes .card.resolved { opacity: .6; }
.notes .card.resolved .note-body { text-decoration: line-through; }
.note-form { margin-bottom: 1.25rem; }

/* --- Statistics and charts --- */
/* Wraps to one per line on a narrow phone and sits in a row on anything wider;
   no breakpoint needed, the numbers decide for themselves. */
.stats {
  display: grid; gap: .5rem; margin: .8rem 0;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
}
.stat {
  background: var(--surface); border: 0; box-shadow: var(--raise);
  border-radius: var(--radius); padding: var(--s3) var(--s4);
  display: flex; flex-direction: column; gap: .1rem;
}
.stat b { font-size: 1.5rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat span { font-size: .72rem; color: var(--muted); }
/* The letter as it was written, in the width it was written to. */
.letter {
  margin: 0; white-space: pre-wrap; word-break: break-word;
  font: inherit; color: var(--text);
}
.pick-all { display: flex; gap: .4rem; margin-bottom: .5rem; }
.pick-all button {
  min-height: 36px; padding: .25rem .6rem; font-size: .85rem;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer;
}

/* The filters in force, each one a tap away from being taken off. Above the
   drawer, because what a list is doing has to be readable without opening
   anything. */
/* The row of buttons that ends a form. Room under it, because what follows is
   another thing to act on — the welcome letter, the password link — and the two
   ran together closely enough to look like one group of four buttons. */
.stacked-form .filter-actions { margin-top: 1rem; margin-bottom: 1.6rem; }
.stacked-form .filter-actions + .card,
.stacked-form + .card { margin-top: 1.6rem; }

/* Said to a screen reader, drawn for nobody: the cross alone is not a word. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.chosen {
  list-style: none; display: flex; flex-wrap: wrap; gap: .4rem;
  margin: 0 0 .6rem; padding: 0;
}
.chosen a {
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: 36px; padding: .2rem .5rem .2rem .75rem;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 999px;
  font-size: .82rem; font-weight: 500; text-decoration: none;
}
.chosen a:hover { border-color: var(--line-strong); }
.chosen a span[aria-hidden] { color: var(--muted); font-size: 1rem; line-height: 1; }
.chosen a:hover span[aria-hidden] { color: var(--danger); }
.chosen-clear a {
  background: none; border-color: transparent; color: var(--muted);
  text-decoration: underline; padding-left: .3rem;
}

/* Rarely, and on purpose: a line at the foot, not a tab across the top. */
.archive-link { margin-top: 1.4rem; }

/* How many are on the roster, under the roster. */
.roster-total {
  margin: .8rem 0 0; color: var(--muted); font-size: .85rem;
  font-variant-numeric: tabular-nums;
}

.chart-head { font-size: .85rem; color: var(--muted); font-weight: 600; margin: 1.2rem 0 .4rem; }
.chart { margin: 0 0 1rem; }
.chart svg {
  display: block; width: 100%; height: 7rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.chart-line { fill: none; stroke: var(--accent); stroke-width: 2; }
.chart-area { fill: color-mix(in srgb, var(--accent) 18%, transparent); stroke: none; }
.chart-bar { fill: color-mix(in srgb, var(--accent) 62%, transparent); }
.chart-axis {
  display: flex; justify-content: space-between;
  font-size: .7rem; color: var(--muted); padding: .25rem .1rem 0;
}
/* What the height of a column is worth. Without it the shape is a silhouette,
   and two charts one under the other invite a comparison neither supports. */
.chart-plot { position: relative; }
.chart-grid { stroke: var(--line-strong); stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart-tick {
  position: absolute; right: .35rem; transform: translateY(50%);
  font-size: .7rem; line-height: 1; color: var(--muted);
  font-variant-numeric: tabular-nums; pointer-events: none;
  /* Sitting on the line it labels, so it needs the panel behind it. */
  background: var(--surface); padding: 0 .25rem; border-radius: 3px;
}

/* --- Timeline --- */
.tl-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .3rem 0;
}
.tl-row { display: flex; align-items: stretch; min-height: 2rem; }
.tl-label {
  flex: 0 0 8.5rem; position: sticky; left: 0; z-index: 3;
  background: var(--surface); border-right: 1px solid var(--line);
  padding: .3rem .5rem; font-size: .8rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tl-label a { color: var(--text); text-decoration: none; }
.tl-track { position: relative; flex: 1 0 48rem; margin: .12rem 0; }
.tl-ruler { position: sticky; top: 0; z-index: 4; background: var(--surface); border-bottom: 1px solid var(--line); }
.tl-hour {
  position: absolute; top: 0; bottom: 0;
  border-left: 1px solid var(--line);
  font-size: .7rem; color: var(--muted); padding-left: .2rem;
}
.tl-bar {
  position: absolute; top: 0; bottom: 0;
  display: flex; align-items: center;
  background: color-mix(in srgb, var(--bar, var(--todo)) 38%, var(--surface-2));
  border: 1px solid color-mix(in srgb, var(--bar, var(--todo)) 65%, transparent);
  border-radius: 5px; padding: 0 .3rem; overflow: hidden;
  color: var(--text); text-decoration: none; font-size: .7rem; white-space: nowrap;
}
.tl-bar.status-done { opacity: .5; }
.tl-bar.status-in_progress { box-shadow: inset 0 0 0 2px var(--progress); }
.tl-bar.cut-start { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left-style: dashed; }
.tl-bar.cut-end { border-top-right-radius: 0; border-bottom-right-radius: 0; border-right-style: dashed; }
.tl-now { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--warn); z-index: 2; }

@media (min-width: 760px) {
  .tl-label { flex-basis: 15rem; }
  .tl-track { flex-basis: 60rem; }
}
/* Given a desktop, spend it: the track stops insisting on 60rem and takes
   whatever is there, so a whole day fits and the sideways scroll goes away.
   Below this the fixed basis stays, because scrolling a day on a phone is the
   right answer and squeezing seventeen hours into 380px is not. */
@media (min-width: 1180px) {
  .tl-track { flex: 1 1 auto; min-width: 0; }
  .tl-row { min-height: 2.3rem; }
  .tl-bar { font-size: .75rem; }
  .tl-hour { font-size: .75rem; }
}

/* --- The weather: the next half day, in the side column --- */
.weather-rows {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: clamp(.2rem, .5vh, .45rem);
}
.weather-rows li {
  display: grid; grid-template-columns: 2em 2em 2.8em 1fr auto;
  gap: .55em; align-items: baseline;
  /* "0 mm" broke across two lines in a column this narrow, which doubled the
     height of every row for the sake of two characters. */
  white-space: nowrap;
  background: var(--surface-2); border-radius: var(--radius);
  padding: clamp(.3rem, .6vh, .55rem) clamp(.6rem, .8vw, 1rem);
  font-size: calc(clamp(1rem, 1.35vw, 1.65rem) * var(--wall, 1)); font-variant-numeric: tabular-nums;
}
.w-time { color: var(--muted); }
.w-sym { text-align: center; font-size: 1.35em; line-height: 1; }
.w-temp { font-weight: 600; }
.w-temp.cold { color: var(--accent); }
.w-precip { color: var(--accent); }
/* A dry block says so outright. Blank read as "nobody looked". */
.w-precip.dry { color: var(--muted); }
.w-wind { color: var(--muted); }
/* --- The dish, in the corner of the screen --- */
.dish {
  position: fixed; right: var(--screen-pad); bottom: var(--screen-pad);
  z-index: 15; display: grid; gap: .15rem;
  /* Normally the width of the narrow column. A terminal reporting three
     figures is rare, but it must widen rather than cut the numbers off. */
  width: max-content; min-width: min(23rem, 20vw); max-width: min(28rem, 24vw);
  padding: .5rem clamp(.7rem, .9vw, 1rem);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--pane-radius);
  font-variant-numeric: tabular-nums; line-height: 1.35;
}
.dish[hidden] { display: none; }
@media (max-width: 1099px), (orientation: portrait) {
  /* min-width and max-width from the rule above are what make it a corner
     panel; left standing they squeezed the whole readout into a quarter of a
     phone and wrapped every number onto its own line. */
  .dish {
    position: static; width: auto; min-width: 0; max-width: none;
    margin: .6rem var(--screen-pad);
  }
}
/* The state colours the word that names it, and nothing else. The panel keeps
   the same plain edge as everything else on the wall whatever the dish is
   doing: a box that grows a coloured side is the one thing on the screen that
   does not look like the rest of the screen. */
.dish.warn .dish-state { color: var(--warn); }
.dish.down .dish-state { color: var(--danger); }
.dish-body { display: block; }
.dish-lines { display: grid; gap: .2rem; min-width: 0; }
.dish .dish-usage { font-size: calc(clamp(.82rem, .95vw, 1.1rem) * var(--wall, 1)); color: var(--muted); }
.dish .dish-usage b { color: var(--text); font-weight: 600; }
.dish h2 {
  display: flex; align-items: baseline; gap: .5rem; margin: 0 0 .1rem;
  font-size: calc(clamp(.78rem, .9vw, 1.05rem) * var(--wall, 1)); text-transform: uppercase;
  letter-spacing: .1em; color: var(--muted); font-weight: 600;
}
.dish .dish-state { letter-spacing: 0; text-transform: none; color: var(--text); font-weight: 600; font-size: 1.15em; }
.dish p { margin: 0; display: flex; flex-wrap: wrap; gap: .15rem .8rem; }
.dish .dish-rates {
  font-size: calc(clamp(.9rem, 1.1vw, 1.35rem) * var(--wall, 1)); font-weight: 500;
  flex-wrap: nowrap; white-space: nowrap; gap: .85rem;
}
.dish .dish-quality { font-size: calc(clamp(.82rem, .95vw, 1.1rem) * var(--wall, 1)); color: var(--muted); }
.dish .dish-alerts { font-size: calc(clamp(.9rem, 1vw, 1.2rem) * var(--wall, 1)); color: var(--warn); }
.dish .dish-alerts:empty { display: none; }
/* Made-up numbers have to say so, right above the numbers themselves. */
.dish .dish-demo {
  margin: 0 0 .15rem; padding-bottom: .2rem;
  border-bottom: 1px solid var(--line);
  font-size: calc(clamp(.85rem, .95vw, 1.15rem) * var(--wall, 1)); color: var(--warn);
}
.dish .dish-demo[hidden] { display: none; }

/* --- Live update offer --- */
.live {
  position: fixed; left: 0; right: 0; z-index: 25;
  bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom) + .6rem);
  display: flex; justify-content: center; pointer-events: none;
}
.live[hidden] { display: none; }
.live button { pointer-events: auto; box-shadow: 0 6px 20px rgb(0 0 0 / .3); }

@media (min-width: 760px) {
  .live { bottom: 1rem; }
}

/* --- Screen view: a television in the tent, and a phone in a pocket --- */
/* The wall keeps its own answer, apart from the one the planner is set to. They
   are different displays: a phone follows the hand holding it, and a television
   on a tent wall follows the light in the tent — which at noon in September is
   not the same thing at all. Dark unless somebody standing there says otherwise,
   because that is what it is most of the weekend. */
body.screen {
  color-scheme: dark;
  --bg: #0b0e13; --surface: #151a21; --surface-2: #1e242d;
  --text: #f3f6fa; --muted: #93a0b0; --line: #29313b; --line-strong: #5a6675;
  --accent: #8fb073; --accent-text: #0c1016;
  --todo: #77828f; --progress: #8b83ff; --done: #33b06a;
  --warn: #d99154; --danger: #e5715f;
  --pane-radius: 14px;
  /* Everything on the wall is sized through this: one number to take the
     whole display in or let it out, without touching a dozen clamps. */
  --wall: .9;
  --screen-pad: clamp(.75rem, 1.4vw, 1.6rem);
  --screen-gap: clamp(.6rem, 1.1vw, 1.4rem);
  background: var(--bg); color: var(--text); padding-bottom: 0;
}

/* Daylight on the wall. Only the colours move; every size, gap and clamp above
   is the same screen. */
:root[data-screen-theme="light"] body.screen {
  color-scheme: light;
  --bg: #eef1f5; --surface: #ffffff; --surface-2: #e3e8ee;
  --text: #12171d; --muted: #56616e; --line: #ccd3dc; --line-strong: #7c8895;
  --accent: #4f6c3d; --accent-text: #ffffff;
  --todo: #7d8894; --progress: #4f46e5; --done: #1a7f4b;
  --warn: #a8501c; --danger: #b3261e;
}

/* Top right of the header, beside the clock: the one control on a page that is
   otherwise only read. Big enough to hit with a glove on. */
.screen-theme {
  /* Hard right and centred on the header's own height, clear of both the title
     and the clock rather than in the flow beside either of them. */
  position: absolute; right: .5rem; top: 50%; transform: translateY(-50%);
  min-width: var(--tap); min-height: var(--tap);
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--line);
  border-radius: var(--pane-radius); color: var(--muted);
  font-size: 1.2rem; line-height: 1; cursor: pointer;
}
.screen-theme:hover { color: var(--text); border-color: var(--line-strong); }

/* The wrap fills the display; on a television it must never scroll, because
   there is nothing to scroll it with. */
.screen-wrap {
  max-width: none; margin: 0;
  padding: clamp(.35rem, .5vw, .6rem) var(--screen-pad) var(--screen-pad);
  display: flex; flex-direction: column;
  gap: clamp(.3rem, .7vh, .6rem);
  min-height: 100vh; min-height: 100dvh;
}

.screen-head {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center;
  gap: .1rem 1rem; padding: clamp(.2rem, .3vw, .4rem) clamp(.7rem, 1vw, 1.25rem);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--pane-radius);
  /* Room at the right for the day-night button, which is taken out of the flow
     and would otherwise have the date running underneath it. Set here, after
     the shorthand above and inside the same rule: as its own line further up it
     was simply overwritten, which is exactly how the date ended up under it. */
  position: relative;
  padding-right: calc(var(--tap) + 1rem);
}
/* One size across the whole header. The seconds, the zone and the date used to
   step down from the hour, which read as three different things on a wall meant
   to be taken in at a glance from across a tent. Written once so the title and
   the clock cannot drift apart. */
.screen-head { --head-size: calc(clamp(1.1rem, 1.6vw, 1.8rem) * var(--wall, 1)); }
/* Narrow enough that the header wraps to two lines, and a button pinned to the
   middle of it sits over the second one. In the flow instead, at the end of the
   row it already wraps into. */
@media (max-width: 699px) {
  .screen-head { padding-right: clamp(.7rem, 1vw, 1.25rem); }
  .screen-theme { position: static; transform: none; margin-left: auto; }
}
.clock-zone { margin-left: .3em; font-size: 1em; font-weight: 600; letter-spacing: .08em; }
.screen-head h1 { font-size: var(--head-size); }
/* Clickable, but it stays a heading: the screen is furniture, not a web page. */
.screen-head h1 a { color: inherit; text-decoration: none; }
.clock { display: flex; align-items: baseline; gap: .6rem; margin: 0; }
.clock-time {
  font-size: var(--head-size); font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
/* One size and one colour across the whole header: the hour, the seconds, the
   zone and the date are one line, read in one glance from the other end of a
   tent. Half of it in --muted made it read as a heading with a footnote. */
.clock, .clock-seconds, .clock-date { font-size: var(--head-size); color: var(--text); }
.clock-seconds { font-size: 1em; font-weight: 600; }

.screen-grid, .screen-side, .screen-col { display: grid; gap: var(--screen-gap); }
.screen-side { align-content: start; }
.screen-block {
  min-width: 0; display: flex; flex-direction: column;
  padding: clamp(.45rem, .65vw, .85rem);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--pane-radius);
}
.screen-block h2 {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .45rem; padding: .1rem .25rem .4rem;
  border-bottom: 1px solid var(--line);
  font-size: calc(clamp(.85rem, 1.3vw, 1.5rem) * var(--wall, 1)); text-transform: uppercase;
  letter-spacing: .1em; color: var(--muted); font-weight: 600;
}
.screen-block h2 .tally {
  font-size: .95em; letter-spacing: 0; color: var(--text);
  background: var(--surface-2); border-radius: 999px; padding: .05em .55em;
}

.screen-list { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(.25rem, .55vh, .5rem); align-content: start; min-height: 0; }
.screen-list li {
  display: grid; grid-template-columns: max-content minmax(0, 1fr);
  gap: .15rem .8rem; align-items: baseline;
  background: var(--surface-2); border-radius: var(--radius);
  padding: clamp(.4rem, .8vh, .75rem) clamp(.6rem, .8vw, 1rem);
  font-size: calc(clamp(1.1rem, 1.8vw, 2.2rem) * var(--wall, 1));
}
.screen-time { color: var(--muted); font-variant-numeric: tabular-nums; text-transform: uppercase; white-space: nowrap; }
.screen-title { min-width: 0; overflow-wrap: break-word; }
.screen-people { grid-column: 2; display: flex; flex-wrap: wrap; gap: .3rem; }
.screen-people:empty { display: none; }
.screen-list.big li { font-size: calc(clamp(1.2rem, 2.2vw, 2.7rem) * var(--wall, 1)); border-left: 4px solid var(--progress); }
.screen-block.late .screen-list li { border-left: 4px solid var(--warn); }
/* How long it has been sitting there, under the time it should have happened. */
.screen-late {
  display: block; color: var(--warn); font-weight: 600;
  font-size: calc(clamp(.85rem, 1.25vw, 1.5rem) * var(--wall, 1)); text-transform: none;
}
.screen-late:empty { display: none; }

/* One name per line, in callsign order, so the same person is always in the
   same place on the wall. */
.screen-roster { display: grid; gap: .3rem; justify-items: start; padding: .1rem .25rem; }
.screen-roster .chip { font-size: calc(clamp(.95rem, 1.4vw, 1.7rem) * var(--wall, 1)); padding: .25rem .7rem; }
.screen-empty { color: var(--muted); margin: 0; font-size: calc(clamp(1rem, 1.5vw, 1.8rem) * var(--wall, 1)); }
.screen-rest { margin: .4rem 0 0; padding: 0 .25rem; color: var(--muted); font-size: calc(clamp(.85rem, 1.2vw, 1.4rem) * var(--wall, 1)); }

/* A phone is too narrow for a time column beside the title. */
@media (max-width: 560px) {
  .screen-list li { grid-template-columns: 1fr; gap: .1rem; }
  .screen-time { font-size: .85em; }
  .screen-people { grid-column: auto; }
}

/* A television: three columns, everything on one page, no scrollbar. */
@media (min-width: 1100px) and (orientation: landscape) {
  .screen-wrap { height: 100vh; overflow: hidden; }
  .screen-grid {
    flex: 1; min-height: 0;
    grid-template-columns: minmax(0, 2fr) minmax(0, 2fr) minmax(0, 1fr);  /* 40/40/20 */
    /* Without this the row sizes itself to the longest list and the columns
       run off the bottom of the screen instead of being cut off inside. */
    grid-template-rows: minmax(0, 1fr);
  }
  /* What is running takes the room it needs; what is overdue gets the rest, so
     its list ends at the bottom edge rather than mid-column. */
  .screen-col.attention { grid-template-rows: auto minmax(0, 1fr); align-content: stretch; }
  .screen-col { min-height: 0; }
  .screen-block, .screen-list { min-height: 0; overflow: hidden; }
  /* The list fills its column, so a row cut off at the bottom edge fades out
     instead of ending in a hard slice. */
  .screen-list {
    flex: 1;
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.75rem), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 1.75rem), transparent);
  }
  .screen-list li { grid-template-columns: max-content minmax(0, 1fr) max-content; align-items: center; }
  .screen-people { grid-column: auto; justify-content: flex-end; }
  /* The narrow column has no room for a third column of names. */
  .screen-side .screen-list li { grid-template-columns: max-content minmax(0, 1fr); }
  .screen-side .screen-people { grid-column: 2; justify-content: flex-start; }
  /* The corner panel floats over this column, so the column stops above it. */
  .screen-side {
    display: flex; flex-direction: column; min-height: 0;
    padding-bottom: var(--dish-space, 0px);
  }
  /* Down against the dish rather than floating under the roster: the two
     panels read as one stack in the corner. */
  .screen-side > .weather { margin-top: auto; }
  /* The fifth of the width the grid gives the narrow column, worked out the
     same way the grid does — a corner panel of its own width looked cut off
     beside the column above it. */
  .dish {
    width: calc((100vw - 2 * var(--screen-pad) - 2 * var(--screen-gap)) / 5);
    min-width: 0; max-width: none;
  }
  /* A long title must not push the rows below it off the bottom edge. */
  .screen-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
}

/* The small variants stay small only where a mouse does the pointing. Last in
   the file so it outranks the component sizes above on a phone. */
@media (max-width: 759px) {
  .button.small, button.small, .statusbar .pill { min-height: var(--tap); }
}
