:root {
  /* Built around Zoom's blue. Surfaces and text are the same hue drained of
     saturation, so the one saturated colour in the interface is the accent and
     the eye goes straight to whatever is actionable.

     This replaces a warm paper-and-clay palette. Note what the change costs:
     blue used to mean "that lives in Zoom, not here", which only worked while
     nothing else was blue. Now that the interface is blue, that distinction is
     carried by treatment instead of hue — a filled control acts here, an
     outlined one hands you to Zoom, a tinted chip is a reference Zoom owns.

     Every pairing below was checked against WCAG AA at small text sizes; the
     tints are as light as they are so the text on them clears 4.5:1. */
  --paper:        #F3F7FE;
  --surface:      #FFFFFF;
  --surface-sunk: #E9F0FC;
  --ink:          #0C1B33;
  --ink-soft:     #53658A;
  --line:         #D8E2F2;
  --accent:       #0B5CFF;
  --accent-soft:  #EBF2FF;
  --good:         #0A6B53;
  --warn:         #8A6412;
  --bad:          #C0263C;
  --radius:       10px;
  --gap:          1.1rem;
  --measure:      68ch;

  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  font-size: 15px;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { font-family: var(--serif); font-weight: 600; line-height: 1.25; letter-spacing: -0.01em; }
h1 { font-size: 1.6rem; margin: 0 0 .5rem; }
h2 { font-size: 1.2rem; margin: 1.9rem 0 .8rem; }
h3 { font-size: 1.02rem; margin: 1.5rem 0 .5rem; }
p  { max-width: var(--measure); }
/* Secondary text: the sentence that qualifies something rather than stating
   it. This was only ever defined as `.field .hint`, so every hint standing on
   its own outside a field fell through to ordinary body copy. */
.hint { color: var(--ink-soft); font-size: .88rem; }
/* Bold inside secondary text marks the one phrase that changes what the
   reader does — the consequence, the reassurance, the instruction — so that a
   long note has a spine to skim down instead of being read or skipped whole.
   It lifts to full ink as well as gaining weight: against muted text, weight
   alone is a difference you have to look for. One per paragraph, two only
   where the paragraph really has two halves, and never a whole sentence — the
   moment everything is emphasised, nothing is. */
.hint strong, .lede strong, .status-note strong,
.choice p strong, .banner p strong, .consequences strong {
  color: var(--ink); font-weight: 600;
}
a  { color: var(--accent); text-underline-offset: 3px; }

.masthead {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  padding: 1rem 0;
}
.masthead-inner, .page { max-width: 1120px; margin: 0 auto; padding: 0 2rem; }
/* Its own flex layout (not the .nav class) so the brand link never inherits
   .nav a's padding, which was nudging it out of line with the page content. */
.masthead-inner { display: flex; align-items: center; justify-content: space-between; }
.page { padding-top: 1.9rem; padding-bottom: 3rem; }
/* A page that asks rather than shows. At the full width of a list page a form
   leaves its card far wider than the fields inside it, which reads as a layout
   that stopped halfway; here the card is the width of what it holds. The cap
   on single fields comes off with it — it exists to stop an input running the
   width of a table page, and this column is already that width. */
.page-narrow { max-width: 47rem; }
.page-narrow .field { max-width: none; }

.brand {
  display: inline-flex; align-items: center; gap: .6rem;
  font-family: var(--serif); font-size: 1.2rem; text-decoration: none; color: var(--ink);
}
/* Sized in rem, not em, so the mark keeps its own proportions instead of
   tracking the wordmark's serif face. Block display drops the inline
   baseline gap that would otherwise push the mark off centre. */
.brand img { width: 1.75rem; height: 1.75rem; display: block; }
.nav { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.nav a {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .5rem .85rem; border-radius: var(--radius);
  text-decoration: none; color: var(--ink-soft);
}
.nav a:hover  { background: var(--surface-sunk); color: var(--ink); }
.nav a.active { background: var(--accent-soft); color: var(--accent); }

/* A page's own primary action sits on the title line instead of taking a row to
   itself. It wraps under the heading on narrow screens, where side by side would
   squeeze both. The h1 keeps its bottom margin here so the lede below is unaffected. */
.page-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
}
.page-head h1 { margin-bottom: 0; }
.page-head + .lede { margin-top: .5rem; }

.lede { font-size: 1rem; color: var(--ink-soft); max-width: var(--measure); margin: 0 0 2rem; }

/* The parts of one event. Real links, so the underline marks which page you
   are on rather than which panel a script last showed.

   They all mark the current page the same way. Two of them used to borrow the
   colour of the region they open, but a tab already labelled "Panelists &
   interpreters" is not hard to find, and one amber tab in a row of blue ones
   asks a question it never answers. Colour still separates the regions inside
   a page, where two cards take the same kind of file for different people.
   Only deleting keeps a colour of its own here, and it is not a peer of the
   other four. */
.tabs {
  display: flex; flex-wrap: wrap; gap: .15rem;
  border-bottom: 1px solid var(--line);
  margin: 1.6rem 0 1.75rem;
}
.tabs a {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .6rem .9rem; margin-bottom: -1px;
  text-decoration: none; color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  border-radius: var(--radius) var(--radius) 0 0;
  white-space: nowrap;
}
.tabs a .icon { width: 1em; height: 1em; }
.tabs a:hover { color: var(--ink); background: var(--surface-sunk); }
.tabs a.active {
  color: var(--tab, var(--accent));
  border-bottom-color: var(--tab, var(--accent));
  font-weight: 600;
}
.tabs a.tab-danger { --tab: var(--bad); }
.tabs a.tab-danger:hover { color: var(--bad); background: #FBE1E6; }
/* Deleting is on this bar because it belongs to an event, not because it
   ranks with the other four. The gap says so. */
.tabs .tab-end { margin-left: auto; }
@media (max-width: 46rem) { .tabs .tab-end { margin-left: 0; } }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.25rem 1.4rem; margin-bottom: var(--gap);
}
.card > :first-child { margin-top: 0; }

/* A drop zone's tint. It appears there and nowhere else — never on a button,
   so it cannot blur what a control's colour means.

   Both drop zones now carry the accent. The panelist one was amber for a
   day, and the reason was sound at the time: the two took the same kind of
   file for very different people, they sat on the same long page, and a
   colour learned over a few visits was the one guard against dropping the
   attendee list into the box that hands everybody the right to speak.

   Splitting an event into five tabs took that reason away. The two are
   never on screen together any more. Each is behind its own tab, under its
   own heading, over its own sentence saying what to drop and what it sets
   off — three labels apiece, all of them read before a file is let go, and
   none of them a colour anyone had to learn.

   What amber still carried was "mind this", which the panelist upload
   genuinely is. That is now said in words on the zone itself, and the guard
   that actually stops the mistake was never the colour: it is the review
   screen, which counts how many of the people in the file are already
   attending and refuses to go quietly if the answer looks like the wrong
   list. See teamReviewPage.

   The classes stay, because they still say which region a card belongs to,
   and because bringing a colour back should be a one-line change. */
.card-region { --region: var(--accent); --region-soft: var(--accent-soft); --region-line: #A8C5FF; }
.card-region .dropzone { background: var(--region-soft); border-color: var(--region-line); }
.card-region .dropzone-hint strong { color: var(--region); }
.card-footer { border-top: 1px solid var(--line); margin-top: 1.5rem; padding-top: 1.25rem; }

/* What an irreversible action takes with it, one kind of thing per line. A
   paragraph listing three losses in a row is read as one statement and
   skimmed as none; separate lines are counted. The glyph says which kind of
   thing before the line is read. */
.consequences { list-style: none; padding: 0; margin: 1rem 0 1.4rem; display: grid; gap: .6rem; }
.consequences li { display: flex; align-items: flex-start; gap: .65rem; }
.consequences .icon {
  width: 1.05rem; height: 1.05rem; flex: none; margin-top: .32rem; color: var(--ink-soft);
}

.check-row { display: flex; gap: .6rem; align-items: center; margin: 0 0 .4rem; }
.check-row + .hint { margin-top: 0; margin-bottom: 1.4rem; max-width: var(--measure); }

/* Where a tab stands, above the detail it summarises. The state reads as a
   sentence rather than as a figure in a tile: there is one number worth
   knowing here, and a sentence carries it with its meaning attached, where a
   large numeral would need a label underneath to say what it counted.

   The rule underneath separates the summary from the table without making it
   a second card — it is the same content, said shorter. */
.status-row {
  display: flex; flex-wrap: wrap; gap: .75rem 1.5rem;
  align-items: flex-start; justify-content: space-between;
  border-bottom: 1px solid var(--line);
  padding-bottom: 1.15rem; margin-bottom: 1.35rem;
}
/* Nothing below it to be separated from — the attendance card is the summary
   and nothing else — so the rule would be a line drawn under the last thing
   on the card. */
.status-row:last-child { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
.status-state { min-width: 18rem; flex: 1 1 22rem; }
.status-line { margin: 0; font-size: 1.05rem; }
.status-line .pill { vertical-align: 1px; margin-left: .35rem; }
.status-note { margin: .35rem 0 0; color: var(--ink-soft); max-width: var(--measure); }
/* The actions keep their reading order — the primary one first — but sit to
   the right of the state on a screen wide enough to hold both. */
.status-actions { flex: 0 0 auto; }
@media (max-width: 46rem) { .status-actions { width: 100%; } }

/* ─── The schedule ──────────────────────────────────────────────────────────

   An event is days times sessions, and drawn as a flat table it printed the
   date once per session and the length of a session once per row — nine rows
   to carry three dates and one duration. What is repeated has to be read and
   discarded before the shape can be seen, and the shape is the only reason to
   open this page.

   So: the day is said once, at the head of the times it holds, and the times
   sit in a column of fixed width. That column is what does the work. 09:30 on
   Thursday lands directly above 09:30 on Friday, so an event that keeps to
   the same hours every day looks like one at a glance, and a day that breaks
   the pattern is the only thing on the page that is out of line. */
.timetable { list-style: none; margin: 0; padding: 0; }
.timetable-day {
  display: grid; grid-template-columns: 4.3rem minmax(0, 1fr);
  gap: 0 1.25rem;
  padding: .8rem 0;
  border-top: 1px solid var(--line);
}
.timetable-day:first-child { border-top: 0; padding-top: .1rem; }
.timetable-day:last-child { padding-bottom: .1rem; }

/* Each day on its own surface. The list stays a list — a schedule is a list
   of days, and a day is not a section of the page — so only the drawing
   changes. Every box carries the same padding as every other card, which is
   what keeps 09:30 on Thursday directly above 09:30 on Friday across the
   gaps: the borders group what the eye was already grouping, rather than
   cutting the column the layout is built on. */
.timetable-boxed .timetable-day {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.1rem 1.4rem; margin-bottom: var(--gap);
}
.timetable-boxed .timetable-day:first-child { border-top: 1px solid var(--line); }
/* The date sits on the baseline of the first session it heads, not a line
   above it. Stacked, the numeral came to rest beside the *second* row of
   times and looked as though it belonged to that one. */
.timetable-date { margin: 0; max-width: none; padding-top: .3rem; }
/* The same uppercase treatment as a table's column header, which is what it
   is: a label for the figure beside it. Fixed width, because the point of
   the whole column is that the numbers line up, and MON is wider than TUE. */
.timetable-weekday {
  display: inline-block; width: 2.5rem;
  font-size: .85rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-soft); vertical-align: baseline;
}
/* The display face, on the one numeral in the interface that is a date
   rather than a quantity. Tabular, so 9 and 10 share a left edge. */
.timetable-daynum {
  font-family: var(--serif); font-size: 1.45rem; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
/* Printed only on a day whose month is not the month of the day above it —
   so never, on an event held inside one month, where the month and the year
   are said once in the summary instead. */
.timetable-month {
  display: block; margin-top: .1rem;
  font-size: .85rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft);
}
.timetable-slots { list-style: none; margin: 0; padding: 0; }
.timetable-slot {
  display: flex; align-items: baseline; gap: 1.25rem;
  padding: .3rem 0;
}
.timetable-time {
  flex: none; width: 8rem;
  font-variant-numeric: tabular-nums; letter-spacing: .01em;
}
.timetable-to { color: var(--ink-soft); padding: 0 .35rem; }
/* What the session is called, which the times above have mostly already said.
   Kept because attendance reports sessions by this name and the reader needs
   the two to match, but set as the secondary thing it is. */
.timetable-label { color: var(--ink-soft); font-size: .93rem; }
/* A session that runs past midnight. Rare, and silently wrong if unmarked —
   the end time would simply read as earlier than the start. */
.timetable-over {
  font-size: .85rem; vertical-align: .3em; margin-left: .15rem; color: var(--ink-soft);
}
@media (max-width: 34rem) {
  .timetable-day { grid-template-columns: 4.2rem minmax(0, 1fr); gap: 0 1rem; }
  .timetable-slot { gap: .75rem; }
  .timetable-time { width: 7.25rem; }
}

/* ─── Which languages an event is interpreted into ─────────────────────────

   The one thing on the panelist page that a sentence could not carry. It was
   already there, spread down fourteen rows of an "Interprets" column, and
   answering "is every language covered, and by how many people" meant reading
   every row and keeping a tally.

   It is set in the accent tint because that is this interface's word for
   something Zoom owns, which a language pair is — the same chip the column
   below uses, so the summary and the detail read as one vocabulary. The count
   is cut out in the surface colour rather than tinted deeper: at this size
   the accent needs the lighter ground behind it to stay legible. */
/* The block fills a card of its own now, so the card's padding is the only
   spacing it needs. The small uppercase heading it used to carry went with
   the move: a card gets a card's title. */
.coverage { margin: 0; }
.coverage-lane {
  display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap;
  margin-bottom: .4rem;
}
.coverage-from { font-weight: 600; white-space: nowrap; }
.coverage-arrow { color: var(--ink-soft); font-weight: 400; margin-left: .45rem; }
.coverage-langs {
  display: flex; flex-wrap: wrap; gap: .4rem;
  list-style: none; margin: 0; padding: 0;
}
.lang {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .18rem .22rem .18rem .65rem; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  font-size: .88rem; white-space: nowrap;
}
.lang-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.4rem; height: 1.4rem; padding: 0 .35rem;
  border-radius: 999px; background: var(--surface);
  font-size: .85rem; font-weight: 600; font-variant-numeric: tabular-nums;
}

.btn {
  display: inline-flex; align-items: center; gap: .55rem;
  font: inherit; font-size: .95rem;
  padding: .45rem .85rem; border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  cursor: pointer; text-decoration: none;
}
.btn:hover { background: var(--surface-sunk); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: #0A4ADB; }
.btn-danger { background: var(--bad); border-color: var(--bad); color: #fff; }
.btn-danger:hover { background: #A31F32; }
.btn-quiet { border-color: transparent; background: transparent; color: var(--ink-soft); }
/* Outlined, against .btn-primary's fill: the fill acts here, the outline hands
   you to Zoom. Must follow .btn:hover — same specificity, so order decides. */
.btn-zoom { color: var(--accent); border-color: var(--accent); background: var(--surface); }
.btn-zoom:hover { background: var(--accent-soft); }
/* Colour on hover only. At rest these sit quiet in a table; under the pointer
   they answer "what does this one do?" just as it is being asked. Must follow
   .btn:hover — same specificity, so source order decides. */
.btn-remove:hover { background: #FBE1E6; border-color: var(--bad); color: var(--bad); }
.btn-copy:hover   { background: #FAEED6; border-color: var(--warn); color: var(--warn); }
.btn-restore:hover { background: #E3F4EE; border-color: var(--good); color: var(--good); }

.btn[disabled] { opacity: .5; cursor: not-allowed; }

/* A control that is waiting on Zoom. The glyph is swapped for a spinner
   inside the icon's own box, so the button does not change size by a pixel —
   a control that resizes under the pointer is worse than one that looks
   inert. See startSpinner in app.js. */
@keyframes icon-spin { to { transform: rotate(360deg); } }
.icon-spin { animation: icon-spin .7s linear infinite; transform-origin: 50% 50%; }
@media (prefers-reduced-motion: reduce) {
  /* Still turning — it is reporting progress, not decorating — but slowly. */
  .icon-spin { animation-duration: 2.4s; }
}
.btn[data-busy="1"], .tabs a[data-busy="1"] { cursor: progress; }
.btn-row { display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; }
/* For controls that repeat once per row of a table, where a full-size button
   sets the row height and a column of them dominates the page. */
.btn-sm { padding: .28rem .6rem; font-size: .88rem; gap: .4rem; }
.btn-sm .icon { width: 1em; height: 1em; }

/* Ghost controls keep their padding for a comfortable target; a negative
   margin equal to that padding pulls them flush with the surrounding text. */
.inset-left  { margin-left: -0.85rem; }
.inset-right { margin-right: -0.85rem; }

.icon { width: 1.05em; height: 1.05em; flex: none; stroke-width: 1.9; }

.field { margin-bottom: 1rem; max-width: 34rem; }
.field label { display: block; font-weight: 600; margin-bottom: .35rem; }
.field .hint { margin: .3rem 0 0; }

/* Controls share one definition so an input outside a .field — the inline name
   editors on the Levels page — is styled identically to one inside. */
.field input, .field select, .field textarea, .input-inline {
  width: 100%; font: inherit; font-size: .95rem;
  padding: .45rem .7rem; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--surface); color: var(--ink);
}
/* One height for every single-line control, so a text box and a select sitting
   side by side line up. Selects carry extra intrinsic height otherwise. */
.field input:not([type=checkbox]), .field select, .input-inline {
  height: 2.2rem; line-height: 1.3;
}
.field input:focus-visible, .field select:focus-visible,
.field textarea:focus-visible, .input-inline:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
/* Align on the TOP edge: every control then starts at the same height, and a
   hint hanging under one field cannot shove its neighbour's control down. */
.field-row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
.field-row .field { flex: 1 1 12rem; }
/* A button has no label, so it needs a spacer the exact size of one to line up
   with the controls beside it. Expressed in em so it tracks the label's own box. */
.field-row .field-action { margin-bottom: 1rem; }
.field-row .field-action::before {
  content: ''; display: block; height: 1.65em; margin-bottom: .35rem;
}
.input-inline { max-width: 22rem; }

/* Two courses of action shown side by side, rather than one of them written as
   a checkbox and the other left to be inferred from not ticking it.
   auto-fit with a floor means they sit as a row on a laptop and stack on a
   phone without a media query deciding where. */
/* Same 34rem as a .field, so the form reads as one column with a consistent
   left edge and right edge rather than as three different widths stacked. */
.choices {
  display: grid; gap: .85rem; margin: 1.25rem 0 0;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  max-width: 34rem;
}
.choice {
  display: block; padding: .95rem 1.05rem; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
  transition: border-color .15s ease, background .15s ease;
}
.choice:hover { border-color: var(--accent); }
.choice-head { display: flex; align-items: center; gap: .55rem; font-weight: 600; }
.choice-head .icon { width: 1.15rem; height: 1.15rem; color: var(--ink-soft); }
/* The radio stays visible and native. The accent treatment below is an
   enhancement on top of it, never the only signal of what is selected. */
.choice input[type=radio] { accent-color: var(--accent); margin: 0; flex: none; }
.choice p { margin: .45rem 0 0; font-size: .9rem; color: var(--ink-soft); max-width: none; }
.choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.choice:has(input:checked) .choice-head,
.choice:has(input:checked) .choice-head .icon { color: var(--accent); }
.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* One person on the import review screen: who they are, then what should
   happen to them. Three options rather than two, and they sit tighter than a
   standalone pair of cards because the page may show several people at once. */
.review-row { padding: 1.1rem 0; border-bottom: 1px solid var(--line); }
.review-row:last-child { border-bottom: 0; padding-bottom: 0; }
.review-who { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; }
.review-row .choices {
  max-width: none; margin-top: .7rem; gap: .6rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.choice-inline { padding: .65rem .8rem; }
.choice-inline p { margin-top: .3rem; font-size: .85rem; }

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: .55rem .8rem; border-bottom: 1px solid var(--line); }
th { font-size: .85rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); }
tbody tr:hover { background: var(--surface-sunk); }
/* Only once the script that routes the click is actually running — see
   public/app.js. Without it the row is not clickable, and a pointer over it
   would be promising something that does not happen. */
[data-row-clicks] tbody tr:has(.row-link) { cursor: pointer; }
td.numeric, th.numeric { text-align: right; font-variant-numeric: tabular-nums; }
td.end, th.end { text-align: right; }

/* A table is width:100%, so every column without a stated width takes an equal
   share of whatever is left over. Where the last columns hold controls, that
   share is dead space *between* a button and the row it acts on. `width: 1%`
   with nowrap collapses such a column onto its own content, and the slack goes
   to the text columns that can actually use it. */
th.shrink, td.shrink { width: 1%; white-space: nowrap; }
/* Columns that refuse to wrap have to be able to go somewhere on a narrow
   screen, or the controls in them are simply cut off at the card's edge. */
.table-scroll { overflow-x: auto; }

/* A long list scrolls inside its own box, under a heading that stays put and
   a column header that stays visible. The box is capped well under a screen,
   so the cards after it are always a scroll or two away. */
.list-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;
}
.list-head h2 { margin: 0; }
.list-count {
  font-size: .9rem; font-weight: 600; color: var(--ink-soft); background: var(--surface-sunk);
  border-radius: 999px; padding: .1rem .6rem; margin-left: .4rem; vertical-align: middle;
}
.list-search { display: block; position: relative; flex: 1 1 18rem; max-width: 24rem; }
.list-search[hidden] { display: none; }
.list-search .icon { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%); color: var(--ink-soft); }
.list-search input {
  width: 100%; box-sizing: border-box; padding: .5rem .7rem .5rem 2.3rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); color: var(--ink); font: inherit;
}
.list-search input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.list-empty { color: var(--ink-soft); margin: .5rem 0 0; }
.list-scroll .table-scroll {
  max-height: min(60vh, 34rem); overflow: auto;
  border: 1px solid var(--line); border-radius: var(--radius);
}
.list-scroll thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface); box-shadow: 0 1px 0 var(--line);
}
/* Controls in a row sit on one line or the column is too narrow — a wrapped
   pair of buttons is a sizing bug, not a layout to accommodate. */
