/**
 * Components: cards, buttons, forms, badges, the availability grid, dialogs,
 * tables and toasts.
 */

/* --------------------------------------------------------------------------
   Cards
   -------------------------------------------------------------------------- */

.card {
  padding: var(--sp-5);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.card > h2, .card > h3 { margin-top: 0; }

.card-compact { padding: var(--sp-4); }

/* A card whose whole surface opens its editor. The hover lift is the only
   signal a mouse gets, so it is deliberately visible. */
.card-clickable {
  cursor: pointer;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}

.card-clickable:hover {
  border-color: var(--brand-700);
  box-shadow: 0 0 0 1px var(--brand-700);
}

/*
 * The title, which is the card's real control. It has to read as the heading
 * it sits in rather than as a button, so it takes the h4's font and loses the
 * chrome - but it stays a button, so it is focusable and announced as one.
 */
.card-title-button {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
}

.card-title-button:hover { text-decoration: underline; }

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  /* 44px min target: comfortable for touch and for motor impairments. */
  min-height: 2.75rem;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--brand-700);
  color: var(--text-inverse);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.btn:hover { background: var(--brand-600); }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn-secondary {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border-strong);
}
.btn-secondary:hover { background: var(--surface-sunken); }

.btn-danger {
  background: var(--danger-text);
  color: #fff;
}
.btn-danger:hover { filter: brightness(1.12); }

.btn-ghost {
  background: transparent;
  color: var(--brand-700);
  border-color: transparent;
}
.btn-ghost:hover { background: var(--brand-50); }

.btn-sm { min-height: 2rem; padding: var(--sp-1) var(--sp-3); font-size: var(--text-xs); }

/* Square button for a lone glyph: the 44px target is kept, the padding is not. */
.btn-icon { padding: var(--sp-2); min-width: 2.75rem; }

.btn-group { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* The refresh glyph spins while the day is loading. inline-block because a
   transform does nothing to an inline box. */
.refresh-glyph { display: inline-block; font-size: 1.1rem; line-height: 1; }

.is-refreshing .refresh-glyph { animation: refresh-spin 800ms linear infinite; }

@keyframes refresh-spin { to { transform: rotate(360deg); } }

/* Spinning is the only signal here, so something has to replace it when motion
   is unwelcome rather than leaving the button looking inert. */
@media (prefers-reduced-motion: reduce) {
  .is-refreshing .refresh-glyph { animation: none; opacity: 0.45; }
}

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--sp-1); margin-bottom: var(--sp-4); }

.field > label {
  font-size: var(--text-sm);
  font-weight: 600;
}

/* The asterisk is decorative; "required" is conveyed to assistive tech by the
   required attribute on the control itself. */
.field > label .req { color: var(--danger-text); margin-inline-start: 2px; }

.field-hint { font-size: var(--text-xs); color: var(--text-muted); }

input[type="text"], input[type="number"], input[type="date"], input[type="time"],
input[type="datetime-local"], input[type="email"], input[type="search"],
input[type="url"],
select, textarea {
  width: 100%;
  min-height: 2.75rem;
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}

textarea { min-height: 5rem; resize: vertical; }

input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-sunken);
  color: var(--text-muted);
}

/*
 * A field that is shown, not asked for. Must come after the rule above and
 * carry the type in the selector, or that rule's background wins and a
 * read-only field is indistinguishable from an empty one.
 *
 * Not :disabled - these values matter and should still be selectable and
 * readable by a screen reader walking the form.
 */
input[type="text"][readonly] {
  background: var(--surface-sunken);
  border-style: dashed;
  color: var(--text-muted);
  cursor: default;
}

/* aria-invalid drives the error styling, so the visual and the announced state
   can never disagree. */
[aria-invalid="true"] {
  border-color: var(--danger-text);
  background: var(--danger-bg);
}

.field-error {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--danger-text);
}

