/* ===========================================================================
   Keystone
   Staff chrome uses the Hutchins palette. Public pages take their colours from
   the resolved brand via --brand-* custom properties, so adding a brand needs
   no stylesheet change.
   =========================================================================== */

:root {
  --ground:       #FAF9F8;
  --surface:      #FFFFFF;
  --sunk:         #F2F0EE;
  --ink:          #333333;
  --ink-strong:   #1F1F1F;
  --muted:        #695E57;   /* 5.54:1 on --sunk */
  --faint:        #776A61;   /* 4.61:1 on --sunk - was 3.59:1 */
  --rule:         #E4E1DE;
  --rule-strong:  #DDDDDD;

  --magenta:      #7E0039;
  --gold:         #F88D0D;
  --teal:         #00A3A5;

  --accent:       #7E0039;
  --accent-soft:  #F6E9EE;
  --on-brand:     #FFFFFF;   /* text on a --magenta fill */

  --ok:           #3D7061;
  --ok-soft:      #E6EDEA;
  --risk:         #A9483E;
  --risk-soft:    #F7E9E7;
  --warn:         #8A4B00;   /* --gold is 2.38:1 on white; this carries text */
  --warn-soft:    #FCF0E0;

  /* The focus ring. Named rather than reached for as a colour because it was gold on a
     button and the brand colour on a tab, a toggle and a date field, so the one mark a
     keyboard user follows changed colour as they moved across a form. Gold belongs to no
     state and to no brand, which is why it can be the ring without reading as a status. */
  --focus:        var(--gold);

  /* The ground behind a modal. Held as a colour rather than as an opacity so each theme
     can say how heavy it needs to be over its own ground. */
  --scrim:        rgb(20 14 17 / 55%);

  /* The browser draws the date/time picker glyph as a dark bitmap. On a light ground it
     is already legible; on a dark one it has to be inverted or it disappears. */
  --picker-tint:  none;

  --radius: 3px;

  /* ---- Scale ------------------------------------------------------------
     Every value in this file used to be chosen where it was written, which is
     why nothing quite lined up: an input 38px tall beside a select 32px tall
     beside a button somewhere between the two. These are the only sizes any
     component may reach for, so agreement is the default rather than
     something to be checked.

     Only colours are redefined for the public subtree, so this scale is
     shared by both sides of the site and the two feel like one product. */

  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  --text-xs:   .72rem;
  --text-sm:   .82rem;
  --text-base: .9rem;
  --text-md:   1rem;
  --text-lg:   1.15rem;
  --text-xl:   1.4rem;
  --text-2xl:  1.8rem;

  /* One monospace stack. Inline code and the template editor carried two different ones,
     which showed up as two letterforms for the same idea on the same page. */
  --font-mono: ui-monospace, Consolas, Menlo, monospace;

  --radius-lg:   6px;
  --radius-pill: 999px;

  /* Two elevations, because the interface has two: something floating just above the
     page, and something covering it. */
  --shadow-raised: 0 6px 20px rgb(0 0 0 / 12%);
  --shadow-modal:  0 16px 48px rgb(0 0 0 / 35%);

  /* One height for anything a person clicks or types into, so a row of mixed
     controls sits on a single line rather than stepping up and down. */
  --control-h:     2.25rem;
  --control-pad-x: .65rem;

  /* The smallest reliable touch target. A floor rather than a size: a control that is
     already taller keeps its height, and the ones that are not are raised to this on a
     phone. It was written as 44px in five places and as 2.75rem in a sixth, which is the
     same number twice over and two things to remember to change. */
  --touch-min:     2.75rem;

  /* Where a line of prose should wrap to stay readable. */
  --measure: 68ch;
}

/* Dark is opt-in, not automatic.
   Following the operating system meant the admin interface arrived dark for anyone whose
   machine is set that way, which is not what a school office expects to open. The palette
   is kept and applied only when the page asks for it by stamping data-theme="dark" on the
   root, which is also what a future toggle would set. */
:root[data-theme="dark"] {
    --ground:      #1A1618;
    --surface:     #221D20;
    --sunk:        #272124;
    --ink:         #EDE9E7;
    --ink-strong:  #FFFFFF;
    --muted:       #A79B95;
    --faint:       #958983;  /* 4.65:1 on --sunk - was 3.91:1 */
    --rule:        #342C30;
    --rule-strong: #443A3E;

    --magenta:     #D4699B;
    --gold:        #F9A63F;
    --teal:        #3CC2C4;

    --accent:      #D4699B;
    --accent-soft: #2E1A24;
    --on-brand:    #1A1618;  /* dark mode lightens --magenta, so a fill needs dark text */

    --ok:          #6FA694;
    --ok-soft:     #1B2724;
    --risk:        #D07A6E;
    --risk-soft:   #2C1E1C;
    --warn:        #F0B458;
    --warn-soft:   #2C2418;

    /* Heavier than the light theme's, because a 55% scrim over an already dark page
       barely separates the dialog from what is behind it. */
    --scrim:       rgb(0 0 0 / 65%);

    --picker-tint: invert(1) brightness(1.6);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: Arial, Helvetica, sans-serif;
  /* Off the type scale on purpose, and in pixels. This is the size of text that asks for
     nothing, not a size a component chooses; and because the scale is in rem, which is
     measured from the root and not from here, nothing on the scale moves when this does. */
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: "Archivo", Arial, sans-serif;
  color: var(--ink-strong);
  margin: 0;
  line-height: 1.2;
  text-wrap: balance;
}

h1 { font-size: var(--text-2xl); font-weight: 700; letter-spacing: -.015em; }
h2 { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-2); }
h3 { font-size: var(--text-md); font-weight: 600; margin: var(--space-5) 0 var(--space-2); }

a { color: var(--accent); }
p { margin: 0 0 var(--space-3); }
.muted { color: var(--muted); }
.small { font-size: var(--text-sm); }
.mono { font-family: var(--font-mono); font-size: .86em; }
.nowrap { white-space: nowrap; }
.right { text-align: right; }
.dim { opacity: .55; }

/* Padding and size in em rather than on the scale, because a code span sits inside a
   sentence and has to keep its proportions whatever size that sentence is set at. */
code {
  font-family: var(--font-mono);
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .08em .35em;
  font-size: .86em;
  overflow-wrap: anywhere;
}

/* ---------- Admin shell ---------- */

.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: 260px; flex: 0 0 260px;
  background: var(--surface);
  border-right: 1px solid var(--rule);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
}

.mark {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--rule);
}

.mark .glyph {
  width: 32px; height: 32px; flex: 0 0 32px;
  display: grid; place-items: center;
  background: var(--accent); color: var(--on-brand);
  font-family: "Archivo", Arial, sans-serif; font-weight: 700;
  border-radius: var(--radius);
}

.mark div { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.mark strong { font-size: var(--text-base); }
.mark span { font-size: var(--text-xs); color: var(--faint); }

/* The 2px gap is deliberately below the smallest step on the scale, and is the same
   hairline the role list uses: these are rows of one list, and a real gap between them
   would read as a column of separate cards. */
.sidebar nav { padding: var(--space-2); display: flex; flex-direction: column; gap: 2px; flex: 1; }

.sidebar nav a {
  padding: var(--space-2) var(--control-pad-x);
  border-radius: var(--radius);
  color: var(--muted);
  text-decoration: none;
  font-size: var(--text-base);
}

.sidebar nav a:hover { background: var(--sunk); color: var(--ink); }
.sidebar nav a.active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }

.who { padding: var(--space-3); border-top: 1px solid var(--rule); font-size: var(--text-sm); }

/* An email address is one long unbroken token, so it will not wrap on its own and
   spills past the sidebar. Break it anywhere rather than truncating: which account
   you are signed in as is worth two lines. */
.who .name {
  font-weight: 700; margin-bottom: var(--space-1);
  overflow-wrap: anywhere;
  line-height: 1.3;
}

.linkish {
  background: none; border: none; padding: 0;
  color: var(--muted); cursor: pointer;
  font: inherit; text-decoration: underline;
}
.linkish:hover { color: var(--accent); }

main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.content { padding: var(--space-5) var(--space-5) var(--space-7); max-width: 1280px; width: 100%; }

.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-5);
}
.page-head-text { min-width: 0; }
.page-head h1 { margin: 0; }

.page-head-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* The sentence under a page heading, at one size on both sides of the site.
   All three selectors are declared together on purpose. `.page-head p` is more specific
   than `.page-lead`, so declaring them apart means the element selector quietly wins and
   the migrated pages keep the old size while appearing to have been changed. Identical
   declarations make the specificity difference irrelevant, and the pages still carrying
   the old markup match the ones that do not. */
.page-head p,
.page-lead,
.lead {
  margin: var(--space-2) 0 0;
  max-width: var(--measure);
  font-size: var(--text-md);
  color: var(--muted);
}

/* ---------- Cards, notices ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
}
.card.flush { padding: 0; overflow: hidden; }
.muted-card { opacity: .75; }

/* One treatment, two placements.
   .notice is the markup already written across eighteen pages and .banner is the
   component replacing it. They are declared together so the two look identical
   while the migration happens, rather than the site carrying two different ideas
   of what a message looks like for as long as that takes. */
.notice,
.banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--faint);
  background: var(--surface);
  color: var(--ink);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  border-radius: var(--radius);
  font-size: var(--text-base);
}

/* The tone tints the ground and the edge; the words stay in body ink.
   Colouring a whole paragraph to match its border makes the longer messages
   harder to read for no gain, and the eye has already been told by the edge. */