td .btn-row { flex-wrap: nowrap; gap: .5rem; }
td.end .btn-row { justify-content: flex-end; }

/* The one real link in the row: the keyboard and the screen reader reach the
   event through it, and it is what still works with no script at all. No
   underline: it collided with the row rules, and the row's own hover carries
   the affordance instead.

   Making the *whole* row clickable is deliberately not done here. It was, once:
   an empty ::after on this link, stretched over the row by `inset: 0` and
   bounded by `tbody tr { position: relative }`. A <tr> is not a containing
   block in every browser — an iPhone is one where it is not — and there the
   overlay escaped to the initial containing block instead. Every row's copy
   then landed on the same rectangle near the top of the document, the
   last row in the table was the one painted on top, and every tap in that
   rectangle opened the last event: the other rows, and the page's own
   "Plan an event" button with them.
   The row click is routed in public/app.js now, which cannot go wrong that
   way. Nothing here should grow an absolutely positioned box again. */
.row-link { text-decoration: none; font-weight: 600; }
/* The ring belongs on the row, since the row is what the click target is. */
.row-link:focus-visible { outline: none; }
tbody tr:has(.row-link:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }
.empty { color: var(--ink-soft); padding: 2.5rem 0; text-align: center; }

.banner {
  display: flex; gap: .85rem; align-items: flex-start;
  border: 1px solid var(--line); border-left: 4px solid var(--ink-soft);
  border-radius: var(--radius); padding: .8rem 1rem; margin-bottom: var(--gap);
  background: var(--surface);
}
.banner .icon { width: 1.2rem; height: 1.2rem; margin-top: .12rem; }
.banner-good    { border-left-color: var(--good); }
.banner-warning { border-left-color: var(--warn); }
.banner-bad     { border-left-color: var(--bad); }
.banner h3 { margin: 0 0 .2rem; font-size: 1.05rem; }
.banner p  { margin: 0; }
/* A banner that has to explain itself runs to more than one paragraph, and
   flush paragraphs read as one block nobody finishes. */