fieldset {
  margin: 0 0 var(--sp-4);
  padding: var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

legend { padding: 0 var(--sp-2); font-size: var(--text-sm); font-weight: 600; }

/** Radio cards, used for the required public/private meeting-type choice. */
.choice-group { display: grid; gap: var(--sp-2); }

.choice {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  padding: var(--sp-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}

.choice:hover { background: var(--surface-sunken); }

.choice input { margin: 0.2rem 0 0; width: 1.1rem; height: 1.1rem; flex: none; }

.choice:has(input:checked) {
  border-color: var(--brand-600);
  background: var(--brand-50);
  box-shadow: inset 0 0 0 1px var(--brand-600);
}

.choice:has(input:focus-visible) { box-shadow: var(--focus-ring); }

.choice-label { font-weight: 600; display: block; }
.choice-desc { font-size: var(--text-sm); color: var(--text-muted); }

/* --------------------------------------------------------------------------
   Badges. A status badge pairs an icon glyph with a word, so status is never
   carried by colour alone; an audience badge is the word by itself.
   -------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 0.1rem var(--sp-2);
  border: 1px solid;
  border-radius: 999px;
  font-size: var(--text-xs);
  font-weight: 650;
  white-space: nowrap;
}

.badge-ok { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }
.badge-pending { background: var(--pending-bg); border-color: var(--pending-border); color: var(--pending-text); }
.badge-danger { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-text); }
.badge-info { background: var(--info-bg); border-color: var(--info-border); color: var(--info-text); }
.badge-public { background: var(--public-bg); border-color: var(--public-border); color: var(--public-text); }
.badge-neutral { background: var(--surface-sunken); border-color: var(--border-strong); color: var(--text-muted); }

.badge-icon { font-size: 0.85em; line-height: 1; }

/* --------------------------------------------------------------------------
   Notices
   -------------------------------------------------------------------------- */

.notice {
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid;
  border-radius: var(--radius);
  font-size: var(--text-sm);
}

.notice p:last-child { margin-bottom: 0; }
.notice-info { background: var(--info-bg); border-color: var(--info-border); color: var(--info-text); }
.notice-ok { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }
.notice-warn { background: var(--pending-bg); border-color: var(--pending-border); color: var(--pending-text); }
.notice-danger { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-text); }

/* --------------------------------------------------------------------------
   Availability grid
   -------------------------------------------------------------------------- */

.grid-wrap {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
}

.avail-grid {
  display: grid;
  font-size: var(--text-xs);
  /*
   * The SMALLEST a 15-minute slot may be, not its fixed size: the lane tracks
   * are minmax(--slot-w, 1fr), so slots grow to fill a wide screen and fall
   * back to this floor on a narrow one.
   */
  --slot-w: 1.5rem;
  --room-col: 11rem;
  /*
   * The floor width, computed rather than max-content. --cols is set by
   * renderGrid. max-content would be measured from the booking titles inside
   * the lanes, which can be arbitrarily long, and would stretch the grid far
   * past the day it is drawing.
   */
  min-width: calc(var(--room-col) + (var(--cols, 40) * var(--slot-w)));
}

/*
 * Matches the phone breakpoint in base.css rather than inventing a second one.
 * Media queries cannot read custom properties, so 640px is written literally in
 * both files - see the note in tokens.css.
 *
 * --slot-w is deliberately NOT reduced here. 1.5rem is 24px, the WCAG 2.5.8
 * minimum target, and a slot too small to tap is not worth the width it saves.
 * The grid is already a scroll container; length is the cost, and the AM / PM
 * toggle is the control that shortens it.
 */
@media (max-width: 640px) {
  .avail-grid { --room-col: 6.5rem; }
  .avail-room { padding: var(--sp-2); }

  /*
   * The legend is decorative - it is aria-hidden, and every block already
   * names its own status in its accessible name - so on the screen with the
   * least room to spare it is the first thing to go.
   */
  .legends { display: none; }
}

.avail-row { display: grid; grid-template-columns: var(--room-col) 1fr; }

/*
 * Sticks to the top of .grid-wrap, NOT the viewport.
 *
 * .scroll-x sets overflow-x:auto, and a box with one axis scrollable computes
 * the other from `visible` to `auto` - so the wrapper is this row's scroll
 * container. Offsetting by --header-height (which is what the page header
 * would need) pushed the whole row 3.5rem down, straight on top of the first
 * room. Against this container the correct offset is zero.
 */
.avail-head {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border);
}