.notice.info, .banner.info { border-left-color: var(--rule-strong); background: var(--sunk); }
.notice.ok,   .banner.ok   { border-left-color: var(--ok);          background: var(--ok-soft); }
.notice.warn, .banner.warn { border-left-color: var(--warn);        background: var(--warn-soft); }
.notice.risk, .banner.risk { border-left-color: var(--risk);        background: var(--risk-soft); }

.notice.ok > strong,   .banner.ok   .banner-body strong { color: var(--ok); }
.notice.warn > strong, .banner.warn .banner-body strong { color: var(--warn); }
.notice.risk > strong, .banner.risk .banner-body strong { color: var(--risk); }

/* Across the top of the shell rather than inside a page, for something that is
   true of the whole application and not of the page you happen to be on. */
.banner.full {
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  margin-bottom: 0;
  padding: var(--space-3) var(--space-5);
}

/* A full-width banner has no left edge left to carry its tone, so the rule under it
   takes the job. Written as three classes so it outranks .banner.full's own border. */
.banner.full.info { border-bottom-color: var(--rule-strong); }
.banner.full.ok   { border-bottom-color: var(--ok); }
.banner.full.warn { border-bottom-color: var(--warn); }
.banner.full.risk { border-bottom-color: var(--risk); }

/* ---------- Stats ---------- */

.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-3); margin-bottom: var(--space-4);
}
.stat {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: var(--space-3) var(--space-4);
  display: flex; flex-direction: column;
}
.stat .k { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--faint); }
.stat .v { font-family: "Archivo", Arial, sans-serif; font-size: var(--text-2xl); font-weight: 700; line-height: 1.15; }
.stat .h { font-size: var(--text-xs); color: var(--muted); }

/* ---------- Readiness checks ---------- */

.checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.checks li { display: flex; gap: var(--space-3); align-items: flex-start; font-size: var(--text-base); }
.checks .pip {
  width: 9px; height: 9px; border-radius: 50%; margin-top: var(--space-2); flex: 0 0 9px;
  background: var(--faint);
}
.checks li.done .pip { background: var(--ok); }
.checks li.todo .pip { background: var(--gold); }

/* The text beside the pip, which is now quoting other systems.
   A flex item will not shrink below the widest unbreakable thing inside it, and the
   dashboard puts third-party error text here: a URL, a certificate subject or a stack
   frame has no spaces to break at, so one of them widens the row, then the card, then
   scrolls the whole page sideways on a phone. The messages are already flattened and
   cut short before they arrive; this is what makes the row hold regardless. */
.checks li > div { min-width: 0; overflow-wrap: anywhere; }

/* ---------- Forms ---------- */

.field { margin-bottom: var(--space-4); }
label { display: block; font-size: var(--text-sm); font-weight: 700; color: var(--muted); margin-bottom: var(--space-1); }

input[type=text], input[type=email], input[type=password], input[type=number], input:not([type]), select, textarea {
  font: inherit; font-size: var(--text-base); width: 100%;
  min-height: var(--control-h);
  padding: var(--space-1) var(--control-pad-x);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
}

/* A textarea is sized by its rows, not by the control height, or a two-line note
   is stuck at the height of a single input. */
textarea { min-height: calc(var(--control-h) * 2); padding-block: var(--space-2); }

input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--focus); outline-offset: -1px; border-color: var(--accent);
}
input:disabled { opacity: .6; }

.help { font-size: var(--text-sm); color: var(--faint); margin-top: var(--space-1); max-width: var(--measure); }
.help.endpoint { margin-top: var(--space-3); }

.secret-row { display: flex; gap: var(--space-2); align-items: center; }
.secret-row input { flex: 1; }

.colour-row { display: flex; gap: var(--space-2); align-items: center; }

/* ---------- Toggle switches ---------- */
/* The checkbox stays in the markup and in the accessibility tree, so it is still what a
   screen reader announces and what the keyboard operates, and is drawn as a switch.
   Restyling here converts every .toggle in the application at once. */

.toggle {
  display: inline-flex; align-items: center; gap: var(--space-3);
  font-weight: 400; color: var(--ink);
  cursor: pointer; user-select: none;
  min-height: var(--touch-min);
}

.toggle input[type=checkbox] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 2.6rem; height: 1.5rem;
  margin: 0; padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule-strong);
  background: var(--sunk);
  position: relative;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}

/* The knob. Positioned rather than a second element, so the control stays one input. */
.toggle input[type=checkbox]::after {
  content: "";
  position: absolute;
  top: 50%; left: 2px;
  transform: translateY(-50%);
  width: 1.15rem; height: 1.15rem;
  border-radius: 50%;
  background: var(--surface);
  /* Stronger than the elevation tokens on purpose: this is the edge that separates the
     knob from the track it sits on, not a card floating above a page. */
  box-shadow: 0 1px 2px rgb(0 0 0 / 30%);
  transition: left .15s ease;
}

.toggle input[type=checkbox]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.toggle input[type=checkbox]:checked::after {
  left: calc(100% - 1.15rem - 2px);
  background: var(--on-brand);
}

.toggle input[type=checkbox]:disabled { opacity: .5; cursor: not-allowed; }
.toggle input[type=checkbox]:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .toggle input[type=checkbox], .toggle input[type=checkbox]::after { transition: none; }
}

/* A bare checkbox outside a .toggle, the conditions-of-hire tick, gets the brand colour
   without becoming a switch, because "I accept" is a tick, not a setting. */
input[type=checkbox]:not(.toggle input) { accent-color: var(--accent); width: auto; }

.stack { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }
.stack input { max-width: 380px; }

.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0 var(--space-4); }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0 var(--space-4); }

.search { max-width: 260px; }

/* ---------- Buttons ---------- */

.btn {
  font: inherit; font-size: var(--text-base); font-weight: 700;
  min-height: var(--control-h);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-1);
  text-decoration: none; white-space: nowrap;
}
.btn:hover:not(:disabled) { background: var(--sunk); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
/* Painted with --accent rather than with --magenta, which is the same colour on a staff
   page but the hirer's own on a public one. That is the whole reason the public subtree
   no longer needs a second copy of this rule. */
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-brand); }
.btn.primary:hover:not(:disabled) { filter: brightness(1.12); }
.btn.danger { color: var(--risk); border-color: var(--risk); }
.btn.tiny { min-height: 0; padding: var(--space-1) var(--space-2); font-size: var(--text-xs); }

button:focus-visible, a:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---------- Chips ---------- */

.chip {
  display: inline-block;
  font-size: var(--text-xs); font-weight: 700;
  /* In em rather than on the scale so the box stays in proportion to the label inside
     it wherever a chip is set at a different size. */
  padding: .1em .45em;
  border-radius: var(--radius);
  border: 1px solid var(--rule-strong);
  background: var(--sunk); color: var(--muted);
  margin-right: var(--space-1);

  /* A chip is one label with a border drawn round it. Let it wrap and the border
     breaks across two lines with a word stranded on each, which reads as damage
     rather than as a narrow column. "Senior School" was doing exactly that. */
  white-space: nowrap;

  /* Nowrap on its own is a promise the chip cannot keep. A directory group arrives
     under whatever name the tenant gave it, and something like
     "SG-Hutchins-Senior-School-Facilities-Approvers" is one unbreakable line that
     pushes its table wider than the phone reading it, so the whole page scrolls
     sideways to show one chip. Capped and clipped instead: the row keeps its width,
     and the components that render arbitrary names carry the full text in a title. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;

  /* Clipping moves an inline-block's baseline to its bottom edge, which would drop
     every chip below the text beside it. .chip.tiny already sat at middle for the
     same reason, so this makes the whole family agree rather than splitting it. */
  vertical-align: middle;
}
.chip.ok     { background: var(--ok-soft);     color: var(--ok);     border-color: var(--ok); }
.chip.risk   { background: var(--risk-soft);   color: var(--risk);   border-color: var(--risk); }
.chip.accent { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.chip.warn   { background: var(--warn-soft);   color: var(--warn);   border-color: var(--warn); }

/* ---------- Tables ---------- */

.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-base); }

th {
  text-align: left; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em;
  color: var(--muted); font-weight: 700;
  background: var(--sunk); padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--rule-strong); white-space: nowrap;
}
td { padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--rule); vertical-align: top; }
tbody tr:last-child td { border-bottom: none; }
/* The same idea inside a table, where the shape has to be one cell. Kept on the
   scale so a bare list and a bare table read as the same absence. */
.empty { text-align: center; padding: var(--space-6); color: var(--faint); }

/* ---------- Modal dialogs ------------------------------------------------

   These were missing entirely: every "dialog" was rendering as an ordinary
   block in the page flow rather than over it. */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--scrim);
  display: grid;
  place-items: center;
  padding: var(--space-5);
  overflow-y: auto;
}

.dialog {
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  width: min(680px, 100%);
  /* The overlay's own padding, top and bottom, so the dialog stops exactly where the
     scrim's margin begins rather than at a number that happens to match it today. */
  max-height: calc(100vh - var(--space-5) * 2);
  display: flex;
  flex-direction: column;
}

.dialog-head {
  padding: var(--space-4);
  border-bottom: 1px solid var(--rule);
}

.dialog-head h2 { margin-bottom: var(--space-1); }

.dialog-body { padding: var(--space-4); overflow-y: auto; flex: 1; }

.dialog-foot {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--rule);
  background: var(--sunk);
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* ---------- Roles ---------- */

.role-layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: var(--space-4); align-items: start; }