.banner p + p { margin-top: .6rem; }

/* A count, set to be read rather than parsed. Sized like the language chips
   further down the panelist page, because they are the same kind of object —
   a small tinted token holding one fact — and two sizes of the same idea on
   one page is one size too many. It was set much larger at first, with the
   figure in the display face and the label in capitals; that read as a
   dashboard tile, which is a heavier promise than a head count needs.

   The label carries the weight, not the number. The number is already the
   only numeral in the pill and needs no help being found; the bold word is
   what tells two counts apart at a glance. */
.figures { display: inline-flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.figure {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .2rem .75rem .2rem .25rem; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  font-size: .9rem; white-space: nowrap;
}
/* The count is cut out of the tint on its own disc, the same way a language
   chip carries how many people cover it. Two shapes for one idea would have
   been the odd thing; this is the same object at two sizes of importance.
   Cut out rather than tinted deeper, because at this size the coloured text
   needs the lighter ground behind it to stay legible. */
.figure-value {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.5rem; height: 1.5rem; padding: 0 .35rem;
  border-radius: 999px; background: var(--surface);
  font-weight: 600; font-variant-numeric: tabular-nums;
}
/* The label carries no weight of its own: each one marks the words that
   matter, the same way every other note on these screens does. "14
   panelists" wants the whole word; "42 people received their personal link"
   is a small sentence, and bolding all of it would only shout. */
.figure-label strong { font-weight: 600; }
/* Tinted apart from the figure beside it: two counts of different things,
   which must not read as one series to be added together. Green rather than
   amber, because amber already means "mind this" here and a count of
   interpreters is not a warning. */
.figure-apart { background: #E2F4EE; color: var(--good); }
/* Work still outstanding, in the palette's "mind this" amber. */
.figure-wait { background: #FAEED6; color: var(--warn); }
/* A number only there to give the one beside it its scale — and a nought,
   which is a fact worth printing but not worth colouring. */
.figure-quiet { background: var(--surface-sunk); color: var(--ink-soft); }

/* A question that labels a group of choices rather than a section of the
   page. Same treatment as a field label, since that is what it does. */
.choices-label { font-weight: 600; margin: 1.4rem 0 0; max-width: none; }
.choices-label + .choices { margin-top: .5rem; }

/* nowrap because a pill is a label, not a paragraph: "Has their own link"
   broken across two lines stops reading as one token and drags the row's
   height with it. If a pill is too wide for its column, the column is wrong. */
.pill {
  display: inline-block; padding: .15rem .6rem; border-radius: 999px;
  font-size: .85rem; font-weight: 600;
  background: var(--surface-sunk); color: var(--ink-soft);
  white-space: nowrap;
}
.pill-good { background: #E2F4EE; color: var(--good); }
.pill-bad  { background: #FBE1E6; color: var(--bad); }
.pill-wait { background: #FAEED6; color: var(--warn); }
/* The accent tint means "a thing Zoom owns", the same as .zoom-ref. A panelist
   is a role Zoom holds and this dashboard only reports, so it reads in that
   vocabulary rather than as a state this tool put the person in. */
.pill-zoom { background: var(--accent-soft); color: var(--accent); }

.progress { background: var(--surface-sunk); border-radius: 999px; height: .75rem; overflow: hidden; }
.progress-fill { background: var(--accent); height: 100%; width: 0; transition: width .3s ease; }
.progress-label { font-variant-numeric: tabular-nums; margin: .6rem 0 0; color: var(--ink-soft); }

dialog {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 2rem; max-width: 32rem; background: var(--surface); color: var(--ink);
}
dialog::backdrop { background: rgba(46, 42, 38, .45); }

/* A question asked without leaving the page. It borrows the markup of the
   standalone page that asks the same question with JavaScript off, so the
   cards inside it are real cards — flattened here, because a bordered box
   inside a bordered box inside a backdrop is three frames too many. */
.modal {
  width: min(44rem, calc(100vw - 2.5rem));
  max-width: none;
  padding: 1.75rem;
}
.modal h1 { font-size: 1.35rem; }
.modal .card {
  border: 0; padding: 0; margin: 0; background: none;
}
.modal .card + .card { margin-top: 1.6rem; }
.modal .card h2 { margin-top: 0; font-size: 1.05rem; }
.modal .lede { margin-bottom: 1.25rem; }
.modal .btn-row { margin-top: 1.6rem; }
.modal .choices { max-width: none; }
@keyframes modal-in {
  from { opacity: 0; transform: translateY(6px) scale(.99); }
  to   { opacity: 1; transform: none; }
}
.modal[open] { animation: modal-in .16s ease-out; }
@media (prefers-reduced-motion: reduce) { .modal[open] { animation: none; } }

.stack > * + * { margin-top: 1.25rem; }
.muted { color: var(--ink-soft); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .95rem; }
/* An identifier only meaningful inside Zoom, not a record this app keeps. A tinted
   chip rather than coloured text, which would read as ordinary body copy now. */
.zoom-ref {
  background: var(--accent-soft); color: var(--accent);
  padding: .05rem .35rem; border-radius: 4px;
}
/* Marks a note whose instruction is to go and do something in Zoom. */
.hint-zoom { border-left: 3px solid var(--accent); padding-left: .8rem; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* A <select> upgraded into something you can type into. The select itself is
   still there, holding the value; this is only what you see. */
.combo { position: relative; }
.combo-input { width: 100%; }
.combo-list {
  position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + .25rem);
  margin: 0; padding: .25rem; list-style: none; max-height: 15rem; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 6px 20px rgba(46, 42, 38, .08);
}
.combo-option {
  padding: .4rem .6rem; border-radius: 6px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.combo-option.is-active { background: var(--accent-soft); color: var(--accent); }
.combo-empty { padding: .4rem .6rem; color: var(--ink-soft); }

/* Drag-and-drop file upload. The native input sits inside, stretched to fill
   the whole zone and made invisible, so the entire box is one click target
   in every browser without any JS needed to route the click. */
/* Capped and left-aligned with the form fields above it. Full-bleed, it read
   as a banner rather than a target, and centred text inside a box that wide
   floats with nothing to hold it. */
/* The target fills the card and is deep enough to be aimed at rather than
   found. A drop target that stops two thirds of the way across reads as one
   more element in the column; at full width it reads as a place. */
.dropzone {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .5rem; min-height: 11rem;
  border: 2px dashed var(--line); border-radius: var(--radius);
  padding: 2rem 1.5rem; text-align: center; cursor: pointer;
  background: var(--surface-sunk);
  transition: border-color .15s ease, background .15s ease;
}
.dropzone:hover { border-color: var(--region, var(--accent)); }
/* Dashed to solid, and light to full colour: the change reads the same
   whichever region this drop zone belongs to. */
.dropzone.is-dragover {
  border-style: solid;
  border-color: var(--region, var(--accent));
}
.dropzone input[type=file] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}
.dropzone-hint { margin: 0; color: var(--ink-soft); max-width: 42ch; }
.dropzone-file { margin: .6rem 0 0; font-weight: 600; color: var(--ink); }
/* The one glyph on the zone, in the region's own colour: at this size it is
   what says "a file goes here" before any of the words are read. */
.dropzone .icon {
  width: 1.75rem; height: 1.75rem; flex: none;
  color: var(--region, var(--accent)); opacity: .75;
}

/* The sign-in page. It is the only page with no masthead above it, so it is
   the only one free to sit in the middle of the viewport rather than start
   under a header. These rules must follow `.page` — both are single-class
   selectors, so source order is what lets them override its width and padding.

   The ghost grid behind the card is the mark enlarged to page scale: icon.svg
   is a calendar, and a calendar is the thing this tool schedules against. Held
   at around 5% of the accent so it reads as a surface rather than as content,
   and masked away towards the edges so it never competes with the card. */
.page-gate {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  max-width: none;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 2.5rem 1.25rem;
}
.page-gate::before {
  content: '';
  position: absolute; inset: 0; pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(11, 92, 255, .07) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(11, 92, 255, .07) 1px, transparent 1px);
  background-size: 88px 88px;
  /* Centred, so a cell boundary always falls on the middle of the card and the
     grid stays symmetrical about it at every window width. */
  background-position: center;
  /* Reaches nothing well short of the corners. A grid running to the edges
     reads as wallpaper; one that dissolves reads as a halo the card sits in. */
  -webkit-mask-image: radial-gradient(ellipse 52% 62% at 50% 48%, #000 8%, transparent 82%);
  mask-image: radial-gradient(ellipse 52% 62% at 50% 48%, #000 8%, transparent 82%);
}

/* One entrance for the whole column rather than a stagger of four: the page
   has a single job, and arriving in one piece is the calmer way to say so. */
@keyframes gate-in {
  from { opacity: 0; transform: translateY(9px); }
  to   { opacity: 1; transform: none; }
}
.gate {
  position: relative; width: 100%; max-width: 25rem;
  animation: gate-in .42s cubic-bezier(.2, .7, .3, 1) both;
}
@media (prefers-reduced-motion: reduce) { .gate { animation: none; } }

.gate-mark { display: block; width: 52px; height: 52px; margin: 0 auto .75rem; }
.gate-wordmark { font-size: 1.35rem; letter-spacing: -.015em; text-align: center; margin: 0 0 1.5rem; }
.gate-note {
  max-width: none; margin: 1.15rem 0 0;
  text-align: center; font-size: .88rem; color: var(--ink-soft);
}

/* The one card in the application that is not in a list, and the only one that
   needs to sit *in* the grid behind it rather than on flat paper. The shadow is
   mixed from the accent rather than from black: a grey shadow on this ground
   turns the blue tint underneath muddy. Tight and short, so it reads as contact
   with the surface and not as a card hovering above the page. */
.page-gate .card {
  margin-bottom: 0;
  box-shadow: 0 20px 50px -28px rgba(11, 92, 255, .45);
}
.page-gate .lede { margin-bottom: 1.4rem; }
/* The card is already narrower than `.field`'s own cap, which would otherwise
   leave the controls short of the card's right edge. */
.page-gate .field { max-width: none; }
.page-gate .btn-row { margin-top: 1.35rem; }
.page-gate .btn { width: 100%; justify-content: center; padding-block: .55rem; }