.avail-times { display: grid; }

.avail-time {
  padding: var(--sp-1) 0;
  text-align: center;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  border-left: 1px solid var(--border);
}

.avail-room {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-raised);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  position: sticky;
  left: 0;
  z-index: 2;
  /* The standard sticky-column cue: the lane visibly passes underneath, which
     is what says the grid continues past the right-hand edge. */
  box-shadow: 2px 0 4px -2px rgb(0 0 0 / 0.18);
}

.avail-room-name { font-size: var(--text-sm); font-weight: 650; }

.avail-lane {
  display: grid;
  /* One implicit row: slots and booked blocks share it, so a block can span
     several columns while still sitting in the same track as the slots it
     covers. Grid placement rather than absolute positioning means the two can
     never drift out of alignment. */
  grid-auto-rows: minmax(2.75rem, auto);
  border-bottom: 1px solid var(--border);
  background: var(--slot-free);
}

.avail-slot {
  grid-row: 1;
  border: none;
  border-left: 1px solid var(--border);
  min-height: 2.75rem;
  padding: 0;
  background: transparent;
  cursor: pointer;
}

.avail-slot:hover { background: var(--slot-free-hover); }

/* The run of slots a drag currently covers. Stronger than :hover so the two
   never read as the same thing. */
.avail-slot.is-selecting {
  background: var(--brand-700);
  opacity: 0.55;
}

/* A booked block sits in row 1 above the slot buttons it spans. */
.avail-block {
  grid-row: 1;
  z-index: 1;
  display: flex;
  /* Title over organiser. Centred as a pair so a block reads the same whether
     it is 15 minutes wide or three hours. */
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  /* Tight: a 15-minute block can be as narrow as --slot-w, so both lines
     ellipse away rather than forcing the slot wider. */
  gap: 0;
  line-height: 1.25;
  margin: 3px 1px;
  padding: 0 3px;
  border-radius: var(--radius-sm);
  background: var(--slot-busy);
  color: var(--slot-busy-text);
  font-size: var(--text-xs);
  font-weight: 600;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  border: none;
  cursor: pointer;
  text-align: left;
}

.avail-block:hover { filter: brightness(1.08); }
/* max-width:100% lets each line shrink inside the column rather than forcing
   the block wider than the slots it spans. */
.avail-block > span {
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The organiser is supporting detail: same size, lighter, so the title still
   leads at a glance. */
.avail-block-who { font-weight: 400; opacity: 0.85; }

.avail-block-pending {
  background: var(--slot-pending);
  color: var(--slot-pending-text);
  /* Hatching is what separates pending from confirmed: the block carries no
     glyph, so the distinction must not rest on hue alone. */
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 5px,
    rgb(0 0 0 / 0.14) 5px 10px
  );
}

/* ---------------------------------------------------------------------------
   Meeting audience on a CONFIRMED block.
   The audience is named in words - in the hover card and in the accessible
   name - and never carried by the block's colour or edge.
   --------------------------------------------------------------------------- */

/* A public meeting is the one audience with outside legal weight, so it keeps
   an outline on the block itself. Every other audience is carried by the hover
   card and the accessible name, not by the block's appearance. */
.avail-block-public { box-shadow: inset 0 0 0 2px var(--public-border); }

/* The legend and the hours toggle share a line, and stack when it runs out. */
/* Separates one month's bookings from the next. Sits between the section's
   own h2 and each booking's h4. */
.month-heading {
  margin: var(--sp-5) 0 var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* The first one heads the list, so it needs no space above it. */
.month-heading:first-child { margin-top: 0; }

.grid-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

/*
 * A segmented group: one border around the row, no gaps inside it.
 *
 * Generic on purpose - the view switcher, the hours filter and the room tabs
 * are the same control wearing three labels, and they looked subtly different
 * from each other when each had its own rules.
 */
.segmented { display: flex; }

.segmented .btn { border-radius: 0; }
.segmented .btn:first-child { border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); }
.segmented .btn:last-child { border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); }
.segmented .btn + .btn { margin-inline-start: -1px; }