.role-list { display: flex; flex-direction: column; gap: 2px; }
.role-item {
  text-align: left; font: inherit; cursor: pointer;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: var(--space-2) var(--control-pad-x);
  display: flex; flex-direction: column; color: var(--ink);
}
.role-item:hover { background: var(--sunk); }
.role-item.on { border-color: var(--accent); background: var(--accent-soft); }

.perm-group { margin-bottom: var(--space-4); }
.perm-group-name {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em;
  color: var(--faint); font-weight: 700; margin-bottom: var(--space-1);
}
.perm {
  display: flex; gap: var(--space-2); align-items: flex-start;
  padding: var(--space-1) 0; font-weight: 400; color: var(--ink); margin: 0;
}
.perm input { width: auto; margin-top: var(--space-1); accent-color: var(--accent); }

/* ---------- Brand editor ---------- */

.brand-head { display: flex; gap: var(--space-3); align-items: center; margin-bottom: var(--space-4); }
.swatch {
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: var(--radius); border: 1px solid var(--rule-strong);
}
.swatch.small { width: 26px; height: 26px; flex: 0 0 26px; }

/* ---------- Public pages ---------- */

/* ---------------------------------------------------------------------------
   The public surface owns its whole palette.

   A brand's colours are stored values that do not change with the viewer's
   theme. Mixing them with the theme tokens produced a page that contradicted
   itself in dark mode: a light brand ground carrying white headings, and
   panels painted with the dark theme's surface colour holding dark brand ink.

   So every neutral is redefined here, derived from the brand's own ink and
   ground with color-mix. The public site therefore always renders in its
   brand's colours, in either theme. That is a deliberate choice for a branded
   site, and it works for any brand added later without touching this file.

   The corollary, and the thing to check when adding a colour: any token this
   subtree uses that is redefined for dark has to be pinned here too, or the
   dark value reaches in and lands on a light brand ground. That is what the
   block below is doing beyond the neutrals.

   The staff interface still follows the viewer's theme; only this subtree opts
   out. Layout tokens are shared, and none of them belong in here or in a theme
   block: the two sides of the site are meant to differ in colour alone.
   --------------------------------------------------------------------------- */
.public {
  --ground:      var(--brand-ground, #FAF9F8);
  --surface:     var(--brand-surface, #FFFFFF);
  --sunk:        color-mix(in srgb, var(--brand-ink, #333) 5%,  var(--brand-ground, #FAF9F8));

  --ink:         var(--brand-ink, #333333);
  --ink-strong:  color-mix(in srgb, var(--brand-ink, #333) 88%, #000000);
  --muted:       color-mix(in srgb, var(--brand-ink, #333) 78%, var(--brand-ground, #FAF9F8));
  --faint:       color-mix(in srgb, var(--brand-ink, #333) 64%, var(--brand-ground, #FAF9F8));

  --rule:        color-mix(in srgb, var(--brand-ink, #333) 14%, var(--brand-ground, #FAF9F8));
  --rule-strong: color-mix(in srgb, var(--brand-ink, #333) 26%, var(--brand-ground, #FAF9F8));

  --accent:      var(--brand-primary, #7E0039);
  --accent-soft: color-mix(in srgb, var(--brand-primary, #7E0039) 8%, var(--brand-surface, #FFF));

  /* Text on the brand fill. Without it the staff theme's value reaches in, and in dark
     mode that value is dark ink: the header band, the name in it and every primary button
     on the hire pages would be dark on dark. Both brand primaries are dark colours, so
     white is right here whichever theme the viewer is in. */
  --on-brand:  #FFFFFF;

  /* Semantic colours are fixed light-palette values: the ground beneath them
     here is always the brand's light ground, whatever the viewer's theme. */
  --ok:        #2F6B4F;
  --ok-soft:   #E6EDEA;
  --risk:      #9E3826;
  --risk-soft: #F7E9E7;
  --warn:      #8A4B00;
  --warn-soft: #FCF0E0;

  /* The ring, and the gold it is made from. A custom property substitutes its var() where
     it is declared, so re-stating --gold on its own would not reach a --focus computed up
     on the root: both have to be said here. The dark theme's lighter gold would sit at
     about two to one against a light brand ground. */
  --gold:      #F88D0D;
  --focus:     var(--gold);

  /* The browser's date and time glyph is drawn dark, and this subtree is light in either
     theme, so it must not pick up the inversion the dark staff pages need. */
  --picker-tint: none;

  min-height: 100vh; display: flex; flex-direction: column;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--brand-font-body, Arial, sans-serif);
}

.public h1, .public h2 { font-family: var(--brand-font-heading, "Archivo", Arial, sans-serif); }

.wrap { max-width: 860px; margin: 0 auto; width: 100%; padding: 0 var(--space-5); }

.public-head {
  background: var(--accent);
  color: var(--on-brand); padding: var(--space-5) 0;
}
.public-head .brand-name {
  font-family: var(--brand-font-heading, "Archivo", Arial, sans-serif);
  font-size: var(--text-xl); font-weight: 700; letter-spacing: -.01em;
}
.public-head .brand-tagline { font-size: var(--text-base); opacity: .85; }

.public-body { padding: var(--space-6) var(--space-5) var(--space-7); flex: 1; }

.public-foot {
  border-top: 1px solid var(--rule);
  padding: var(--space-4) 0; font-size: var(--text-sm); color: var(--muted);
}
.public-foot .wrap { display: flex; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }

.placeholder {
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin: var(--space-5) 0;
  background: var(--surface);
}

/* The 4rem is off the scale on purpose. This is a small card alone on an otherwise empty
   page, and it wants more air above it than any block that sits within a page does. */
.gate { max-width: 520px; margin: 4rem auto; padding: 0 var(--space-5); text-align: center; }
.gate .btn { margin-top: var(--space-4); }

/* ---------- Landing page steps ------------------------------------------ */

.steps { margin: var(--space-6) 0 var(--space-4); }

.steps ol {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
  counter-reset: step;
}

.steps li {
  counter-increment: step;
  border-top: 2px solid var(--accent);
  padding-top: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Numbered because these really are sequential: you cannot request a date
   before choosing a space. */
.steps li::before {
  content: counter(step);
  font-family: "Archivo", Arial, sans-serif;
  font-weight: 700;
  font-size: var(--text-xs);
  color: var(--accent);
}

.steps li strong { font-size: var(--text-md); }
.steps li span { color: var(--muted); font-size: var(--text-base); }

.empty-state {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-5);
  margin: var(--space-5) 0;
}

.empty-state h2 { margin-bottom: var(--space-2); font-size: var(--text-lg); }
.empty-state p { margin: 0; color: var(--muted); max-width: var(--measure); }
.empty-state-action { margin-top: var(--space-4); }

/* ---------- Responsive ---------------------------------------------------

   Three things matter on a phone and none of them are handled by simply
   letting the layout reflow:

   1. The admin sidebar becomes a horizontal strip, not a 260px column that
      eats most of the screen.
   2. Wide data tables become stacked cards. A table with eight columns cannot
      be made legible at 375px by scrolling alone: you lose the header, so
      every cell needs its own label.
   3. Controls reach --touch-min so they can actually be tapped.
   ------------------------------------------------------------------------- */

@media (max-width: 1024px) {
  .content { padding: var(--space-4) var(--space-4) var(--space-7); }

  /* The application-wide banner runs across the top of the same column the content sits
     in, so its side padding has to follow the content's down to here. It used to be
     written as a plain .banner rule further down, which never applied: .banner.full is
     two classes and outranks it, so the banner kept its desktop inset and every ordinary
     in-page banner got the wider padding instead. */
  .banner.full { padding: var(--space-3) var(--space-4); }

  .role-layout { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 900px) {
  .shell { flex-direction: column; }

  .sidebar {
    width: 100%; flex: none; height: auto;
    position: sticky; top: 0; z-index: 20;
    flex-direction: row; align-items: center;
    border-right: none; border-bottom: 1px solid var(--rule);
  }

  .mark { border-bottom: none; border-right: 1px solid var(--rule); flex: 0 0 auto; }
  .mark div { display: none; }

  /* One rule rather than the three this had grown into. The strip scrolls sideways and
     takes the leftover width, the account block beside it must not be scrolled away, and
     the strip's own scrollbar is hidden because a bar under the links reads as a stray
     horizontal scroll of the page itself. */
  .sidebar nav {
    flex-direction: row; flex: 1; min-width: 0;
    overflow-x: auto; gap: var(--space-1);
    -webkit-overflow-scrolling: touch;   /* momentum scrolling */
    scrollbar-width: none; -ms-overflow-style: none;
  }
  .sidebar nav::-webkit-scrollbar { width: 0; height: 0; display: none; }

  .sidebar nav a { white-space: nowrap; }

  /* The account block was hidden here, which also hid the only way to sign out of the
     admin interface on a phone. Kept, reduced to the sign-out control: the address is
     what does not fit, not the button. */
  .who {
    flex: 0 0 auto;
    padding: var(--space-2) var(--space-3);
    border-top: none;
    border-left: 1px solid var(--rule);
  }
  .who .name { display: none; }

  .page-head { flex-direction: column; align-items: stretch; gap: var(--space-3); }
  .page-head .btn, .page-head select, .page-head input { width: 100%; }
}

@media (max-width: 640px) {
  body { font-size: 16px; }          /* iOS zooms any input below 16px on focus */

  h1 { font-size: var(--text-xl); }
  .lead { font-size: var(--text-md); }

  /* .content and .card are not repeated here. Once both are on the scale the values they
     were given at this width are the values they already have, so a rule saying so again
     is only somewhere for the two to drift apart. */
  .wrap { padding: 0 var(--space-4); }
  .public-body { padding: var(--space-5) var(--space-4) var(--space-6); }

  .stats { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
  .stat .v { font-size: var(--text-xl); }

  .grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
  .venue-grid { grid-template-columns: minmax(0, 1fr); }
  .steps ol { grid-template-columns: minmax(0, 1fr); }

  /* Tappable. --touch-min is the smallest reliable touch target; a tiny button keeps the
     ordinary control height, which is still a fair target for a secondary action. */
  .btn, button, select { min-height: var(--touch-min); }
  .btn.tiny { min-height: var(--control-h); }

  /* The attribute selectors are repeated deliberately: the base rule uses
     input[type=text] and friends, which outranks a bare `input` selector even
     inside this media query. Anything under 16px makes iOS zoom on focus. */
  input[type=text], input[type=email], input[type=password], input[type=number],
  input[type=date], input[type=time], input:not([type]), select, textarea {
    font-size: 16px;
    padding: var(--space-2) var(--control-pad-x);
    min-height: var(--touch-min);
  }

  /* Scoped away from .toggle: a switch has its own dimensions, and this rule only wins
     the cascade by accident of specificity. */
  input[type=checkbox]:not(.toggle input) { min-height: 0; width: 20px; height: 20px; }

  /* Raised from 40px to the same floor as everything else: a nav link is the one control
     on this strip somebody is most likely to be aiming at while walking. */
  .sidebar nav a { min-height: var(--touch-min); display: flex; align-items: center; }

  .secret-row { flex-wrap: wrap; }
  .secret-row input { flex: 1 1 100%; }
  .secret-row .btn { flex: 1 1 auto; }

  .stack input { max-width: none; }

  /* Dialogs fill the screen rather than floating in a cramped box. */
  .overlay { padding: 0; align-items: stretch; }
  .dialog {
    width: 100% !important;
    max-height: 100vh; height: 100%;
    border-radius: 0; border: none;
    display: flex; flex-direction: column;
  }
  .dialog-body { flex: 1; overflow-y: auto; }
  .dialog-foot { flex-wrap: wrap; gap: var(--space-2); }
  .dialog-foot .btn { flex: 1 1 auto; }

  .checks li { font-size: var(--text-base); }
  .public-foot .wrap { flex-direction: column; gap: var(--space-2); }

  /* --- Tables become cards ------------------------------------------------
     Each row turns into a labelled block. The label comes from data-label on
     the cell, so the header text is not lost when the header row is hidden. */
  .table-wrap { overflow-x: visible; }

  table, thead, tbody, tr, th, td { display: block; }

  thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

  tbody tr {
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    margin: var(--space-2) var(--space-3);
    padding: var(--space-1) 0;
    background: var(--surface);
  }

  tbody tr:hover { background: var(--surface); }

  tbody td {
    border: none;
    display: flex;
    gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-1) var(--space-3);
    text-align: left;
  }

  tbody td::before {
    content: attr(data-label);
    flex: 0 0 38%;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--faint);
    font-weight: 700;
  }

  /* Cells with no label, and the actions cell, take the full width. */
  tbody td:not([data-label])::before { content: none; }
  tbody td.right { justify-content: flex-start; }
  tbody td.empty { display: block; text-align: center; }
  tbody td.empty::before { content: none; }

  td.nowrap { white-space: normal; }

  /* --- Except the cost breakdown -----------------------------------------
     The card treatment above is for a grid of records, where a row is a thing
     and its columns need naming once the header is hidden. A quote is not that.
     It is a short list of amounts that has to add up in front of somebody, and
     the rules above were pulling it apart: every line became its own bordered
     card, so a three line quote read as three unrelated boxes, and the amount
     lost its right alignment, so the figures no longer sat in a column and the
     total no longer lined up with what it was totalling. A hire quote that does
     not visibly add up is the one thing on this page a hirer will query.

     Two short columns fit a phone perfectly well, so it simply stays a table. */
  .quote-lines, .quote-lines tr, .quote-lines td { display: revert; }

  .quote-lines tr {
    border: none; border-radius: 0; margin: 0; padding: 0; background: none;
  }

  .quote-lines td { padding: var(--space-1) 0; text-align: left; }
  .quote-lines td.amount { text-align: right; }
  .quote-lines tr.sum td { border-top: 1px solid var(--rule); padding-top: var(--space-2); }
}

@media (max-width: 380px) {
  .stats { grid-template-columns: minmax(0, 1fr); }
  tbody td { flex-direction: column; gap: var(--space-1); }
  tbody td::before { flex: none; }
}

@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }

/* ---------- Venue listings ---------------------------------------------- */

.venue-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: var(--space-4);
    margin: var(--space-5) 0 var(--space-6);
}

.venue-card {
    display: block;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--surface);
    text-decoration: none;
    color: inherit;
    overflow: hidden;
    /* --accent rather than the brand variable and its fallback written out: inside the
       public subtree the accent is already the hirer's colour, and on a staff page it is
       the school's, which is what this rule wanted in each place anyway. */
    border-top: 3px solid var(--accent);
}

.venue-card:hover { border-color: var(--accent); background: var(--sunk); }
.venue-body { padding: var(--space-4); }
.venue-body h2 { font-size: var(--text-md); margin-bottom: var(--space-1); }
.venue-body p { font-size: var(--text-base); margin: 0 0 var(--space-2); }

.venue-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-2) 0 var(--space-4); }

/* ---------- Opening hours ----------------------------------------------- */

.hours { list-style: none; margin: var(--space-2) 0 var(--space-5); padding: 0; max-width: 320px; }
.hours li {
    display: flex; justify-content: space-between;
    padding: var(--space-1) 0; border-bottom: 1px solid var(--rule); font-size: var(--text-base);
}
.hours li:last-child { border-bottom: none; }
.hours .day { font-weight: 700; }

/* ---------- Availability preview ---------------------------------------- */

.preview {
    border: 1px solid var(--rule);
    border-left: 3px solid var(--accent);
    background: var(--sunk);
    border-radius: var(--radius);
    padding: var(--space-3) var(--space-4);
    margin: var(--space-4) 0;
}

.preview-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }

.preview-list li {
    display: flex; gap: var(--space-3); align-items: baseline;
    font-size: var(--text-base); padding: var(--space-1) 0;
    border-left: 3px solid transparent; padding-left: var(--space-2);
}

.preview-list li.free  { border-left-color: var(--ok); }
.preview-list li.clash { border-left-color: var(--risk); }
.preview-list li.free > span:first-child  { font-weight: 700; }
.preview-list li.clash > span:first-child { font-weight: 700; color: var(--risk); }


/* ---------- Key/value details ---------- */
/* Two columns where there is room, stacked where there is not, so a term never
   collapses onto a sliver of width beside a long value. */

.detail-list {
  display: grid; grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--space-2) var(--space-4); margin: 0 0 var(--space-4);
}
.detail-list dt { font-size: var(--text-sm); font-weight: 700; color: var(--muted); }
.detail-list dd { margin: 0; font-size: var(--text-base); overflow-wrap: anywhere; }

/* A heading and its explanation inside a card whose own padding was removed so a
   table can meet its edges. The same inset as .card-head further down, which is the
   same block with an action alongside the heading: seen on neighbouring pages, the two
   should start their text at the same place. */
.card-intro { padding: var(--space-4) var(--space-4) var(--space-2); }
.card-intro h2 { margin-top: 0; }

.chip-row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

@media (max-width: 640px) {
  .detail-list { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
  .detail-list dd { margin-bottom: var(--space-2); }
}

/* ---------- Setup steps ---------- */
/* A different shape from the landing page's .steps, which lays three cards across.
   Numbered because the content genuinely is a sequence: the API key is useless before
   the domain is authenticated, and the test is useless before both. */

.setup-steps {
  /* The marker's diameter, named because the text indent is measured from it. Off the
     spacing scale on purpose: it is the size of a numbered disc, not a gap. */
  --marker: 1.6rem;

  counter-reset: step;
  list-style: none;
  margin: var(--space-4) 0;
  padding: 0;
  display: flex; flex-direction: column; gap: var(--space-3);
}
/* The marker is positioned rather than laid out as a column: making the li a grid or
   flex container turns every text node and inline link inside it into a separate item,
   which wraps the sentence one word per line. */
.setup-steps li {
  counter-increment: step;
  position: relative;
  padding-left: calc(var(--marker) + var(--space-3));
  min-height: var(--marker);
  font-size: var(--text-base); max-width: var(--measure);
}
.setup-steps li::before {
  content: counter(step);
  position: absolute; left: 0; top: 0;
  width: var(--marker); height: var(--marker); border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid var(--accent);
  font-size: var(--text-xs); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* A bordered block inside a card, for an action that belongs to the section but is not
   one of its settings. */
.subcard {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sunk);
  padding: var(--space-4);
  margin-top: var(--space-4);
}
.subcard h3 { margin: 0 0 var(--space-1); font-size: var(--text-md); }
.subcard .notice { margin-bottom: 0; }


/* ---------- Tabs ---------- */
/* Scrolls sideways on a narrow screen rather than wrapping into a second row that
   pushes the panel down the page. */

.tabs {
  display: flex; gap: var(--space-1);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--rule);

  /* Still scrolls when the tabs outrun the width, but never draws a scrollbar:
     a bar sitting under the tab labels reads as a stray horizontal scroll of the
     page itself. The fade at the right edge is what says there is more. */
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.tabs::-webkit-scrollbar { width: 0; height: 0; display: none; }
.tabs button {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: var(--space-2);
  background: none; border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: var(--space-2) var(--space-3);
  min-height: var(--touch-min);
  font: inherit; font-size: var(--text-base); font-weight: 600;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
}
.tabs button:hover { color: var(--ink-strong); }
.tabs button.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.tabs button:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

.tab-count {
  display: grid; place-items: center;
  min-width: 1.15rem; height: 1.15rem; padding: 0 var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--warn-soft); color: var(--warn);
  border: 1px solid var(--warn);
  font-size: var(--text-xs); font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Kept in the accessibility tree and out of the page: the panel needs a name for
   screen readers, but the tab above it already says the same word on screen. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}


/* ---------- Public account bar ---------- */
/* Sits on the brand-coloured band, so its controls are outlined in the header's own
   text colour rather than the neutral palette, which would be invisible there. */

.public-head-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
}

.public-account {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
}

/* Only what a ghost button adds to a button: the height and the inset are already the
   ones every other control has, and repeating them here was two more places for the row
   of controls in the header to fall out of line with the rest of the site. */
.public-account .btn.ghost {
  background: transparent;
  border: 1px solid currentColor;
  color: inherit;
  font-size: var(--text-sm);
  text-decoration: none;
}
/* Mixed from the header's own text colour rather than from white, so the hover is
   visible whatever colour a brand puts its header text in. */
.public-account .btn.ghost:hover { background: color-mix(in srgb, currentColor 16%, transparent); }

.public-account .linkish { color: inherit; opacity: .85; }
.public-account .linkish:hover { color: inherit; opacity: 1; }
.public-account form { margin: 0; }

@media (max-width: 640px) {
  /* Account controls move under the brand name rather than squeezing beside it. */
  .public-head-row { flex-direction: column; align-items: stretch; }
  .public-account { justify-content: flex-start; }
}


/* ---------- A hirer's own bookings ---------- */

.booking-groups { display: flex; flex-direction: column; gap: var(--space-6); }
.booking-groups h2 { margin: 0 0 var(--space-1); }
.booking-groups > div > .muted { margin: 0 0 var(--space-3); }

.booking-card {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-4);
  margin-bottom: var(--space-3);
}
.booking-card header {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: var(--space-4); flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.booking-card h3 { margin: 0; font-size: var(--text-md); }
.booking-card .detail-list { margin-bottom: 0; }

.date-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.date-list li { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* A date that has already happened is still worth showing, since it is the record of
   what was booked, but it should not read as something coming up. */
.date-list li.past { opacity: .6; }


/* ---------- Opening hours, as edited ----------
   The list a hirer reads is .hours further up. This is the same information as a form. */

.hours-table { width: 100%; margin: var(--space-4) 0 var(--space-2); }
.hours-table th { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--faint); }
.hours-table td { padding: var(--space-1) var(--space-2) var(--space-1) 0; vertical-align: middle; }
.hours-table input[type=time] { width: auto; min-width: 8rem; }

/* A closed day keeps its times visible but out of the way, so turning it back on does
   not mean re-entering them. */
.hours-table tr.dim input[type=time] { opacity: .45; }

@media (max-width: 900px) {
  /* The four columns become a labelled block per day rather than a squeezed grid. */
  .hours-table thead { display: none; }
  .hours-table, .hours-table tbody, .hours-table tr, .hours-table td { display: block; width: 100%; }
  .hours-table tr {
    border: 1px solid var(--rule); border-radius: var(--radius);
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);
  }
  .hours-table td { padding: var(--space-1) 0; }
  .hours-table td[data-label="From"]::before,
  .hours-table td[data-label="Until"]::before {
    content: attr(data-label);
    display: inline-block; min-width: 4rem;
    font-size: var(--text-xs); color: var(--muted);
  }
  .hours-table input[type=time] { width: auto; }
}


/* ---------- Conditions of hire and attachments ---------- */

/* Terms are stored as plain text and shown as written, so the wrapping has to be forced.
   A <pre> would otherwise send a long clause off the side of the page. */
.terms-text {
  margin: var(--space-2) 0 0;
  padding: var(--space-3) var(--space-4);
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font: inherit; font-size: var(--text-base); line-height: 1.55;
  white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 24rem; overflow-y: auto;
}

.terms-box, .version { margin: var(--space-2) 0; }
.terms-box > summary, .version > summary {
  cursor: pointer; padding: var(--space-2) 0;
  font-weight: 600; font-size: var(--text-base);
}
.terms-box > summary:hover, .version > summary:hover { color: var(--accent); }
.version > summary { display: flex; gap: var(--space-3); align-items: baseline; flex-wrap: wrap; }

.doc-list { list-style: none; margin: var(--space-1) 0 var(--space-2); padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.doc-list li { display: flex; gap: var(--space-3); align-items: baseline; flex-wrap: wrap; }

.upload-row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; margin-top: var(--space-2); }
.upload-row select { width: auto; }

@media (max-width: 640px) {
  .upload-row { flex-direction: column; align-items: stretch; }
  .upload-row select { width: 100%; }
}


/* ---------- Rates ---------- */

.rate-block {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4) 0;
  margin-bottom: var(--space-4);
  background: var(--sunk);
}
.rate-block h3 { margin: 0 0 var(--space-2); font-size: var(--text-base); }

/* ---------- Money ---------- */

.money { font-variant-numeric: tabular-nums; }
.money.total { font-weight: 700; }

.quote-lines { width: 100%; margin: var(--space-2) 0; }
.quote-lines td { padding: var(--space-1) 0; font-size: var(--text-base); vertical-align: top; }
.quote-lines td.amount { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.quote-lines tr.sum td { border-top: 1px solid var(--rule); padding-top: var(--space-2); font-weight: 700; }

.payment-row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; margin-top: var(--space-2); }
.payment-row .btn { text-decoration: none; }


/* ---------- Date and time controls ---------- */
/* Browsers style these natively and inconsistently. The parts that can be reached are
   the field itself and the picker indicator, which is enough to make them sit in the
   brand rather than looking like something the operating system dropped in. */

/* Height, inset and text size taken from the same tokens as every other field. These had
   been given 44px and their own padding, so a date sat eight pixels taller than the text
   box next to it and set its text a size larger: the exact stepping the control tokens
   were introduced to end. The touch floor still applies to them on a phone, from the
   responsive block, where it applies to every control at once. */
input[type=date],
input[type=time],
input[type=datetime-local] {
  font: inherit;
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  padding: var(--space-1) var(--control-pad-x);
  min-height: var(--control-h);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  color-scheme: light dark;   /* the native picker follows the page, not the OS */
}

input[type=date]:hover,
input[type=time]:hover,
input[type=datetime-local]:hover { border-color: var(--accent); }

input[type=date]:focus-visible,
input[type=time]:focus-visible,
input[type=datetime-local]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--accent);
}

/* The calendar and clock button. Tinted to the brand, and given a real target size: the
   default is a few pixels wide and genuinely hard to hit on a phone. */
input[type=date]::-webkit-calendar-picker-indicator,
input[type=time]::-webkit-calendar-picker-indicator,
input[type=datetime-local]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  padding: var(--space-1);
  margin-left: var(--space-1);
  border-radius: var(--radius);
  filter: var(--picker-tint);
}
input[type=date]::-webkit-calendar-picker-indicator:hover,
input[type=time]::-webkit-calendar-picker-indicator:hover,
input[type=datetime-local]::-webkit-calendar-picker-indicator:hover {
  background: var(--accent-soft);
}

/* A field the availability check has rejected, so the problem is on the control itself
   rather than only in a message further down the form. */
input.invalid,
input[type=date].invalid,
input[type=time].invalid {
  border-color: var(--risk);
  background: var(--risk-soft);
}
input.checking { border-color: var(--warn); }

/* The way out of the admin interface. Set apart from the section links because it leaves
   the admin area rather than moving within it. */
.sidebar nav .home-link {
  display: block;
  /* The same inset as the links below it. They sit in one column, so a couple of pixels
     of difference shows as text that does not start in the same place. */
  padding: var(--space-2) var(--control-pad-x);
  margin-bottom: var(--space-1);
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  color: var(--muted);
  text-decoration: none;
  font-size: var(--text-base); font-weight: 600;
}
.sidebar nav .home-link:hover { color: var(--accent); border-color: var(--accent); }

a.brand-name { text-decoration: none; color: inherit; display: inline-block; }
a.brand-name:hover { opacity: .9; }


/* ---------- Signed-in home ---------- */

.notices { display: flex; flex-direction: column; gap: var(--space-3); margin-bottom: var(--space-5); }

.upcoming { list-style: none; margin: var(--space-2) 0 var(--space-4); padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.upcoming li {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr) auto;
  gap: var(--space-3); align-items: center;
  padding: var(--space-2) var(--space-1);
  border-bottom: 1px solid var(--rule);
}
.upcoming li:last-child { border-bottom: none; }
.upcoming .when, .upcoming .what { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.upcoming .what strong { overflow-wrap: anywhere; }

@media (max-width: 640px) {
  /* Three columns at 375px gives each about a hundred pixels, which is not enough for a
     date and a venue name. Stacked, with the status pulled up beside the date. */
  .upcoming li { grid-template-columns: minmax(0, 1fr) auto; }
  .upcoming .what { grid-column: 1 / -1; }
}


/* ---------- Chosen dates ---------- */

.date-rows { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-3); }

.date-row {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 7rem) auto minmax(0, 7rem) auto minmax(0, 1fr);
  gap: var(--space-2); align-items: center;
  padding: var(--space-2) var(--control-pad-x);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--rule);
  border-radius: var(--radius);
}

/* The verdict is carried on the row's own edge, so a long list can be scanned for the
   one date that is a problem without reading every message. */
.date-row.free  { border-left-color: var(--ok); }
.date-row.clash { border-left-color: var(--risk); background: var(--risk-soft); }

.date-row .dash { color: var(--faint); }
.date-row .remove { justify-self: start; }
.date-row .verdict { justify-self: end; }

@media (max-width: 760px) {
  /* Date on its own line, then the two times side by side, then the verdict. Six columns
     at phone width gives each about fifty pixels. */
  .date-row {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    gap: var(--space-2);
  }
  .date-row > input[type=date] { grid-column: 1 / -1; }
  .date-row .remove { grid-column: 1; justify-self: start; }
  .date-row .verdict { grid-column: 2 / -1; justify-self: end; }
}


/* ---------- Role grants ---------- */

.grant-table { width: 100%; margin: var(--space-2) 0 var(--space-4); }
.grant-table th {
  text-align: left; font-size: var(--text-xs); text-transform: uppercase;
  letter-spacing: .08em; color: var(--faint); padding-bottom: var(--space-1);
}
.grant-table td { padding: var(--space-2) var(--space-2) var(--space-2) 0; border-top: 1px solid var(--rule); vertical-align: middle; }


/* ---------- Calendar ---------- */

.calendar-controls {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-3);
}
.cal-nav { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.month-nav { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.month-nav .month-name { min-width: 11rem; text-align: center; font-size: var(--text-md); }
.field.inline { margin: 0; display: flex; align-items: center; gap: var(--space-2); }
.field.inline label { margin: 0; }
.field.inline select { width: auto; }

/* Month or week are alternatives, so they are joined into one control. Two loose buttons
   would read as two things that might be switched on together. */
.view-switch { display: inline-flex; }
.view-switch .btn { border-radius: 0; position: relative; }
.view-switch .btn:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.view-switch .btn:last-child { border-radius: 0 var(--radius) var(--radius) 0; margin-left: -1px; }
.view-switch .btn.on {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
  z-index: 1;
}

.legend { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; margin-bottom: var(--space-4); }

/* Two words that split the row into the part you can switch and the part you cannot. */
.legend .legend-label {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em;
  color: var(--faint);
}

.legend .key {
  font: inherit; font-size: var(--text-xs); padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill); border: 1px solid;
  display: inline-flex; align-items: center;
}
button.key { cursor: pointer; }
button.key:hover { filter: brightness(.95); }
/* Inline-block so the labels line up whichever mark is showing, and so the struck-out
   state below draws its line through the word without crossing the cross. */
.legend .key .mark {
  display: inline-block; width: .85rem; margin-right: var(--space-1);
  font-weight: 700; text-align: center;
}

/* Switched off. The fill is what carries "this kind is on show", so taking it away, and
   with it the colour and the tick, says the opposite three times over. The struck-out
   label is the fourth, for anyone reading a printout in grey. */
.legend .key.off {
  background: transparent;
  color: var(--muted);
  border-color: var(--rule-strong);
  text-decoration: line-through;
}

/* A sample of a mark rather than a switch, so it does not offer a pointer. */
.legend .key.sample { cursor: default; }

/* Colour carries the status, and a second cue carries it too, a border weight or a
   dashed edge, because roughly one man in twelve cannot separate these hues. */
.legend .confirmed, .entry.confirmed { background: var(--ok-soft);     color: var(--ok);     border-color: var(--ok); }
.legend .pending,   .entry.pending   { background: var(--warn-soft);   color: var(--warn);   border-color: var(--warn); }
.legend .declined,  .entry.declined  { background: var(--sunk);        color: var(--faint);  border-color: var(--rule); text-decoration: line-through; }
.legend .timetable, .entry.timetable { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); border-style: dashed; }
.legend .closure,   .entry.closure   { background: var(--risk-soft);   color: var(--risk);   border-color: var(--risk); }

/* A door standing unlocked is a condition the day is in rather than something booked, so
   it is drawn as a bar with no fill: present, readable, and not competing with the
   bookings it sits behind. The dotted edge is the second cue, since timetable already has
   the dashed one. */
.legend .doorschedule, .entry.doorschedule {
  background: transparent; color: var(--muted); border-color: var(--muted); border-style: dotted;
}
.legend .clash { background: transparent; color: var(--risk); border-color: var(--risk); border-width: 2px; }

.calendar {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
}

.calendar-head {
  padding: var(--space-2);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .08em;
  color: var(--faint); background: var(--sunk);
  border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
}

.calendar-day {
  min-height: 7.5rem;
  padding: var(--space-1);
  border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  display: flex; flex-direction: column; gap: var(--space-1);
}
.calendar-day.outside { background: var(--sunk); }
.calendar-day.outside .day-number { color: var(--faint); }
.calendar-day.today { box-shadow: inset 0 0 0 2px var(--accent); }

/* Seven cells instead of forty-two, so a day gets the depth to show everything on it and
   nothing has to be folded away behind a count. */
.calendar.week .calendar-day { min-height: 16rem; }

.day-number { font-size: var(--text-xs); font-weight: 700; color: var(--muted); }

.entry {
  display: flex; gap: var(--space-2); align-items: baseline;
  width: 100%; text-align: left;
  padding: var(--space-1);
  border: 1px solid; border-radius: var(--radius);
  font: inherit; font-size: var(--text-xs); line-height: 1.25;
  cursor: pointer;
  overflow: hidden;
}
.entry .at { font-variant-numeric: tabular-nums; flex: 0 0 auto; opacity: .85; }
.entry .what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A clash is the thing an administrator most needs to see, so it gets a weight of its
   own on top of whatever the entry already is. */
.entry.clash { border-width: 2px; border-color: var(--risk); font-weight: 700; }

/* What the count folds away is the rest of a busy day, and a busy day is exactly the one
   worth opening, so this is a real button rather than a note about one. It takes the same
   inset as an entry, since it sits directly under a stack of them and a few pixels of
   difference there reads as a misalignment rather than as a different kind of thing. */
.more {
  align-self: flex-start;
  background: none; border: none;
  padding: var(--space-1);
  font: inherit; font-size: var(--text-xs); color: var(--muted);
  text-decoration: underline; cursor: pointer;
}
.more:hover { color: var(--accent); }

@media (max-width: 900px) {
  /* A seven-column month grid at phone width gives each day about fifty pixels, which
     fits a number and nothing else. Below this it becomes a list of days that have
     something in them. */
  .calendar { grid-template-columns: minmax(0, 1fr); border-left: none; }
  .calendar-head { display: none; }
  .calendar-day { min-height: 0; flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  .calendar-day.outside { display: none; }
  .day-number { flex: 0 0 3rem; font-size: var(--text-sm); }
  .entry { width: auto; flex: 1 1 12rem; }
  .more { flex: 0 0 auto; }

  /* A month of forty-two rows is only readable once the empty ones are dropped. A week is
     seven, and there a quiet Tuesday is worth seeing: it is the answer to "when is the hall
     free", which is the question a week view gets opened for. */
  .calendar:not(.week) .calendar-day:not(:has(.entry)) { display: none; }
  .calendar.week .calendar-day { min-height: 0; }
  .calendar.week .day-number { flex: 0 0 4.5rem; }

  /* The range name is the longest thing in the row, and a week's is longer than a month's.
     Given its own line it can grow without pushing the arrows off the screen. */
  .month-nav .month-name { min-width: 0; flex: 1 1 100%; order: -1; text-align: left; }
}

/* One toggle per space when editing a group's membership. */
.member-list { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-1); }

/* Only what a tiny chip adds. Its size and its padding were being restated here in values
   that worked out within half a pixel of the ones .chip already sets, which is two rules
   to keep in step for no difference anybody could see. What it really is, is a chip that
   trails a word rather than starting a row. */
.chip.tiny { margin-left: var(--space-1); vertical-align: middle; }

/* --- Brand images -------------------------------------------------------- */

/* The mark is the way home, so it keeps a generous hit area on a phone whether
   it is rendering as a logo or as the fallback name. */
.brand-mark { display: inline-flex; align-items: center; min-height: var(--touch-min); text-decoration: none; }
.brand-logo { max-height: 2.4rem; max-width: min(15rem, 60vw); width: auto; height: auto; display: block; }

.asset-row { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); margin-top: var(--space-2); }