/* Pressed is the selected option. Weight and fill both carry it, so the state
   does not rest on colour alone. */
.segmented .btn[aria-pressed="true"] {
  background: var(--brand-700);
  border-color: var(--brand-700);
  color: var(--text-inverse);
  font-weight: 700;
  z-index: 1;
}

.segmented .btn[aria-pressed="true"]:hover { background: var(--brand-600); border-color: var(--brand-600); }

/* The room tabs can outrun the window; they scroll rather than wrap, so the
   strip stays one row and the grid below keeps its place. */
.room-tabs { overflow-x: auto; max-width: 100%; }
.room-tabs .btn { white-space: nowrap; }

/*
 * No bottom margin: this is a flex child of .grid-controls, which centres its
 * children. A margin there is centred along with the box, which left the
 * legend sitting half the margin above the AM / PM buttons beside it.
 */
.legends { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-6); }

/* ---------------------------------------------------------------------------
   Hover card
   --------------------------------------------------------------------------- */

.hover-card {
  position: absolute;
  z-index: 70;
  max-width: 20rem;
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-xs);
  pointer-events: none;  /* never swallow the hover that opened it */
}

/* Title and status chip on one line, wrapping when the title is long. */
.hover-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}

.hover-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.3;
}

.hover-facts {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-1) var(--sp-3);
}

.hover-facts dt { color: var(--text-muted); white-space: nowrap; }
.hover-facts dd { margin: 0; font-weight: 600; }

.legend { display: flex; flex-wrap: wrap; gap: var(--sp-4); font-size: var(--text-xs); }
.legend-item { display: flex; align-items: center; gap: var(--sp-2); }
.legend-swatch {
  width: 1.25rem; height: 0.9rem;
  border-radius: 3px;
  border: 1px solid var(--border-strong);
}

/* --------------------------------------------------------------------------
   Tables
   -------------------------------------------------------------------------- */

.table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }

.table caption {
  padding-bottom: var(--sp-2);
  text-align: left;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.table th, .table td {
  padding: var(--sp-3);
  text-align: left;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.table th {
  background: var(--surface-sunken);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.table tbody tr:hover { background: var(--surface-sunken); }
.table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   Dialog
   -------------------------------------------------------------------------- */

dialog {
  width: min(42rem, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  padding: 0;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

dialog::backdrop { background: rgb(8 16 24 / 0.55); }

.dialog-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
}

.dialog-head h2 { margin: 0; font-size: var(--text-lg); }

/* Title and its status chip on one line, wrapping on a narrow dialog. */
.dialog-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}
.dialog-body { padding: var(--sp-5); overflow-y: auto; }
.dialog-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--border);
  background: var(--surface-sunken);
}

.dialog-close {
  flex: none;
  min-height: 2rem;
  padding: 0 var(--sp-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
}
.dialog-close:hover { background: var(--surface-sunken); color: var(--text); }

/* --------------------------------------------------------------------------
   Toasts. Announced via an aria-live region defined in the markup.
   -------------------------------------------------------------------------- */

.toast-region {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-width: min(30rem, calc(100vw - 2rem));
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid;
  border-radius: var(--radius);
  background: var(--surface-raised);
  box-shadow: var(--shadow);
  font-size: var(--text-sm);
}

.toast-ok { border-color: var(--ok-border); background: var(--ok-bg); color: var(--ok-text); }
.toast-error { border-color: var(--danger-border); background: var(--danger-bg); color: var(--danger-text); }
.toast-info { border-color: var(--info-border); background: var(--info-bg); color: var(--info-text); }

.toast-dismiss {
  flex: none;
  margin-inline-start: auto;
  padding: 0 var(--sp-1);
  background: none;
  border: 0;
  color: inherit;
  font-size: var(--text-base);
  cursor: pointer;
}

/* --------------------------------------------------------------------------
   Loading and empty states
   -------------------------------------------------------------------------- */

.skeleton {
  background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border) 50%, var(--surface-sunken) 75%);
  background-size: 200% 100%;
  border-radius: var(--radius-sm);
  animation: shimmer 1.4s infinite linear;
}