.asset { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-3);
         border: 1px solid var(--rule); border-radius: var(--radius); }

/* Checkerboard behind the preview: a white logo on a white card is invisible, and
   the first anyone would know is an empty header on the live site. The colours here are
   fixed rather than tokens on purpose: this is a swatch showing what the file itself
   looks like, so it must not move with the theme the page happens to be in. */
.asset-preview {
    flex: 0 0 5.5rem; height: 4rem; display: grid; place-items: center;
    border-radius: var(--radius); overflow: hidden; padding: var(--space-1);
    background-color: #fff;
    background-image:
        linear-gradient(45deg, rgba(0,0,0,.06) 25%, transparent 25%, transparent 75%, rgba(0,0,0,.06) 75%),
        linear-gradient(45deg, rgba(0,0,0,.06) 25%, transparent 25%, transparent 75%, rgba(0,0,0,.06) 75%);
    background-size: 12px 12px;
    background-position: 0 0, 6px 6px;
}

/* The reversed mark is meant for dark ground, so it is previewed on one. */
.asset-preview.inverse { background-color: #2a2a2a; }

.asset-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }

.asset-detail { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.asset-detail input[type="file"] { font-size: var(--text-xs); max-width: 100%; }

.row-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

/* --- Choosing a person on their behalf ----------------------------------- */

.people { list-style: none; margin: var(--space-1) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.people li button { text-align: left; }

.chosen-person { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; margin-top: var(--space-1); }

@media (max-width: 480px) {
    .asset { flex-direction: column; }
    .asset-preview { flex: none; width: 100%; }
}

/* --- Access on approval -------------------------------------------------- */

.access-block { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--rule); }
.access-block h3 { margin: 0 0 var(--space-1); }

.grant-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }

.grant-list li { padding: var(--space-2) var(--control-pad-x); border-radius: var(--radius);
                 border-left: 3px solid var(--rule); background: var(--sunk); }

.grant-list li.free { border-left-color: var(--ok); }
.grant-list li.clash { border-left-color: var(--risk); }

/* Only the two things a code span in this list needs that a code span anywhere else does
   not: a lighter ground, because the row it sits on is already the sunk one, and a gap
   from the words before it. It used to restate the font, the padding and the radius in
   values a shade different from the shared ones, so the same idea was drawn two ways on
   the same page. */
.grant-list code { background: var(--surface); margin-left: var(--space-2); }

/* A chip that does something, rather than one that only reports. */
.chip.clickable { cursor: pointer; border: 1px dashed var(--rule); background: transparent; }
.chip.clickable:hover:not(:disabled) { border-style: solid; background: var(--sunk); }
.chip.clickable:disabled { cursor: default; opacity: .5; }

/* --- Message templates --------------------------------------------------- */

textarea.code { font-family: var(--font-mono); font-size: var(--text-sm); line-height: 1.45;
                white-space: pre; overflow-wrap: normal; overflow-x: auto; }

/* The preview renders a whole email, which brings its own colours with it,
   so it sits on a fixed neutral ground rather than inheriting the admin theme. */