@keyframes shimmer {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

.empty-state {
  padding: var(--sp-7) var(--sp-5);
  text-align: center;
  color: var(--text-muted);
}

.empty-state p { max-width: 42ch; margin-inline: auto; }

/* --------------------------------------------------------------------------
   Stat tiles for the reports page
   -------------------------------------------------------------------------- */

.stat-row {
  display: grid;
  gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
}

.stat {
  padding: var(--sp-4);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.stat-value {
  font-size: var(--text-2xl);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.stat-label { font-size: var(--text-xs); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }

/** Horizontal bar, used for utilization by room. */
.bar-track {
  height: 0.5rem;
  background: var(--surface-sunken);
  border-radius: 999px;
  overflow: hidden;
}
.bar-fill { height: 100%; background: var(--brand-600); }

/* Today's row in week view, so the current day is findable at a glance. */
.avail-row.is-today .avail-room-name { color: var(--brand-700); }

/*
 * The line between what has happened and what has not, in a list that runs
 * straight through both. A rule with the word sitting in it, so the join is
 * findable while scrolling without shouting over the month headings.
 */
.now-divider {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* It is a <p>, and the base stylesheet caps paragraphs at a reading
     measure - which stopped the rule two thirds of the way across. */
  max-width: none;
  margin: var(--sp-5) 0 var(--sp-4);
  color: var(--brand-700);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.now-divider::before,
.now-divider::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--brand-700);
  opacity: 0.45;
}

/* ---------------------------------------------------------------------------
   The printed sheets

   One landscape page, whichever view produced it: the week is one room over
   five days, the day is every room over one date. A list rather than a picture
   of the grid, because the grid clips to the selected hour window and nobody
   can change that once it is on paper.

   Both sheets share every rule here. They are read side by side by the same
   people, and a day sheet that set its own type size or rule weights would
   look like a different document rather than the same one for a shorter span.
   What differs between them is structure, not style, and that lives in the
   markup each one builds.

   Everything here is in service of the single page - one table, small type,
   tight rows - so a change that loosens any of them is a change that can spill
   the sheet onto a second page.
   --------------------------------------------------------------------------- */

.print-sheet { display: none; }

@media print {
  .print-sheet { display: block; }

  .print-sheet h2 { margin: 0; font-size: 1.1rem; }
  .print-sheet-range {
    margin: 0 0 var(--sp-3);
    max-width: none;
    font-size: 0.8rem;
  }

  .print-sheet table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.72rem;
  }

  .print-sheet th,
  .print-sheet td {
    padding: 0.15rem 0.45rem;
    text-align: left;
    vertical-align: top;
    line-height: 1.25;
  }

  .print-sheet thead th { border-bottom: 1.5px solid #000; }

  /* Hairlines between bookings, a heavy rule between groups - days on the week
     sheet, rooms on the day sheet. The group boundary is what the eye follows
     down a dense sheet, so the contrast between the two weights is doing the
     real work - raise one and raise the other. */
  .print-sheet tbody td { border-bottom: 1px solid #ddd; }
  .print-sheet tbody tr.print-sheet-groupstart > * {
    border-top: 2px solid #000;
    /* A rule with rows packed hard against it reads as a box, not a break. */
    padding-top: 0.3rem;
  }
  .print-sheet tbody th {
    border-right: 1px solid #ddd;
    padding-right: var(--sp-3);
    white-space: nowrap;
    font-weight: 700;
  }

  /* A group should not be split across a page break if it can be helped. The
     day sheet makes this reachable: a room with a dozen bookings is a block
     worth keeping whole, where a week's Wednesday rarely had the chance. */
  .print-sheet tbody tr { break-inside: avoid; }

  /* Not a colour: this prints on a mono laser as often as not. */
  .print-sheet-pending { font-style: italic; white-space: nowrap; }

  .print-sheet-note { margin: var(--sp-3) 0 0; font-size: 0.72rem; }
}