.email-preview {
    margin-top: var(--space-2); border: 1px solid var(--rule); border-radius: var(--radius);
    overflow: auto; max-height: 26rem; background: #ffffff; color: #333333;
}

.email-preview table { width: 100%; }

/* The dialog for editing a long document wants more room than a form dialog. Written as a
   width rather than as a max-width, which is what it had been in two places with two
   different numbers: .dialog sets its own width, and a max-width above that number can
   only ever narrow it, so neither declaration was doing anything and every wide dialog
   was the ordinary size. Two classes outrank the one-class rule that fills the screen on
   a phone, which is what the !important in that rule is guarding against. */
.dialog.wide { width: min(56rem, 100%); }

/* --- Resources found in the Schoolbox calendar --------------------------- */

/* A card whose heading carries an action, so the bulk button sits with the
   explanation of what it would do rather than adrift above the table. */
.card-head { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: flex-start;
             justify-content: space-between; padding: var(--space-4) var(--space-4) var(--space-2); }
.card-head h2 { margin: 0 0 var(--space-1); }
.card-head p { margin: 0; }

.pad { padding: 0 var(--space-4) var(--space-4); }

/* The bookings list filter row: a status choice and the class toggle side by
   side, wrapping onto their own lines on a phone. */
.list-filters { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-4); align-items: center; }
.list-filters .toggle { margin: 0; }

/* --- Choosing spaces by typing ------------------------------------------- */

.space-picker { position: relative; min-width: min(26rem, 100%); }

.space-picker .chosen { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
                        margin-bottom: var(--space-1); }

.chip.removable { display: inline-flex; align-items: center; gap: var(--space-2); }

/* No padding of its own: the gap on the chip is already what separates the cross from the
   label, and a sliver of padding on top of it was one more number to keep in step. */
.chip.removable .remove {
    border: 0; background: none; cursor: pointer; font-size: var(--text-md); line-height: 1;
    padding: 0; color: inherit; opacity: .7;
}
.chip.removable .remove:hover { opacity: 1; }

.space-picker .space-search { width: 100%; }

/* Above the calendar grid, which is a dense mass of positioned cells the list
   would otherwise disappear behind. */
.space-picker .options {
    position: absolute; z-index: 40; top: 100%; left: 0; right: 0; margin-top: var(--space-1);
    max-height: 20rem; overflow-y: auto;
    background: var(--surface); border: 1px solid var(--rule);
    border-radius: var(--radius); box-shadow: var(--shadow-raised);
}

.space-picker .option {
    display: flex; align-items: center; gap: var(--space-2); width: 100%; text-align: left;
    border: 0; background: none; cursor: pointer; padding: var(--space-2) var(--control-pad-x); font: inherit;
    color: inherit;
}
.space-picker .option:hover { background: var(--sunk); }
.space-picker .option.chosen { font-weight: 600; }
.space-picker .option .tick { width: 1rem; color: var(--ok); }

.space-picker .option-group {
    padding: var(--space-2) var(--control-pad-x) var(--space-1); font-size: var(--text-xs); letter-spacing: .04em;
    text-transform: uppercase; color: var(--muted);
}

.space-picker .empty { padding: var(--space-3); color: var(--muted); }

/* Catches the click that closes the list, without a document-level handler that
   could outlive the page. */
.picker-backdrop { position: fixed; inset: 0; z-index: 30; }

.field.inline.grow { flex: 1 1 22rem; }

/* ---------- Build stamp ---------------------------------------------------
   Quiet by design. It answers one question, asked occasionally, and should not
   compete with anything on the page while nobody is asking it. */
.admin-foot {
  margin-top: var(--space-6);
  padding: var(--space-3) 0 var(--space-1);
  border-top: 1px solid var(--rule);
  color: var(--faint);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* ---------- Messages: banners and toasts ---------------------------------
   One vocabulary, two shapes. A banner describes a state that is still true
   and holds its place in the layout; a toast reports something that just
   happened and floats over it. Both take their colour from the same four
   tones, so the same situation never reads as amber in one place and red in
   another. */

.banner-body { flex: 1; min-width: 0; }
.banner-body strong { display: block; }

/* A banner naming several things at once. No bullets and no browser indent: each
   name is already set in bold on its own line, and 40px of list padding would
   push the whole thing away from the title it belongs to. */
.banner-body ul {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* The banner's own box, its tones and its full-width form are declared with .notice near
   the top of the file, where the two are kept identical while the markup migrates. A
   second set of rules lived here: it repeated the layout word for word, and then set the
   whole message in the tone colour, which the rule up there had deliberately decided
   against and explains why. Because these came later they quietly won, so a warning read
   as amber prose in a banner and as body ink in the notice beside it. Removed rather than
   reconciled: one description of a message is the point. */

.banner-close,
.toast-close {
  flex: none;
  background: none;
  border: 0;
  padding: 0 var(--space-1);
  font-size: var(--text-lg);
  line-height: 1;
  color: inherit;
  opacity: .6;
  cursor: pointer;
}
.banner-close:hover,
.toast-close:hover { opacity: 1; }

/* The stack sits above the page and out of the way of the sidebar. Bottom right
   rather than top: the top of every page here is a heading somebody is reading. */
.toasts {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* The gutter on both sides, so the stack can never be the thing that pushes a narrow
     page sideways. */
  width: min(24rem, calc(100vw - var(--space-4) * 2));
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--rule-strong);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-raised);
  font-size: var(--text-base);
  animation: toast-in .16s ease-out;
}

.toast-body { flex: 1; min-width: 0; }
.toast-body strong { display: block; }

.toast.info { border-left-color: var(--rule-strong); }
.toast.ok   { border-left-color: var(--ok); }
.toast.warn { border-left-color: var(--warn); }
.toast.risk { border-left-color: var(--risk); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(.4rem); }
  to   { opacity: 1; transform: none; }
}

/* Somebody who has asked for less movement gets the toast without the entrance. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
}

/* On a narrow screen the stack spans the width and sits above the thumb. */
@media (max-width: 640px) {
  .toasts { left: var(--space-4); right: var(--space-4); width: auto; }
}

/* ---------- Sortable, filterable tables ----------------------------------

   Written for the Spaces table, which is long enough that reading it top to
   bottom stopped being how anybody uses it. Everything here is built from the
   shared scale and palette, so the next long table can pick it up unchanged. */

/* The controls sit inside the card, above the rows they act on, so the filter
   and the list it is narrowing read as one object rather than two. */
.table-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  align-items: flex-end;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  background: var(--sunk);
  border-bottom: 1px solid var(--rule-strong);
}

.filter-fields {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: flex-end; flex: 1 1 24rem;
}

/* min-width: 0 rather than a floor in rems. A flex item defaults to refusing to
   shrink below its own content, which is exactly how a row of filters pushes a
   375px page sideways. */
.table-filters .field { margin: 0; flex: 1 1 9rem; min-width: 0; }
.table-filters .field.search { flex: 2 1 14rem; }

.table-filters label {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--faint);
}

.sort-pair { display: flex; gap: var(--space-2); align-items: center; }
.sort-pair select { flex: 1 1 auto; min-width: 0; }
.sort-pair .btn { flex: 0 0 auto; }

/* How many rows are shown and how many are not. The element carries role="status",
   so the new numbers are announced when a filter changes rather than waiting to be
   found. */
.filter-count {
  display: flex; flex-wrap: wrap;
  align-items: baseline; gap: var(--space-2);
  margin: 0;
  font-size: var(--text-sm);
  color: var(--muted);
}
.filter-count strong { color: var(--ink); }

/* ---- Sortable headings ----
   The heading stays a th so the column association survives, and aria-sort on it
   carries the direction. The button inside takes the click and the keyboard,
   because a th is neither clickable nor focusable on its own. */

th.sortable { padding: 0; }

th.sortable > .sort {
  display: flex; align-items: center; gap: var(--space-1);
  width: 100%;
  min-height: var(--control-h);
  padding: var(--space-2) var(--space-3);
  border: 0; background: none; cursor: pointer;
  font: inherit; color: inherit;
  letter-spacing: inherit; text-transform: inherit; text-align: left;
}

th.sortable > .sort:hover { color: var(--ink); background: var(--rule); }
th.sortable > .sort:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

/* The sorted column is coloured as well as arrowed, so which one it is survives a
   glance rather than depending on picking a five-pixel triangle out of a header row. */
th[aria-sort="ascending"] > .sort,
th[aria-sort="descending"] > .sort { color: var(--accent); }

/* Drawn with borders rather than set as a character, so it takes the text colour it
   sits in and needs no font that happens to carry the glyph. */
.sort-arrow {
  flex: 0 0 auto;
  width: 0; height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}
.sort-arrow.asc  { border-bottom: 5px solid currentColor; }
.sort-arrow.desc { border-top: 5px solid currentColor; }

/* A faint one under the pointer or on focus, which is what tells somebody the other
   seven headings are clickable too. */
th.sortable > .sort:hover .sort-arrow:not(.asc):not(.desc),
th.sortable > .sort:focus-visible .sort-arrow:not(.asc):not(.desc) {
  border-top: 5px solid var(--faint);
}

/* The sort control in the filter bar is the stand-in for the headings, so it is only
   present once they are gone. */
.sort-on-mobile { display: none; }

@media (max-width: 640px) {
  .sort-on-mobile { display: block; }
  .filter-fields { flex-basis: 100%; }

  /* The table has folded into cards and the shared rule clips the header row out of
     sight. Clipped is not gone: the sort buttons in it would still take keyboard
     focus, which means tabbing through eight controls nobody can see. Removed here
     instead, because every cell already carries its own label and the sort is
     offered again in the filter bar above. */
  .spaces-table thead { display: none; }
}

/* Nine columns need room. Squeezed below this the browser breaks "The Hutchins
   School" into one word per line and the row grows to three times its height, so
   the wrapper scrolls sideways instead. Not applied once the table has folded into
   cards, where a minimum width would push the whole page sideways. */
@media (min-width: 641px) {
  .spaces-table { min-width: 58rem; }
}

/* ---------- A value to be copied into somebody else's form ---------- */
/* Setting up single sign-on means retyping four addresses into Entra, and getting one
   character wrong produces an error that names neither the field nor the value it
   disagreed with. user-select: all makes one click select the whole value ready to
   copy, which is as close to a copy button as this can honestly get: the content
   security policy allows no inline script, and the clipboard API would mean
   introducing the project's first JavaScript file to save one keystroke.

   display: block and the wrapping rule because these are long absolute URLs and the
   Identity tab has to survive a 375px screen without pushing the page sideways. */
.copy-value {
  display: block;
  user-select: all;
  -webkit-user-select: all;
  font-family: ui-monospace, Consolas, monospace;
  font-size: var(--text-sm);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  overflow-wrap: anywhere;
  cursor: text;
}

.copy-value:hover { border-color: var(--accent); }

/* The steps carry boxed groups of values rather than a sentence, so the measure meant
   for prose would strand them in a narrow column on a wide screen. */
.setup-steps li > .subcard { max-width: none; margin-top: var(--space-3); }

/* A settings field the tab badge is counting.
   The count on the tab strip says how many are left in a section; without this the
   reader has to work out which by opening each one and guessing what blank means.
   A rule down the left rather than a filled box, so a form with three of them still
   reads as a form. */
.field.needs-value {
  /* Pulled back by the padding and the rule together. The rule sits between the two, so
     taking back only the padding left every marked field's contents three pixels to the
     right of every unmarked one, and a form with two of them stepped in and out down the
     page. */
  --rule-width: 3px;
  border-left: var(--rule-width) solid var(--warn);
  padding-left: var(--space-3);
  margin-left: calc((var(--space-3) + var(--rule-width)) * -1);
}

.field.needs-value > label { display: flex; align-items: center; gap: var(--space-2); }

/* ---------- Week, drawn against the clock ----------
   A month is lists in boxes; a week is a timetable. Entries are placed by when they
   happen and share the width where they overlap, so a double booking is a shape rather
   than something to be worked out by reading two times in a column. */

.timegrid { border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; }

/* One column for the hour labels and seven of equal width. minmax(0,1fr) rather than 1fr
   so a long title cannot widen its own day and knock the rest out of alignment. */
.timegrid-head,
.timegrid-allday,
.timegrid-body { display: grid; grid-template-columns: 3.5rem repeat(7, minmax(0, 1fr)); }

.timegrid-head { border-bottom: 1px solid var(--rule-strong); background: var(--sunk); }
.timegrid-head .timegrid-day {
  padding: var(--space-2); font-size: var(--text-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted); text-align: center;
}

.timegrid-allday { border-bottom: 1px solid var(--rule); }
.timegrid-allday .timegrid-day { padding: var(--space-1); min-height: 1.75rem; }
.timegrid-gutter {
  font-size: var(--text-xs); color: var(--faint); text-align: right;
  padding: var(--space-1) var(--space-2); border-right: 1px solid var(--rule);
}

/* The body scrolls on its own so the day headings stay put on a short screen. */
.timegrid-body { max-height: 70vh; overflow-y: auto; }

.timegrid-body .timegrid-day { position: relative; border-left: 1px solid var(--rule); }

/* Each hour is a fixed height, and that height is what every placement is a fraction of.
   Fixed rather than flexible: a row that grows to fit its contents would stretch under a
   busy hour and throw every entry below it out of line with the clock. */
.timegrid-hour { height: 2.75rem; border-top: 1px solid var(--rule); }
.timegrid-hour:first-child { border-top: none; }

.timegrid-body .timegrid-gutter .timegrid-hour {
  border-top: none; position: relative;
}
.timegrid-body .timegrid-gutter .timegrid-hour span {
  position: absolute; top: -.5em; right: 0; background: var(--surface); padding-left: .25rem;
}

.timegrid-day.today { background: var(--accent-soft); }

/* Placed entries sit over the hour lines. Absolute, with top and height set inline as
   percentages of the day, so the same numbers work at any width. */
.entry.placed {
  position: absolute; overflow: hidden; margin: 0;
  display: flex; flex-direction: column; align-items: flex-start;
  font-size: var(--text-xs); line-height: 1.2; padding: 2px 4px;

  /* A short booking can be shorter than its own text. Clipping beats letting a fifteen
     minute entry spill over the hour below it and sit on top of the next one. */
  min-height: 1rem;
}

.entry.placed .at { font-weight: 700; }
.entry.placed .what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

/* A phone cannot show seven columns against a clock and stay legible, so the week falls
   back to a day at a time, stacked. */
@media (max-width: 760px) {
  .timegrid-head,
  .timegrid-allday,
  .timegrid-body { grid-template-columns: 2.75rem repeat(7, minmax(4.5rem, 1fr)); }
  .timegrid { overflow-x: auto; }
}

/* --- Picking a few things out of a long list ------------------------------ */

/* Drawn as one unit rather than as a loose stack, because it is three controls that
   only make sense together: what you have chosen, what you are searching, and what is
   left. Loose, the chips read as the end of the field above. */
.picker { display: flex; flex-direction: column; gap: var(--space-2); }

.picker-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.picker-head label { margin: 0; }

.picker-search { width: 100%; }

/* The chosen row sits above the search, so narrowing the list never hides what is
   already ticked. Capped and scrolled rather than growing without limit: forty chips
   would push the list itself off the bottom of a dialog. */
.picker-chosen { max-height: 7.5rem; overflow-y: auto; }

/* A chosen chip is a label and a cross. Flex so a long door name ellipses and the cross
   stays put; the plain .chip rules would clip whichever came last. */
.chip.chosen {
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid var(--accent); border-style: solid;
  min-height: var(--touch-min);
}
.chip.chosen > span:first-child { overflow: hidden; text-overflow: ellipsis; }
.chip.chosen .chip-x { flex: 0 0 auto; opacity: .7; font-weight: 700; }
.chip.chosen:hover:not(:disabled) .chip-x { opacity: 1; }

.chip.quiet { color: var(--faint); min-height: var(--touch-min); }

/* The height is the point: eighty doors have to become a short list with a scrollbar,
   or the dialog's own buttons end up below the fold and the thing cannot be saved. */
.picker-list {
  max-height: 20rem; overflow-y: auto;
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--surface);
  -webkit-overflow-scrolling: touch;
}

.picker-group + .picker-group { border-top: 1px solid var(--rule); }

/* Sticky so the heading stays readable while its own doors scroll under it. Without it
   a long building scrolls its name away and the ticks below belong to nothing. */
.picker-group-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--sunk);
  border-bottom: 1px solid var(--rule);
}
.picker-group-head .perm-group-name { margin: 0; }

.picker-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
  padding: 0 var(--space-3);
}
.picker-row:hover { background: var(--sunk); }
.picker-row.on { background: var(--accent-soft); }
.picker-row .toggle { flex: 1 1 14rem; min-width: 0; }
.picker-row .toggle > span { min-width: 0; overflow-wrap: anywhere; }

/* A second line, not a word trailing the name, once there is a location to show. */
.picker-row .toggle > span .small { display: block; }

.picker-row select.inline { flex: 0 0 auto; }

.picker.disabled { opacity: .6; }
