:root {
  --color-bg: #f3f4f7;
  --color-surface: #ffffff;
  --color-text: #16181d;
  --color-muted: #5b6472;
  --color-border: #e3e6eb;
  --color-primary: #2952e3;
  --color-primary-dark: #1e3fc4;
  --color-primary-tint: #eef1fd;
  --color-danger: #dc2626;
  --color-free: #e9f7ef;
  /* Darkened from the original #2fa85a: that shade was 2.77:1 against its
     own --color-free fill, short of the 3:1 WCAG 1.4.11 floor for a UI
     component boundary. This shade clears ~3.9:1 against the fill and
     ~4.4:1 against a white card background. */
  --color-free-border: #278a49;
  --color-booked: #fdecea;
  --color-booked-border: #dc2626;
  /* --color-danger is only 4.22:1 on --color-booked, short of the 4.5:1
     WCAG 1.4.3 floor for regular-size text. This shade clears ~5.66:1
     against that same fill, for danger-colored text that sits on it. */
  --color-danger-text: #b91c1c;
  --color-mine: #fff1e0;
  /* ~4.5:1 against --color-mine, ~5:1 against a white card background —
     comfortably clears the 3:1 WCAG 1.4.11 floor, same reasoning as
     --color-free-border above. */
  --color-mine-border: #b45309;
  /* Security-event severity, low → high reading as yellow → red — same
     fill/border/row-tint shape as --color-mine above, applied to the
     activity log's blocked-attempt rows instead of a booking slot. High
     deliberately reuses --color-booked/--color-danger rather than a fourth
     palette: it's the same "this is the bad one" red already used
     everywhere else in the app, not a new meaning to learn. */
  --color-sev-low: #fff3c4;
  --color-sev-low-border: #8a6d00;
  --color-sev-low-row: #fffaeb;
  --color-sev-medium: #ffdcb0;
  --color-sev-medium-border: #9a4b00;
  --color-sev-medium-row: #fff3e0;
  --color-sev-high: var(--color-booked);
  --color-sev-high-border: var(--color-danger);
  --color-sev-high-row: #fef2f2;
  --radius: 10px;
  --radius-lg: 14px;
  --gap: 1rem;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.08);
  --ticker-height: 2.75rem;
  --footer-height: 2.5rem;
}

* { box-sizing: border-box; }

/* Protected pages start hidden and only reveal once js/auth.js confirms the
   signed-in user actually has the right role — otherwise a direct link to,
   say, /admin/rooms.html would flash real page structure for a moment
   before the redirect-away kicks in. */
body.auth-guard { display: none; }

/* Skip-to-content link: the first focusable element on every page, off-screen
   until it receives keyboard focus. inset-inline-start (not left) so it
   anchors to the reading-direction start on both RTL and LTR pages. */
.skip-link {
  position: absolute;
  top: -100px;
  inset-inline-start: 0;
  background: var(--color-primary);
  color: #fff;
  padding: 0.75rem 1.25rem;
  z-index: 200;
  border-radius: 0 0 8px 0;
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus {
  top: 0;
}

/* Fixed at the bottom of the viewport on every page (kiosk excepted, see
   .kiosk-page .app-footer below) so the accessibility statement link stays
   reachable without scrolling or swiping. On book.html the announcements
   ticker also claims bottom:0 when it's showing (body.has-announcements-ticker)
   — the footer sits directly above it instead of overlapping. */
.app-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 35;
  min-height: var(--footer-height);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  text-align: center;
  padding: 0.5rem 1rem;
  font-size: 0.85rem;
}
.app-footer a { color: var(--color-muted); }

body:not(.kiosk-page) {
  padding-bottom: var(--footer-height);
}

body.has-announcements-ticker:not(.kiosk-page) .app-footer {
  bottom: var(--ticker-height);
}

/* Visually hidden but present for screen readers — for cases like each room
   card in admin/rooms.html, where the room's "name" is an editable <input>
   (so it can't itself be a heading) but IS 5568's elevated 2.4.10 still
   requires a real H-tag identifying what section "Working hours"/"Excluded
   dates" belong to. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 0.5rem;
}
h2 { font-size: 1.2rem; }
h3 { font-size: 1.05rem; }
h4 { font-size: 0.95rem; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.04em; }

.topbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: 0.75rem;
  padding: 0.85rem 1.25rem;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  z-index: 10;
}

.topbar h1,
.topbar .topbar-heading-group {
  grid-column: 2;
  justify-self: center;
}

.topbar h1 {
  font-size: 1.15rem;
  margin: 0;
  color: var(--color-text);
  text-align: center;
}

.topbar h1::before {
  content: "";
  display: inline-block;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 3px;
  background: var(--color-primary);
  margin-inline-end: 0.55rem;
}

.topbar nav {
  grid-column: 3;
  justify-self: end;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Some nav links are wrapped in one element so JS can reveal them as a
   group only for certain signed-in visitors, rather than one at a time —
   display: contents keeps the wrapper itself out of the box model, so its
   <a> children lay out as direct flex items of .topbar nav (respecting
   its gap) instead of being visually grouped inside a nested inline box.
   The .hidden utility's !important still wins over this when the group is
   hidden. */
#admin-nav-links {
  display: contents;
}

/* On phone widths the grid's 1fr side column gets too narrow for the nav,
   whose buttons then wrap into a vertical stack roughly three buttons
   tall. Stack the heading over a single horizontal button row instead
   (still wrapping for pages with a longer nav) and shrink the buttons, so
   the whole header collapses to two compact rows. */
@media (max-width: 639px) {
  .topbar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.6rem;
    padding: 0.6rem 1rem;
  }
  .topbar nav {
    justify-content: center;
  }
  .topbar nav .btn {
    padding: 0.45rem 0.8rem;
    font-size: 0.9rem;
  }
}

.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 1.25rem 1rem 3rem;
}

.container.wide {
  max-width: 1200px;
}

/* The activity log's table carries eleven columns, so the usual 1200px
   cap forced constant horizontal scrolling on a wide monitor while
   leaving a third of the screen empty. Viewport-relative so it actually
   uses the space available, and dropped entirely below laptop width where
   80% would be narrower than the plain container. */
.container.extra-wide {
  max-width: 80vw;
}

@media (max-width: 1024px) {
  .container.extra-wide {
    max-width: none;
  }
}

/* Slightly denser than the other tables — it's a dense reference table
   read in bulk, not a form. */
.log-table {
  font-size: 0.85rem;
}

.container.full-width {
  max-width: none;
}

.container.full-width .narrow-card {
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
}

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  margin-bottom: var(--gap);
  box-shadow: var(--shadow-sm);
}

.form-field {
  margin-bottom: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/* For a form-field placed in a horizontal, align-items: flex-end row
   alongside a plain button (which has no margin-bottom) — without this,
   the field's own margin-bottom pushes its bottom edge below the
   button's, throwing off the flex-end alignment. */
.inline-form .form-field {
  margin-bottom: 0;
}

.form-field label {
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--color-muted);
}

input, select, textarea {
  font: inherit;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: var(--color-text);
  width: 100%;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

input:focus, select:focus, textarea:focus {
  outline: none;
  /* The actual required indicator is this border-color change (~6.2:1
     against a white background, well past the 3:1 floor for a focus
     indicator) — the box-shadow below is a supplementary soft glow, not
     the thing carrying compliance, since a translucent glow can't hit
     3:1 on its own without stopping looking like a glow. Using the
     primary color at partial opacity here (rather than the flat, much
     paler --color-primary-tint, ~1.1:1 against white) at least makes that
     glow itself meaningfully more visible than before. */
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(41, 82, 227, 0.35);
}

/* A value the form shows but doesn't let you change (e.g. the booking
   email, which comes from the signed-in account). Muted fill so it reads
   as "fixed" at a glance, but normal text contrast — it's information the
   user still needs to read, not a disabled control. */
.input-readonly {
  background: var(--color-bg);
  color: var(--color-muted);
  cursor: default;
}

.input-readonly:focus {
  border-color: var(--color-border);
  box-shadow: none;
}

.field-note {
  font-size: 0.8rem;
  color: var(--color-muted);
}

.date-field {
  cursor: pointer;
}
.date-field:hover input {
  border-color: var(--color-primary);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.65rem 1.1rem;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  min-height: 44px;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}

.btn:hover { border-color: var(--color-primary); color: var(--color-primary); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn:disabled:hover { border-color: var(--color-border); color: var(--color-text); }

.btn-primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover { background: var(--color-primary-dark); border-color: var(--color-primary-dark); color: #fff; }

.btn-danger {
  background: var(--color-danger);
  border-color: var(--color-danger);
  color: #fff;
}
.btn-danger:hover { background: #b91c1c; border-color: #b91c1c; color: #fff; }

.btn-block { width: 100%; }

/* Compact button for inside list rows (My Bookings' Cancel) — the default
   .btn padding reads fine as a standalone control but is oversized next to
   a single line of row text, especially on phones where it wraps to two
   lines and dwarfs the boxed notes in sibling rows. */
.btn-sm {
  padding: 0.4rem 0.75rem;
  font-size: 0.85rem;
  white-space: nowrap;
  flex-shrink: 0;
}

/* The three values of a My Bookings entry, each with its own label, kept
   on one line so labelling them doesn't double the height of every row
   (and the length of the whole list). Wraps only when genuinely out of
   room on a narrow phone. */
.booking-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 1rem;
  min-width: 0;
}

.booking-field {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  white-space: nowrap;
}

.booking-field-label {
  color: var(--color-muted);
  font-size: 0.8rem;
  font-weight: 600;
}

.booking-field-value {
  font-weight: 600;
}

/* A booking the desk cancelled is kept in the list on purpose, but must
   not read as still active — muted and struck through, with the reason
   spelled out in the note beside it. */
.list-row.is-cancelled .booking-field-value {
  color: var(--color-muted);
  text-decoration: line-through;
  font-weight: 500;
}

.booking-note-cancelled {
  border-color: var(--color-booked-border);
  background: var(--color-booked);
  color: var(--color-danger-text);
}

/* The non-clickable counterpart of a row's Cancel button ("Already passed" /
   "Too late to cancel") — boxed with the same shape so the two row endings
   read as the same kind of element, but in the flat muted palette on the
   page background so it doesn't invite a tap. */
.booking-note {
  padding: 0.4rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-bg);
  color: var(--color-muted);
  font-size: 0.85rem;
  white-space: nowrap;
  flex-shrink: 0;
}

.muted { color: var(--color-muted); font-size: 0.9rem; }

.error-text {
  color: var(--color-danger);
  font-size: 0.9rem;
  margin-top: 0.5rem;
}

/* Informational, not a failure — e.g. "this hour has already started, the
   booking runs until 14:00". Distinct from .error-text so it doesn't read
   as something having gone wrong. */
.modal-note {
  margin-top: 0.5rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--color-primary);
  border-radius: 8px;
  background: var(--color-primary-tint);
  color: var(--color-primary-dark);
  font-size: 0.9rem;
  font-weight: 600;
}

/* An attribution pair (who + an optional comment) shown before a certain
   kind of change may be saved. Empty (and so invisible) otherwise. */
.attribution-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: flex-end;
  margin-top: 0.75rem;
}

.attribution-row:empty {
  display: none;
}

.librarian-field {
  margin-bottom: 0;
  min-width: 180px;
  flex: 1 1 180px;
}

/* Shown when adding a room exclusion would cancel existing bookings —
   deliberately louder than a plain .error-text line, since ticking the
   box below it destroys other people's bookings. */
.exclusion-cancel-warning {
  margin-top: 0.75rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--color-danger);
  border-radius: 8px;
  background: var(--color-booked);
}

.exclusion-cancel-warning p {
  margin: 0;
  color: var(--color-danger-text);
  font-weight: 600;
  font-size: 0.9rem;
}

.exclusion-cancel-warning label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
}

/* Same override as .privacy-ack-row's checkbox: the global
   `input { width: 100% }` rule otherwise turns the checkbox into a
   full-width flex item that squeezes the label text into a narrow
   wrapped column beside it. */
.exclusion-cancel-warning input[type="checkbox"] {
  width: auto;
  flex-shrink: 0;
}

/* The full privacy notice, read inline on privacy-ack.html. Capped in
   height so the checkbox and "continue" button stay visible on screen
   while the text is being read, instead of sitting a long scroll below
   it — the box scrolls, the page doesn't. */
.privacy-scroll {
  max-height: 40vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 1rem 0;
  padding: 0.25rem 1rem;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-bg);
  font-size: 0.9rem;
  line-height: 1.6;
}

.privacy-scroll h2 {
  font-size: 1rem;
  margin: 1rem 0 0.35rem;
}

.privacy-scroll p {
  margin: 0 0 0.75rem;
}

.privacy-ack-row {
  margin: 0.75rem 0;
  font-size: 0.9rem;
}

.privacy-ack-row label {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  cursor: pointer;
}

.privacy-ack-row input[type="checkbox"] {
  width: auto;
  margin-top: 0.15rem;
  flex-shrink: 0;
}

/* A swipeable, snap-scrolling carousel (one room per "page", each peeking
   its neighbours) on a phone-sized screen, so viewing every room costs
   horizontal swipes instead of scrolling down past all of them; on
   anything wider, a single row that always fits every room card without
   needing to scroll down to see them all — cards flex to share the
   available width equally rather than wrapping to a second row. */
.rooms-grid {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  gap: var(--gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-block-end: 0.25rem;
}

@media (prefers-reduced-motion: reduce) {
  .rooms-grid {
    scroll-behavior: auto;
  }
}

.rooms-grid > .room-card {
  flex: 0 0 88%;
  min-width: 0;
  scroll-snap-align: center;
}

/* 1280px, not the 640px breakpoint used elsewhere on this page — a row of
   every room (currently up to 8) needs real desktop width to not be
   cramped, and phones AND tablets (including iPad Pro portrait, 1024px,
   and smaller laptop windows up to 1279px) are exactly the "forced to
   scroll/squint" case this carousel exists for. Keep in sync with the
   matching 1280px in .slot-grid below, which controls how many columns
   of hour buttons fit inside a single room card and needs to flip at the
   same point this does. */
@media (min-width: 1280px) {
  .rooms-grid {
    overflow-x: visible;
    scroll-snap-type: none;
  }
  .rooms-grid > .room-card {
    flex: 1 1 0;
  }
}

/* Prev/next + dot pagination for the mobile carousel above — irrelevant
   once .rooms-grid stops being a carousel at the same 640px breakpoint. */
.carousel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  margin-top: 0.5rem;
}

.carousel-controls[hidden] {
  display: none;
}

@media (min-width: 1280px) {
  .carousel-controls {
    display: none;
  }
}

.carousel-nav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
}
.carousel-nav-btn:hover:not(:disabled) { border-color: var(--color-primary); color: var(--color-primary); }
.carousel-nav-btn:active:not(:disabled) { transform: translateY(1px); }
.carousel-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.carousel-nav-btn svg { width: 18px; height: 18px; }

/* Each icon is drawn pointing the way it acts in a left-to-right reading
   order (prev = left, next = right); flipped under RTL so the arrows keep
   matching the direction the carousel actually advances in, not raw
   left/right. */
html[dir="rtl"] .carousel-nav-btn svg {
  transform: scaleX(-1);
}

.carousel-dots {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
}

.carousel-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
}

.carousel-dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-border);
  border: 1px solid var(--color-muted);
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

/* The active dot isn't marked by color alone (WCAG 1.4.1) — it's also
   visibly larger, same reasoning as the free/mine/taken/closed slot legend
   not relying purely on color. */
.carousel-dot[aria-current="true"]::before {
  background: var(--color-primary);
  border-color: var(--color-primary-dark);
  transform: scale(1.4);
}

.room-card h3 {
  margin: 0 0 0.5rem;
  text-align: center;
}

.text-center {
  text-align: center;
}

.legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.9rem 1.5rem;
  margin: 0.5rem 0 1.25rem;
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--color-muted);
}

.legend-swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 1px solid;
  flex-shrink: 0;
}

.legend-swatch-free {
  background: var(--color-free);
  border-color: var(--color-free-border);
}

.legend-swatch-mine {
  background: var(--color-mine);
  border-color: var(--color-mine-border);
}

.legend-swatch-taken {
  background: var(--color-booked);
  border-color: var(--color-booked-border);
}

.legend-swatch-closed {
  background: var(--color-bg);
  border-color: var(--color-border);
}

/* Matches .kiosk-room .slot-btn.taken's stripe pattern below, so the legend
   swatch actually looks like the grid cell it's explaining. */
.legend-swatch-striped {
  background-image: repeating-linear-gradient(
    45deg,
    var(--color-booked) 0,
    var(--color-booked) 4px,
    var(--color-booked-border) 4px,
    var(--color-booked-border) 5px
  );
}

/* Sized to still read from across a room, same reasoning as the kiosk
   overrides for the ticker/heading elsewhere in this file. */
.kiosk-legend {
  padding: 0 1.25rem;
  flex-shrink: 0;
}
.kiosk-legend .legend-item {
  font-size: 1.1rem;
}
.kiosk-legend .legend-swatch {
  width: 20px;
  height: 20px;
}

/* A fixed column count, not auto-fill — auto-fill computes its column count
   from each grid's own exact pixel width, and 8 flex-equal room cards can
   differ by a rounding pixel or two (unavoidable when dividing a row's
   width by a non-factor count), which is normally invisible but auto-fill
   treats as a hard cutoff: one card can jump to an extra column while its
   otherwise-identical siblings don't. A fixed count can't do that — every
   card gets the same shape regardless of sub-pixel width differences. */
.slot-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.5rem;
  margin-top: 0.5rem;
}

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

  /* A room whose working hours add up to an odd number of slots leaves
     the last one alone in the final row — stuck to one side with an empty
     cell beside it instead of reading as the grid's actual last slot.
     Centering it across both columns instead makes that row read as
     "one slot, centered" rather than "half a row, half empty". Kiosk's
     own slot-grid (display.js) always uses a fixed 6-column layout with
     its own deliberate trailing empty cell (see .kiosk-room .slot-grid),
     so this is scoped to book.html's grid only. */
  body.book-page .slot-grid > .slot-btn:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    justify-self: center;
    width: calc(50% - 0.225rem);
  }
}

.slot-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 44px;
  border-radius: 8px;
  border: 1px solid var(--color-free-border);
  background: var(--color-free);
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.05s ease, filter 0.15s ease;
}
.slot-btn:not(:disabled):hover { filter: brightness(0.96); }
.slot-btn:not(:disabled):active { transform: translateY(1px); }

.slot-btn[aria-pressed="true"] {
  background: var(--color-primary);
  border-color: var(--color-primary-dark);
  color: #fff;
}

.slot-btn.taken {
  background: var(--color-booked);
  border-color: var(--color-booked-border);
  color: var(--color-muted);
  cursor: not-allowed;
}

.slot-btn.mine {
  background: var(--color-mine);
  border-color: var(--color-mine-border);
  color: var(--color-muted);
  cursor: not-allowed;
}

.slot-btn.closed {
  background: var(--color-bg);
  border-color: var(--color-border);
  color: var(--color-muted);
  cursor: not-allowed;
}

.booking-actions {
  /* Three stacked rows — selection summary, privacy checkbox, then the
     reset/confirm buttons — not one flex row letting them all sit
     side-by-side until they happen to wrap. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.75rem;
}

.booking-actions-buttons {
  display: flex;
  gap: 0.5rem;
}

table.hours-table {
  width: 100%;
  border-collapse: collapse;
}

table.hours-table th, table.hours-table td {
  padding: 0.5rem;
  border-bottom: 1px solid var(--color-border);
  text-align: start;
  font-size: 0.9rem;
}

/* Real <ul>/<li> markup (so a screen reader announces "list of N") styled
   to still read as plain rows, not bullets — shared by every list built
   from repeated .list-row items (My Bookings, librarians, announcements,
   room exclusions). */
.plain-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--color-border);
}
.list-row:last-child { border-bottom: none; }

.announcement-row {
  flex-wrap: wrap;
}

.announcement-text {
  flex: 1 1 auto;
  min-width: 160px;
}

.announcement-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}

.announcement-edit-input {
  flex: 1 1 auto;
  min-width: 160px;
}

.badge {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
}

.kiosk-page {
  font-size: 19px;
}

.kiosk-page .topbar h1 {
  font-size: 1.5rem;
}

.topbar-heading-group {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.live-indicator {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  font-weight: 700;
  padding: 0.25rem 0.65rem;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-muted);
}

.live-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-muted);
  flex-shrink: 0;
}

.live-indicator.is-live {
  color: #15803d;
  border-color: #86efac;
  background: #f0fdf4;
}
.live-indicator.is-live .live-dot {
  background: #22c55e;
  animation: live-pulse 2s ease-in-out infinite;
}

.live-indicator.is-offline {
  color: var(--color-danger);
  border-color: #fecaca;
  background: #fef2f2;
}
.live-indicator.is-offline .live-dot {
  background: var(--color-danger);
}

@keyframes live-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.35); }
  50% { box-shadow: 0 0 0 5px rgba(34, 197, 94, 0.12); }
}

.kiosk-page .live-indicator {
  font-size: 1rem;
  padding: 0.35rem 0.9rem;
}
.kiosk-page .live-dot {
  width: 12px;
  height: 12px;
}

/* This page runs unattended on a TV — nothing to scroll with — so the
   whole page is pinned to exactly one viewport height and never grows
   past it. body.kiosk-page becomes a column flexbox: the topbar and
   footer keep their natural height, and .kiosk-container/.kiosk-board
   absorb whatever's left, shrinking rather than overflowing as more
   rows of rooms are added. */
body.kiosk-page {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.kiosk-page .topbar,
.kiosk-page .app-footer {
  flex-shrink: 0;
}

.kiosk-page .app-footer {
  /* This page is already pinned to exactly one viewport height with
     nothing to scroll, so the footer (a normal flex child here, not
     fixed) is already always visible without needing the fixed-position
     treatment every other page gets. relative — not static — for the same
     reason as .kiosk-page .book-header above: .book-footer .book-bands
     and .book-footer-photo are position: absolute and need a real
     positioning context from their parent, or they size themselves
     against the viewport instead of the footer bar. relative keeps the
     normal in-flow position (no offsets set) while still providing that
     context. */
  position: relative;
  padding: 0.5rem 1rem;
}

.container.kiosk-container {
  max-width: 100%;
  /* Same reasoning as .admin-page .container's width: 100% (see its own
     comment): .container's margin: 0 auto plus this being a flex column's
     child means auto cross-axis margins, not stretch, decide the width —
     without an explicit width they'd size this to its own content instead
     of the full bar. Currently masked here (the room grid's own min-content
     width happens to push it wide regardless), but not something to rely
     on for every room count/screen width this ever runs at. */
  width: 100%;
  padding: 1rem 2rem;
  flex: 1;
  min-height: 0; /* lets this flex child shrink below its content's natural height instead of forcing the page to grow */
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.kiosk-board {
  flex: 1;
  min-height: 0;
  display: grid;
  /* Fixed 4 columns, not responsive — this is a dedicated kiosk screen,
     not a page anyone resizes. grid-auto-rows: 1fr means however many
     rows of rooms exist, they always split the available height evenly,
     so cards shrink to fit instead of pushing the page past one screen. */
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 1fr;
  gap: 1rem;
}

.kiosk-room {
  margin-bottom: 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.kiosk-room h2 {
  margin-bottom: 0.9rem;
  font-size: 1.8rem;
  text-align: center;
  flex-shrink: 0;
}

.kiosk-room-closed {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  margin: 0;
}

.kiosk-room .slot-grid {
  /* Always exactly 11 cells (8:00-18:00 inclusive, see BOARD_START_HOUR/
     BOARD_END_HOUR in display.js) — a fixed 6-column grid (auto-flowing
     into 2 rows: 6 + 5, one empty cell at the end of the second row)
     rather than auto-fill/wrap, so every room's grid has the same shape
     regardless of that room's own hours. Row height is capped (not 1fr)
     so cells stay a compact, consistent size instead of stretching to
     fill however much card height happens to be available with few
     room-rows on screen — align-content centers the resulting (shorter
     than available) block within the card instead of leaving it pinned
     to the top. The cap is deliberately below what 1fr would give: big
     enough to read from across a room, not so big the grid swallows the
     whole card.
  */
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: repeat(2, minmax(0, 9vh));
  align-content: center;
  gap: 0.6rem;
}

.kiosk-room .slot-btn {
  min-height: 0;
  height: 100%;
  /* Scales with viewport height rather than a fixed rem size, matching
     the row-height cap above — clamped so it never gets unreadably small
     or larger than the (now capped) cell actually needs. */
  font-size: clamp(0.9rem, 2.4vh, 1.7rem);
  font-weight: 700;
  border-radius: 12px;
}

/* Unlike book.js's version of this same grid, these cells are plain,
   non-interactive <div>s (see display.js) — no disabled/aria-pressed state
   to fall back on, so background color alone was the only thing telling a
   taken hour apart from a free one. A stripe texture (WCAG 1.4.1) fixes
   that without changing the cell's text or size, which matters here: the
   time label already sits close to this grid's tightest clamp()'d width,
   so anything that widens it risked overflow. Same fill/border colors as
   .slot-btn.taken already uses, just patterned instead of flat — no new
   color introduced. */
.kiosk-room .slot-btn.taken {
  background-image: repeating-linear-gradient(
    45deg,
    var(--color-booked) 0,
    var(--color-booked) 6px,
    var(--color-booked-border) 6px,
    var(--color-booked-border) 7px
  );
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(16, 24, 40, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  z-index: 100;
}

.modal {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  width: 100%;
  max-width: 420px;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: var(--shadow-md);
}

.modal-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  margin-top: 1rem;
}

/* Lets a caller of showConfirm() (confirmDialog.js) put a "\n" in its
   message and get an actual line break — plain textContent otherwise
   collapses it to a space, same as any other whitespace. */
#confirm-dialog-message {
  white-space: pre-line;
}

.hidden { display: none !important; }

/* Sign-in page */

.login-page {
  background: linear-gradient(180deg, var(--color-bg) 0%, #eceffa 100%);
}

.login-split {
  /* Minus the fixed footer's height — otherwise this flex-centered card
     can render tall enough that its own bottom edge lands underneath the
     footer's fixed overlay (padding-bottom on body only reserves scroll
     room, it doesn't stop centered content from rendering behind a fixed
     element in the first place). */
  min-height: calc(100vh - var(--footer-height));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.5rem;
}

.login-branding {
  flex: 1;
  max-width: 440px;
  text-align: center;
  padding-inline-end: 3rem;
}

.login-logo {
  max-width: 180px;
  width: 100%;
  height: auto;
  margin-bottom: 1.5rem;
}

.login-branding h1 {
  font-size: 1.5rem;
  line-height: 1.35;
}

.login-divider-col {
  width: 1px;
  align-self: stretch;
  background: var(--color-border);
}

.login-form-panel {
  width: 100%;
  max-width: 380px;
  padding-inline-start: 3rem;
}

.login-form-panel .card {
  margin-bottom: 0;
}

.lang-toggle-fixed {
  position: fixed;
  top: 1rem;
  inset-inline-end: 1rem;
  z-index: 20;
}

@media (max-width: 860px) {
  .login-split {
    flex-direction: column;
    padding: 2.5rem 1.5rem;
    gap: 1.5rem;
  }
  .login-branding {
    /* flex: 1 (set above for the desktop row layout, where it grows the
       branding column horizontally next to the fixed-width card) carries
       over here unless reset — but in this column layout that same rule
       grows it along the now-vertical main axis instead, consuming all
       free height and shoving the card (and its sign-in button) down to
       the bottom of the screen. flex: none lets the branding block and
       the card stack at their natural height, so .login-split's
       justify-content: center actually centers the pair on screen. */
    flex: none;
    padding-inline-end: 0;
    max-width: 100%;
  }
  .login-logo {
    max-width: 120px;
    margin-bottom: 1rem;
  }
  .login-branding h1 {
    font-size: 1.25rem;
  }
  .login-divider-col {
    display: none;
  }
  .login-form-panel {
    padding-inline-start: 0;
  }
}

.divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.25rem 0;
  color: var(--color-muted);
  font-size: 0.85rem;
}
.divider::before, .divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--color-border);
}

/* Admin weekly calendar */

.week-toolbar {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-bottom: 1.25rem;
}

.week-toolbar-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
}

.week-toolbar-row.center-row {
  justify-content: center;
  align-items: flex-end;
  /* iOS Safari's native <input type="date"> doesn't reliably respect a
     max-width when it's a flex item free to size from its own content —
     confirmed live on iPhone: it can render wider than its box and
     visually overlap the Today button next to it instead of the row
     wrapping cleanly. Forcing no-wrap + min-width: 0 on the date field
     (below) makes it actually shrink to whatever space is left after the
     button, instead of overflowing into it. */
  flex-wrap: nowrap;
}

.week-toolbar-row.center-row .date-field {
  /* flex-grow: 0 (not 1) — this pair should stay a compact, centered unit
     (via the row's justify-content: center) rather than the date field
     stretching to consume all leftover row width, which pinned the two
     elements to opposite edges of the card with only the bare `gap`
     between them. flex-shrink stays enabled (with min-width: 0) so the
     field can still shrink instead of overflowing past the Today button
     on narrow screens — see the iOS Safari comment above. */
  flex: 0 1 auto;
  min-width: 0;
}

.week-toolbar-row.center-row #today-btn {
  flex-shrink: 0;
  /* A plain margin, not the row's flex `gap` — flexbox gap isn't
     supported on older iOS Safari (pre-14.5, still in use on some
     iPhones), so a gap-only fix silently collapses to zero spacing on
     those devices while flex-grow/shrink (much older CSS) still work.
     margin-inline-start works everywhere and stays correct under RTL. */
  margin-inline-start: 1.25rem;
}

.week-toolbar .form-field {
  margin-bottom: 0;
}

.week-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.week-nav .btn {
  min-height: 38px;
  padding: 0.4rem 0.8rem;
}

.week-range-heading {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--color-text);
}

.week-grid-wrap {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0.5rem;
}

.week-table {
  width: 100%;
  min-width: 760px;
  border-collapse: collapse;
}

.week-hour-col {
  width: 64px;
  min-width: 64px;
  text-align: end;
  padding: 0.35rem 0.6rem;
  font-size: 0.75rem;
  color: var(--color-muted);
  white-space: nowrap;
  border-inline-end: 1px solid var(--color-border);
  position: sticky;
  inset-inline-start: 0;
  background: var(--color-surface);
}

.week-day-head {
  text-align: center;
  padding: 0.6rem 0.4rem;
  border-bottom: 2px solid var(--color-border);
  min-width: 96px;
}

.week-day-head.is-today {
  background: var(--color-primary-tint);
  border-bottom-color: var(--color-primary);
}

.week-day-name {
  font-size: 0.72rem;
  color: var(--color-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.week-day-num {
  font-size: 1.15rem;
  font-weight: 700;
}

.week-day-status {
  font-size: 0.68rem;
  margin-top: 0.15rem;
}

.week-cell {
  height: 40px;
  border: 1px solid var(--color-border);
  text-align: center;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0 0.35rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 140px;
}

.week-cell.free {
  background: var(--color-free);
  cursor: pointer;
  color: transparent;
}
.week-cell.free:hover {
  background: var(--color-primary-tint);
  color: var(--color-primary);
}
.week-cell.free:hover::after {
  content: "+ Book";
}

.week-cell.taken {
  background: var(--color-booked);
  color: #7a1f1f;
  cursor: pointer;
}
.week-cell.taken:hover { filter: brightness(0.96); }

.week-cell.closed {
  background: repeating-linear-gradient(
    45deg,
    var(--color-bg),
    var(--color-bg) 6px,
    #eceef2 6px,
    #eceef2 12px
  );
  cursor: default;
}

/* Language toggle + capacity UI */

.lang-toggle-btn {
  min-height: 36px;
  padding: 0.35rem 0.75rem;
  font-size: 0.85rem;
}

.room-capacity {
  font-weight: 600;
  margin-top: -0.25rem;
  text-align: center;
}

.room-card-header {
  flex-wrap: wrap;
}

.room-capacity-editor {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.room-capacity-editor label {
  font-size: 0.8rem;
}

.rooms-summary-panel {
  margin-bottom: var(--gap);
}

.rooms-summary-heading {
  margin-top: 0;
  font-size: 1.05rem;
}

.rooms-summary-wrap {
  overflow-x: auto;
}

.rooms-summary-table {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
}

.rooms-summary-room-col {
  text-align: start;
  font-weight: 600;
  white-space: nowrap;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--color-border);
  position: sticky;
  inset-inline-start: 0;
  background: var(--color-surface);
  cursor: pointer;
}

.rooms-summary-row:hover .rooms-summary-room-col {
  background: var(--color-bg);
}

.rooms-summary-row.selected .rooms-summary-room-col {
  background: var(--color-primary-tint);
  color: var(--color-primary-dark);
}

.rooms-summary-count {
  border: 1px solid var(--color-border);
  text-align: center;
  padding: 0.5rem 0.4rem;
  font-weight: 700;
  cursor: pointer;
  min-width: 56px;
  height: 44px;
}

/* Same visual language as .week-cell.free/.closed below, so this matrix
   reads the same way at a glance. */
.rooms-summary-count.open {
  background: var(--color-free);
}
.rooms-summary-count.open:hover {
  background: var(--color-primary-tint);
}

.rooms-summary-count.zero {
  font-weight: 400;
  color: var(--color-muted);
}

.rooms-summary-count.closed {
  background: repeating-linear-gradient(
    45deg,
    var(--color-bg),
    var(--color-bg) 6px,
    #eceef2 6px,
    #eceef2 12px
  );
}
.rooms-summary-count.closed:hover {
  filter: brightness(0.97);
}

.rooms-layout {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  align-items: flex-start;
}

@media (min-width: 640px) {
  .rooms-layout {
    flex-direction: row;
  }
}

.rooms-sidebar {
  flex: 0 0 auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

@media (min-width: 640px) {
  .rooms-sidebar {
    flex: 0 0 240px;
  }
}

.room-list-item {
  display: block;
  width: 100%;
  text-align: start;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.room-list-item:not(.selected):hover {
  background: var(--color-bg);
}

.room-list-item.selected {
  background: var(--color-primary-tint);
  border-color: var(--color-primary);
  color: var(--color-primary-dark);
}

.room-detail {
  flex: 1 1 0;
  min-width: 0;
  width: 100%;
}

.calendar-detail {
  flex: 1 1 0;
  min-width: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
}

/* Scoped to calendar.html's usage only (not admin/rooms.html's own
   .rooms-layout, which keeps the shared default below) — the room list and
   the week grid should read as one matched pair now that the toolbar sits
   above both, so the shorter sidebar card stretches to the grid's height
   instead of stopping at its own content. */
.calendar-rooms-layout {
  align-items: stretch;
}

.calendar-detail .week-grid-wrap {
  flex: 1;
  min-height: 0;
}

.room-capacity-input {
  width: 72px;
}

.room-name-input {
  flex: 1;
  min-width: 160px;
  max-width: 320px;
  font-size: 1.05rem;
  font-weight: 700;
  border-color: transparent;
  background: transparent;
  padding: 0.3rem 0.5rem;
}
.room-name-input:hover {
  border-color: var(--color-border);
}

/* Activity log */

.log-table {
  min-width: 900px;
}

.log-table th, .log-table td {
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.log-severity-badge {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  border: 1px solid transparent;
  line-height: 1.4;
}

.log-severity-low {
  background: var(--color-sev-low);
  border-color: var(--color-sev-low-border);
  color: var(--color-sev-low-border);
}
.log-severity-medium {
  background: var(--color-sev-medium);
  border-color: var(--color-sev-medium-border);
  color: var(--color-sev-medium-border);
}
.log-severity-high {
  background: var(--color-sev-high);
  border-color: var(--color-sev-high-border);
  color: var(--color-sev-high-border);
}

/* Tints the whole row, not just the badge cell, so a blocked attempt reads
   at a glance while scanning the table instead of requiring every row's
   severity cell to be read individually. */
.log-row-severity-low > td { background: var(--color-sev-low-row); }
.log-row-severity-medium > td { background: var(--color-sev-medium-row); }
.log-row-severity-high > td { background: var(--color-sev-high-row); }

/* Print / Save as PDF (admin weekly calendar) */

.print-only {
  display: none;
}

@media print {
  .topbar, .week-toolbar, .rooms-sidebar, .rooms-summary-panel, #modal-backdrop {
    display: none !important;
  }

  .print-only {
    display: block !important;
    margin: 0 0 1rem;
  }

  .container.wide {
    max-width: 100%;
    padding: 0;
  }

  .week-grid-wrap {
    border: none;
    box-shadow: none;
    overflow: visible;
    padding: 0;
  }

  .week-table {
    min-width: 0;
    width: 100%;
  }

  .week-hour-col {
    position: static;
  }
}

/* Analytics (admin/analytics.html) */

.timeframe-row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.timeframe-btn {
  background: var(--color-surface);
}

.timeframe-btn[aria-pressed="true"] {
  background: var(--color-primary);
  border-color: var(--color-primary-dark);
  color: #fff;
}

.charts-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
  margin-top: var(--gap);
}

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

.chart-card h2 {
  margin: 0 0 0.75rem;
}

.chart-container {
  position: relative;
}

.chart-svg {
  display: block;
  width: 100%;
  height: auto;
}

.chart-baseline {
  stroke: var(--color-border);
  stroke-width: 1;
}

.chart-axis-label {
  fill: var(--color-muted);
  font-size: 10px;
}

.chart-bar {
  fill: var(--color-primary);
  transition: fill 0.1s ease;
  cursor: pointer;
}

.chart-bar.is-hovered, .chart-bar:focus-visible {
  fill: var(--color-primary-dark);
}

/* The fill-color change above (~1.3:1 between the two blues) is too subtle
   on its own to count as a visible keyboard-focus indicator — this outline
   (~8.2:1 against a white chart background) is the part that actually
   satisfies the 3:1 focus-indicator requirement. */
.chart-bar:focus-visible {
  outline: 3px solid var(--color-primary-dark);
  outline-offset: 1px;
}

.chart-line {
  stroke: var(--color-primary);
  stroke-width: 2.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.chart-area {
  fill: var(--color-primary-tint);
  stroke: none;
}

.chart-point {
  fill: var(--color-primary);
  stroke: var(--color-surface);
  stroke-width: 1.5;
  transition: r 0.1s ease, fill 0.1s ease;
}

.chart-point.is-hovered {
  fill: var(--color-primary-dark);
  r: 5.5;
}

/* Invisible, generously-sized hit target over each 3.5px dot — picking a
   point by touch would otherwise mean hitting a few pixels. */
.chart-point-hit {
  fill: transparent;
  cursor: pointer;
}

/* Same reasoning as .chart-bar:focus-visible — the dot's own color/size
   change is too subtle to serve as the keyboard focus indicator. */
.chart-point-hit:focus-visible {
  outline: 3px solid var(--color-primary-dark);
  outline-offset: 1px;
  border-radius: 50%;
}

.chart-tooltip {
  position: absolute;
  transform: translate(-50%, -100%) translateY(-8px);
  background: var(--color-text);
  color: #fff;
  padding: 0.4rem 0.6rem;
  border-radius: 8px;
  font-size: 0.8rem;
  line-height: 1.3;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: var(--shadow-sm);
  z-index: 5;
}

.chart-tooltip strong {
  font-size: 0.95rem;
}

.chart-total {
  margin-top: 0.75rem;
  font-weight: 600;
}

/* Announcements ticker (book.html, display.html) */

/* book.html isn't pinned to one viewport height like the kiosk page, so the
   ticker floats fixed at the bottom over whatever's scrolled beneath it.
   .kiosk-page below overrides this back to a normal flex child instead,
   since that page already has a fixed-height flex-column budget the ticker
   can just take a slice of. */
.announcements-ticker {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  height: var(--ticker-height);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--color-primary-tint);
  border-top: 2px solid var(--color-primary);
  box-shadow: 0 -2px 10px rgba(16, 24, 40, 0.1);
  padding: 0 0.75rem;
}

.announcements-ticker.hidden {
  display: none;
}

/* Stacked on top of the fixed footer's own reserved space (see
   body:not(.kiosk-page) above) — both the footer and the ticker are
   fixed to the bottom of book.html at once when the ticker has content. */
body.has-announcements-ticker:not(.kiosk-page) {
  padding-bottom: calc(var(--footer-height) + var(--ticker-height));
}

/* Same "floats as a white pill over the footer bar" technique as
   book.html's .book-page .announcements-ticker (still position: fixed from
   the base .announcements-ticker rule — only overridden here, not
   reset back to a static in-flow bar) — not a second dedicated strip below
   the footer any more. Sized up for the kiosk's TV scale and always wide
   enough that "Powered by" never needs to stand down for it, unlike
   book.html's own <700px phone case: a kiosk screen has the width to keep
   both visible, with the pill sitting between the attribution and the
   campus mark. */
body.kiosk-page .announcements-ticker {
  left: 0;
  right: 0;
  width: min(56%, 1100px);
  margin-inline: auto;
  height: 3rem;
  bottom: calc((var(--footer-height) - 3rem) / 2);
  z-index: 41;
  background: #fff;
  border-top: none;
  border-radius: 999px;
  box-shadow: 0 2px 14px rgba(8, 33, 79, 0.3);
  padding: 0 0.6rem;
  gap: 0.6rem;
}

body.kiosk-page .ticker-track {
  color: var(--bp-navy);
  font-size: 1.4rem;
  font-weight: 700;
}

body.kiosk-page .ticker-pause-btn {
  min-width: 2.4rem;
  min-height: 2.4rem;
  border-radius: 50%;
  background: var(--bp-navy);
  border-color: var(--bp-navy);
  color: #fff;
  font-size: 1.1rem;
}

.ticker-pause-btn {
  flex-shrink: 0;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius);
  min-width: 2.25rem;
  min-height: 2.25rem;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.ticker-viewport {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  overflow: hidden;
}

/* Plain single-pass marquee: one copy of the text, sliding from fully
   off-screen at the left to fully off-screen at the right, then looping
   back to the start — a full sweep every cycle, not a partial one, so each
   repeat looks the same instead of jumping mid-cycle. A plain "left"
   animation (not a logical/RTL-aware property) deliberately keeps the
   scroll direction always left-to-right on screen regardless of the page's
   dir="rtl" — that's the specific direction asked for despite the Hebrew
   text, so it can't be left to flip with direction. */
.ticker-track {
  position: absolute;
  top: 50%;
  left: -100%;
  transform: translateY(-50%);
  white-space: nowrap;
  font-weight: 600;
  color: var(--color-primary-dark);
  animation: ticker-scroll 25s linear infinite;
}

.announcements-ticker.paused .ticker-track {
  animation-play-state: paused;
}

@keyframes ticker-scroll {
  from {
    left: -100%;
  }
  to {
    left: 100%;
  }
}

/* WCAG 2.3.3 (AAA, but cheap to honor): skip the motion entirely for anyone
   who's asked their OS to reduce it, same as the pause button covers WCAG
   2.2.2 for everyone else. */
@media (prefers-reduced-motion: reduce) {
  .ticker-track {
    animation: none;
    left: 0.5rem;
  }
}

/* Book page redesign (book.html) — deliberately scoped under body.book-page
   throughout, rather than restyling .card/.room-card/.slot-btn/.legend
   globally, because display.js (kiosk board) and admin-rooms.js reuse those
   same class names for their own unrelated pages. */

/* Shared navy/blue design tokens — hoisted to :root (rather than kept
   book.html-only) so the same header/footer "chrome" classes below
   (.book-header, .book-bands, .book-footer, etc. — already unscoped) can be
   reused verbatim on index.html and display.html. Page-specific sizing
   tokens (--bp-band, --footer-height) stay scoped per page instead, since
   each page's content column needs its own values. */
:root {
  --bp-navy-deep: #071d47;
  --bp-navy: #0f2f6e;
  --bp-blue: #1b5fc4;
  --bp-blue-light: #2f86f0;
  --bp-teal: #17b0a7;
  --bp-card-border: #e5ecf7;
  --bp-shadow: 0 8px 24px rgba(12, 40, 92, 0.09);
  --bp-slot-bg: #eaf7ee;
  --bp-slot-border: #b7ddc4;
  --bp-slot-text: #1f2b3a;
  --bp-mine-bg: #fff3e0;
  --bp-mine-border: #f0d5ac;
  --bp-mine-text: #96590f;
  --bp-taken-bg: #fdecea;
  --bp-taken-border: #f3c6c1;
  --bp-taken-text: #ad3f39;
  --bp-closed-bg: #e8e8f3;
  /* Was #979db2 — only 2.22:1 against --bp-closed-bg, well under WCAG
     1.4.3's 4.5:1 floor (this labels a real, meaningful time slot like
     "18:00", not decorative text). This shade clears ~5.4:1. */
  --bp-closed-text: #565d70;
  /* Line-art calendar, used as a mask so the fill color stays in CSS. */
  --bp-cal-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M8 3v4M16 3v4M3 10h18'/%3E%3C/svg%3E");
}

body.book-page {
  /* Meant to reach down to just above the room-name tabs, but 19.5rem
     fell well short of that once the "בחירת חדר ושעה" heading, its
     subtitle, and the legend row are actually accounted for between the
     summary cards and the room grid. */
  --bp-band: 27rem;
  --footer-height: 4rem;
  background: #f7fafe;
  min-height: 100vh;
  font-family: "Assistant", system-ui, -apple-system, "Segoe UI", sans-serif;
}

html:has(body.book-page) {
  background: #f7fafe;
}

/* Header ---------------------------------------------------------------- */

.book-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: linear-gradient(100deg, var(--bp-navy-deep) 0%, #0c2a63 40%, #123c86 66%, #1a55b5 100%);
  box-shadow: 0 2px 14px rgba(8, 33, 79, 0.28);
  overflow: hidden;
}

/* Skewed brand bands, shared by the header and the footer bar. Real sibling
   strips overlapping by 1.5px rather than hard gradient stops — a transformed
   gradient antialiases its stops and leaves hairline seams between colors. */
.book-bands {
  position: absolute;
  display: flex;
  transform: skewX(-17deg);
  pointer-events: none;
}

.book-bands i {
  display: block;
  margin-inline-end: -1.5px;
}

.book-bands i:nth-child(1) { flex: 1.15 1 0; background: #212b78; }
.book-bands i:nth-child(2) { flex: 1 1 0; background: #33409e; }
.book-bands i:nth-child(3) { flex: 0.85 1 0; background: #3a9fc4; }
.book-bands i:nth-child(4) { flex: 1.05 1 0; background: #16c0c0; }
.book-bands i:nth-child(5) { flex: 0.95 1 0; background: #2f7de0; }
.book-bands i:nth-child(6) { flex: 0.8 1 0; background: #23408f; margin-inline-end: 0; }

.book-header .book-bands {
  top: -45%;
  bottom: -45%;
  inset-inline-start: 12.5%;
  width: 24%;
}

.book-header-inner {
  position: relative;
  display: grid;
  /* minmax(0, 1fr), not plain 1fr: a plain 1fr track carries an implicit
     minmax(auto, 1fr), which floors its width at its content's own
     min-content size — on a page with many nav buttons (admin/library
     viewing display.html, or any admin-page header), that stopped the
     actions column from ever shrinking below its full unwrapped row width,
     even though .book-header-actions below already sets flex-wrap: wrap.
     The column itself needs to be allowed to shrink before that wrapping
     can do anything, at "narrower desktop" widths above the 860px
     stacked-layout breakpoint below. */
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  column-gap: 1rem;
  min-height: 3.6rem;
  /* Full-bleed: the wordmark hugs the reading-start edge and the actions the
     opposite one, rather than both sitting inside a centred cap with wide
     empty gutters on a large monitor. */
  padding: 0.4rem 1.4rem;
  max-width: none;
}

.book-header-title {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  color: #fff;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0;
  white-space: nowrap;
  /* Grid items also default to min-width: auto (content-based) — without
     this, the title (an "auto" track, so it never shrinks on its own) could
     still force the whole header wider than the viewport at a narrow-ish
     desktop width. Ellipsis rather than letting it clip abruptly if it
     ever actually gets this tight. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.book-title-icon {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  opacity: 0.95;
}

.book-header-actions {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: center;
  display: flex;
  gap: 0.5rem;
}

/* Glassy pills on the gradient — no white plate, so the bar reads as one
   continuous brand surface. */
.book-header-btn,
.book-header .lang-toggle-btn {
  background: rgba(255, 255, 255, 0.13);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  min-height: 36px;
  padding: 0.3rem 0.95rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
}
.book-header-btn:hover,
.book-header .lang-toggle-btn:hover {
  background: rgba(255, 255, 255, 0.24);
  border-color: rgba(255, 255, 255, 0.55);
  color: #fff;
}

.book-header-icon {
  width: 1rem;
  height: 1rem;
}

.book-header-brand {
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  align-self: center;
}

/* Wordmark set in type for now — swap in an <img class="book-header-logo">
   (white / knockout variant, it sits straight on the navy gradient) when the
   real asset is ready. */
.book-brand-lockup {
  display: block;
  color: #fff;
  text-align: start;
  line-height: 1.15;
}

/* Same weight as the footer's <strong>Extropus</strong> (.book-powered-by
   below, plain bold with no tracking) — this used to be heavier (800) with
   tightened letter-spacing, which read as a distinctly different, denser
   face for Hebrew next to the footer's plain bold wordmark. */
.book-brand-name {
  display: block;
  font-size: 1.2rem;
  font-weight: 700;
}

.book-brand-sub {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  /* Checked against the header gradient's lightest stop (#1a55b5, where
     the wordmark actually sits — grid-column:1/justify-self:start puts it
     at the gradient's 100% end): 0.72 opacity landed at ~4.43:1, just
     under WCAG 1.4.3's 4.5:1 floor for this small (0.72rem) text. 0.85
     clears it with real margin (~5.5:1) there and everywhere darker along
     the gradient. */
  color: rgba(255, 255, 255, 0.85);
}

.book-header-logo {
  height: 2.5rem;
  width: auto;
  display: block;
  background: none;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
}

@media (max-width: 860px) {
  .book-header-inner {
    grid-template-columns: 1fr;
    justify-items: center;
    row-gap: 0.6rem;
    padding: 0.75rem 1rem;
  }
  .book-header-title,
  .book-header-actions {
    grid-column: 1;
    grid-row: auto;
    justify-self: center;
  }

  /* The "האקדמית צפת / המרכז האקדמי של הצפון" wordmark is wide enough on
     its own line to force the header noticeably taller on a phone — dropped
     rather than stacked, since the header title already carries the page's
     identity at this width. */
  .book-header-brand {
    display: none;
  }
}

/* Hero band + summary cards ---------------------------------------------- */

body.book-page {
  display: flex;
  flex-direction: column;
}

body.book-page .book-content {
  position: relative;
  isolation: isolate;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  /* Both floors matter: as a flex item the column would otherwise size to its
     max-content contribution (the 8-card room row) instead of the viewport
     line, and its own flex children would carry that width down. */
  width: 100%;
  min-width: 0;
  padding: 0;
}

body.book-page .book-content > * {
  min-width: 0;
}

/* The pale band and the campus photo run from under the header all the way
   down to just above the room-name tabs — --bp-band is that depth, and both
   layers sit behind the content (z-index: -1 inside the isolated stacking
   context above). */
body.book-page .book-content::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: var(--bp-band);
  background: linear-gradient(197deg, #cfe4fb 0%, #dceefc 22%, #e8f3fd 46%, #f2f8fe 72%, #f7fafe 100%);
  /* The lower edge is an arc, not a straight line: the wash reaches deepest
     over the campus photo and sweeps upward across the page. */
  -webkit-mask-image:
    radial-gradient(135% 118% at 4% 104%, #000 0 58%, rgba(0, 0, 0, 0.55) 76%, transparent 94%),
    linear-gradient(to bottom, #000 0 62%, rgba(0, 0, 0, 0.45) 84%, transparent 100%);
  mask-image:
    radial-gradient(135% 118% at 4% 104%, #000 0 58%, rgba(0, 0, 0, 0.55) 76%, transparent 94%),
    linear-gradient(to bottom, #000 0 62%, rgba(0, 0, 0, 0.45) 84%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  z-index: -1;
  pointer-events: none;
}

/* Pale blue band the two summary cards float on, with the campus photo
   bleeding off the reading-end edge and dissolving into the gradient.
   Drop the building shot at /images/campus.jpg (any wide crop) — with no
   file there the band simply stays a clean gradient. */
.book-hero {
  padding: 1.4rem 1rem 1.25rem;
}

.book-hero-photo {
  position: absolute;
  top: 0;
  height: var(--bp-band);
  inset-inline-end: 0;
  /* Wide and faded-late enough that the photo visibly reaches under the
     date card on its far edge and down into the room-name tabs at the
     band's own bottom, rather than dissolving away well short of both. */
  width: min(62%, 980px);
  z-index: -1;
  background-image: url("/images/campus.jpg");
  background-size: cover;
  background-position: center;
  opacity: 0.5;
  filter: saturate(0.5) brightness(1.05);
  -webkit-mask-image:
    linear-gradient(to left, transparent 0%, rgba(0, 0, 0, 0.4) 62%, #000 100%),
    linear-gradient(to bottom, #000 0 72%, rgba(0, 0, 0, 0.4) 92%, transparent 100%);
  mask-image:
    linear-gradient(to left, transparent 0%, rgba(0, 0, 0, 0.4) 62%, #000 100%),
    linear-gradient(to bottom, #000 0 72%, rgba(0, 0, 0, 0.4) 92%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  pointer-events: none;
}

html[dir="ltr"] .book-hero-photo {
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.4) 62%, #000 100%),
    linear-gradient(to bottom, #000 0 72%, rgba(0, 0, 0, 0.4) 92%, transparent 100%);
  mask-image:
    linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.4) 62%, #000 100%),
    linear-gradient(to bottom, #000 0 72%, rgba(0, 0, 0, 0.4) 92%, transparent 100%);
}

/* Deliberately unequal columns: My Bookings carries three labelled values
   plus a status chip per row, the date card only a field and a button. */
body.book-page .book-hero .book-summary-grid {
  position: relative;
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: 1.25rem;
  max-width: 860px;
  margin: 0 auto;
}

@media (max-width: 720px) {
  body.book-page {
    --bp-band: 36rem;
  }
  body.book-page .book-hero .book-summary-grid {
    grid-template-columns: 1fr;
  }
}

body.book-page .book-summary-card {
  margin-bottom: 0;
  border: 1px solid var(--bp-card-border);
  border-top: 1px solid var(--bp-card-border);
  border-radius: 14px;
  padding: 0.85rem 1.1rem 1rem;
  box-shadow: var(--bp-shadow);
}

body.book-page .book-summary-title {
  justify-content: flex-start;
  gap: 0.45rem;
  margin: 0;
  color: var(--bp-navy);
  font-size: 1rem;
  font-weight: 700;
}

body.book-page .book-date-row {
  justify-content: flex-start;
  align-items: center;
  margin-top: 0.7rem;
}

body.book-page .book-date-row .book-date-field {
  flex: 1 1 auto;
  min-width: 0;
  margin-bottom: 0;
}

body.book-page .book-date-field input {
  border-radius: 10px;
  border-color: #cddcf0;
  text-align: center;
  font-weight: 600;
  font-size: 0.9rem;
  padding: 0.4rem 0.6rem;
  color: var(--bp-navy);
}

body.book-page .book-date-row .book-today-btn {
  margin-inline-start: 0.6rem;
  border-radius: 999px;
  background: #dbe7f7;
  border-color: transparent;
  color: #17407e;
  font-size: 0.8rem;
  font-weight: 700;
  min-height: 34px;
  padding: 0.25rem 0.95rem;
}
body.book-page .book-date-row .book-today-btn:hover {
  background: #c7dbf5;
  border-color: transparent;
  color: #123a8a;
}

/* Was plain .muted grey at 0.78rem — read as an afterthought rather than
   the "how much of your weekly/daily allowance is left" figure it
   actually is, so it's now a bold, navy, boxed line instead of blending
   into the card's other small print. */
body.book-page .book-weekly-count {
  display: block;
  text-align: center;
  margin: 0.75rem 0 0;
  padding: 0.5rem 0.75rem;
  border-radius: 10px;
  background: #eaf1fc;
  color: var(--bp-navy);
  font-size: 0.85rem;
  font-weight: 700;
}

/* MY_BOOKINGS_PAGE_SIZE (book.js) already caps how many rows render at
   once, so this max-height is a defensive backstop against a stray extra
   row overflowing past the card's edge into the room grid below it — the
   bug that pagination itself was added to fix — not the thing doing the
   capping. */
body.book-page .book-bookings-list {
  margin-top: 0.5rem;
  max-height: 230px;
  overflow: hidden;
}

/* Prev / "page X of Y" / next — shown only once My Bookings has more than
   one page (see renderMyBookingsPager in book.js). Same round-button shape
   as .carousel-nav-btn, just smaller to match this card's own scale. */
.mybookings-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.mybookings-pager[hidden] {
  display: none;
}

.mybookings-pager-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--bp-card-border);
  background: var(--bp-blue);
  border-color: var(--bp-blue);
  color: #fff;
  cursor: pointer;
}
.mybookings-pager-btn:hover:not(:disabled) {
  background: var(--bp-navy);
  border-color: var(--bp-navy);
}
.mybookings-pager-btn:disabled {
  background: var(--bp-closed-bg);
  border-color: var(--bp-card-border);
  color: var(--bp-closed-text);
  cursor: not-allowed;
}
.mybookings-pager-btn svg {
  width: 16px;
  height: 16px;
}
html[dir="rtl"] .mybookings-pager-btn svg {
  transform: scaleX(-1);
}

.mybookings-pager-label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--bp-navy);
  min-width: 6.5rem;
  text-align: center;
}

body.book-page .book-bookings-list .list-row {
  border-bottom: none;
  padding: 0.3rem 0;
  gap: 0.6rem;
  font-size: 0.85rem;
}

/* On a narrow card the three labelled fields wrap onto two lines and the
   status pill/Cancel button sits beside that whole wrapped block — with no
   border, an entry that wraps reads as ambiguous about which pill belongs
   to which row once more than one entry is stacked. A single line per row
   doesn't have that problem, so the divider stays off above this width. */
@media (max-width: 639px) {
  body.book-page .book-bookings-list .list-row {
    border-bottom: 1px solid var(--bp-card-border);
    padding-block: 0.5rem;
  }
  body.book-page .book-bookings-list .list-row:last-child {
    border-bottom: none;
  }
}

/* Thin separators between the three labelled values of a row, matching the
   inline "room | date | hour" reading rhythm. */
body.book-page .booking-field:not(:last-child)::after {
  content: "|";
  color: #c8d3e2;
  margin-inline-start: 0.55rem;
  font-weight: 400;
}

/* Room names vary in length while date/hour are always the same fixed
   format (DD-MM-YYYY / HH:00), so without a reserved width the room field
   was the only one whose separator landed in a different spot from row to
   row. Fixing its width lines every row's separators up in the same
   columns, like a table.
   Deliberately modest (not sized to the longest plausible room name) —
   this card is narrow enough that a wider reservation pushed the whole
   row past its available width and forced every field onto its own line
   (worse than the original misalignment). This covers the difference
   between short and typical room names; a longer name than the reserved
   width degrades gracefully back to its own natural width, same as
   before this fix. */
body.book-page .booking-field:nth-child(1) .booking-field-value {
  display: inline-block;
  min-width: 2.5rem;
}

body.book-page .booking-fields {
  gap: 0.15rem 0.55rem;
}

body.book-page .booking-field-label {
  font-size: 0.78rem;
  font-weight: 400;
}

body.book-page .booking-field-value {
  color: var(--bp-navy);
}

/* Row-ending status chips — both read as the same neutral "nothing to do
   here" grey used everywhere else in the app (see .booking-note /
   .booking-note-cancelled above), just reshaped into the pill style this
   page uses for its other chips/badges. */
body.book-page .booking-note {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-muted);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.2rem 0.65rem;
}
body.book-page .booking-note-cancelled {
  background: #eef1f6;
  border-color: #dde3ec;
  color: #5b6472;
}
/* Used to grey these out to de-emphasize a cancelled entry, but that read
   as an inconsistency/bug (some rows blue, some grey) rather than an
   intentional distinction — the grey .booking-note-cancelled pill already
   says "cancelled" on its own, so the fields themselves now match every
   other row's navy. */
body.book-page .list-row.is-cancelled .booking-field-value {
  text-decoration: none;
}

/* Reshaped into the same pill as everywhere else on this page — colour is
   deliberately left alone so .btn-danger's red still wins for Cancel (the
   only button this list ever renders); a color here would outrank it on
   specificity and repaint Cancel the wrong color. Sized to match
   .booking-note/.booking-note-cancelled exactly (same font-size/padding)
   rather than the global .btn's 44px min-height, which otherwise made
   Cancel visibly taller than the plain-text status pills next to it. */
body.book-page .book-bookings-list .btn-sm {
  border-radius: 999px;
  min-height: 0;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.2rem 0.65rem;
}

/* Room + hour picker ------------------------------------------------------ */

body.book-page .book-rooms-section,
body.book-page #live-fallback-note,
body.book-page #form-error,
body.book-page #status-text {
  padding-inline: 1.1rem;
}

body.book-page .book-rooms-section > h2 {
  margin: 1.5rem 0 0.4rem;
  font-size: 1.6rem;
  font-weight: 800;
  color: var(--bp-navy);
}

body.book-page .book-rooms-section > p.muted {
  margin: 0;
  font-size: 0.85rem;
}

body.book-page .legend {
  gap: 0.7rem 1.6rem;
  margin: 0.9rem 0 1.4rem;
}

body.book-page .legend-item {
  font-size: 0.82rem;
  gap: 0.45rem;
}

body.book-page .legend-swatch {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border-width: 1px;
}
body.book-page .legend-swatch-free {
  background: #34a853;
  border-color: #2b8c46;
}
body.book-page .legend-swatch-mine {
  background: #f5b93b;
  border-color: #d99a1c;
}
body.book-page .legend-swatch-taken {
  background: #ef5350;
  border-color: #d8403d;
}
body.book-page .legend-swatch-closed {
  background: #fff;
  border-color: #c3cbd9;
}

body.book-page .rooms-grid {
  gap: 1.3rem;
  padding-block-end: 0.5rem;
}

body.book-page .room-card {
  padding: 0;
  border: none;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 6px 20px rgba(12, 40, 92, 0.1);
  overflow: visible;
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}

/* The room name and its capacity line together form one navy panel whose
   bottom corners curve back in, so the white card reads as a sheet tucked
   under a labelled tab. */
body.book-page .room-card h3 {
  position: relative;
  margin: 0;
  padding: 0.75rem 0.9rem 0.1rem;
  background: linear-gradient(105deg, var(--bp-navy) 0%, var(--bp-blue) 100%);
  border-radius: 18px 18px 0 0;
  color: #fff;
  font-size: 1rem;
  font-weight: 700;
  text-align: center;
}

body.book-page .room-capacity {
  /* Overlaps the title block by a pixel: two stacked elements sharing one
     near-horizontal gradient otherwise leave a hairline of card white where
     their edges round off. */
  margin: -1px 0 0;
  padding: 1px 0.75rem 0.7rem;
  background: linear-gradient(105deg, var(--bp-navy) 0%, var(--bp-blue) 100%);
  /* Elliptical, not a plain corner radius: a broad shallow arc sweeping the
     full width, so the panel reads as a curved tab over the white sheet. */
  border-radius: 0 0 46% 46% / 0 0 26px 26px;
  color: rgba(255, 255, 255, 0.78);
  font-size: 0.75rem;
  font-weight: 600;
  text-align: center;
}

body.book-page .room-card .slot-grid,
body.book-page .room-card > p.muted.text-center {
  padding: 0.7rem 0.7rem 0.85rem;
  margin-top: 0;
  gap: 0.45rem;
}

body.book-page .slot-btn {
  min-height: 36px;
  border-radius: 9px;
  border: 1px solid var(--bp-slot-border);
  background: var(--bp-slot-bg);
  color: var(--bp-slot-text);
  font-size: 0.85rem;
  font-weight: 600;
  gap: 0.35rem;
}

body.book-page .slot-btn[aria-pressed="true"] {
  background: var(--bp-blue-light);
  border-color: var(--bp-blue-light);
  color: #fff;
  box-shadow: 0 2px 8px rgba(47, 134, 240, 0.4);
}

body.book-page .slot-btn.taken {
  background: var(--bp-taken-bg);
  border-color: var(--bp-taken-border);
  color: var(--bp-taken-text);
}
body.book-page .slot-btn.mine {
  background: var(--bp-mine-bg);
  border-color: var(--bp-mine-border);
  color: var(--bp-mine-text);
}
body.book-page .slot-btn.closed {
  background: var(--bp-closed-bg);
  border-color: transparent;
  color: var(--bp-closed-text);
}

body.book-page .carousel-dot[aria-current="true"]::before {
  background: var(--bp-blue-light);
  border-color: var(--bp-blue);
}

/* Footer bar + accessibility pill ---------------------------------------- */

/* One navy bar across the bottom: attribution at the reading end, and the
   announcements ticker laid transparently over its reading-start half (see
   the ticker overrides below) so both live on a single strip instead of
   stacking into two. */
.book-footer {
  justify-content: flex-end;
  background: #204c9c;
  border-top: none;
  color: rgba(255, 255, 255, 0.85);
  padding-inline: 1.1rem;
  overflow: hidden;
  box-shadow: 0 -2px 14px rgba(8, 33, 79, 0.18);
}

/* Same band set as the header, tucked beside the campus photo at the
   reading-start edge. */
.book-footer .book-bands {
  top: -60%;
  bottom: -60%;
  inset-inline-start: 10.5rem;
  width: min(16%, 15rem);
}

/* Line-drawn campus mark at the bar's reading-start edge — white outline art
   on transparency, so the navy of the bar shows through it. */
.book-footer-photo {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0.6rem;
  width: 9.5rem;
  background-image: url("/images/campus-outline.png");
  background-repeat: no-repeat;
  background-position: center bottom;
  background-size: contain;
  pointer-events: none;
}

.book-powered-by {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.8rem;
}

/* Explicit color (not just `inherit`) on both the link and its visited
   state, and on the <strong> directly — belt-and-suspenders so "Extropus"
   reliably matches "Powered by"'s white and font-size regardless of any
   browser default link styling, rather than depending on inheritance
   alone to fight it off. */
.powered-by-link,
.powered-by-link:visited {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: rgba(255, 255, 255, 0.85);
  font-size: inherit;
  text-decoration: none;
}
.powered-by-link strong {
  color: inherit;
  font-size: inherit;
}
.powered-by-link:hover,
.powered-by-link:focus-visible {
  color: #fff;
  text-decoration: underline;
}

/* em-sized so it scales with .book-powered-by's own font-size (including
   the kiosk-page override below) without a separate rule per context. The
   width/height attributes on the <img> itself are a fallback so it renders
   at a sane size even before this stylesheet applies — logo2.png is a
   1254x1254 source image, huge at its intrinsic size with no constraint. */
.powered-by-logo {
  height: 1em;
  width: 1em;
  flex-shrink: 0;
}

/* Its own centered row at the end of the page flow (not floated over the
   content, which would sit on top of the room cards on any page taller than
   the viewport) — so the statement stays a real, unobstructed target rather
   than small print inside the dark bar. */
/* margin-top: auto keeps it hugging the footer bar on a short page, while a
   long page just lets it follow the room row. */
.book-a11y-row {
  display: flex;
  justify-content: center;
  margin-top: auto;
  padding: 1rem 1rem 0.85rem;
}

.book-accessibility-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 36px;
  padding: 0.3rem 1rem;
  border-radius: 999px;
  background: #fff;
  border: 1px solid var(--bp-card-border);
  box-shadow: 0 4px 14px rgba(12, 40, 92, 0.14);
  color: var(--bp-navy);
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.book-accessibility-link:hover {
  border-color: var(--bp-blue-light);
  color: var(--bp-blue);
}

.book-a11y-icon {
  width: 1.05rem;
  height: 1.05rem;
  color: var(--bp-blue-light);
  flex-shrink: 0;
}

/* The ticker keeps its own element and marquee, but on this page it is
   painted over the footer bar's reading-start half rather than as a second
   fixed strip below it — hence transparent, footer-height, and half-width. */
body.book-page .announcements-ticker {
  left: 0;
  right: 0;
  width: min(68%, 960px);
  margin-inline: auto;
  height: 3rem;
  bottom: calc((var(--footer-height) - 3rem) / 2);
  z-index: 41;
  background: #fff;
  border-top: none;
  border-radius: 999px;
  box-shadow: 0 2px 10px rgba(8, 33, 79, 0.28);
  padding: 0 0.5rem;
  gap: 0.6rem;
}

/* Phone widths can't fit a centred pill and the attribution on the same 3rem
   strip — the pill takes the whole bar and the attribution stands down for as
   long as there is an announcement to show. */
@media (max-width: 700px) {
  body.book-page .announcements-ticker {
    inset-inline: 0.6rem;
    width: auto;
    margin-inline: 0;
  }
  body.book-page.has-announcements-ticker .book-powered-by {
    display: none;
  }

  /* The bands' desktop positioning (fixed rem offsets, up to 15rem wide) is
     sized for a bar hundreds of pixels wide — on a phone-width bar it runs
     straight into the attribution text, so it's dropped rather than fought
     with. The campus line art still fits fine on its own; it only comes out
     alongside the ticker pill below, which needs the same room the art
     would otherwise sit in. */
  .book-footer .book-bands {
    display: none;
  }
  body.book-page.has-announcements-ticker .book-footer-photo {
    display: none;
  }
}

body.book-page.has-announcements-ticker .app-footer {
  bottom: 0;
}

body.book-page.has-announcements-ticker {
  padding-bottom: var(--footer-height);
}

body.book-page .ticker-track {
  color: #16307a;
  font-size: 1rem;
  font-weight: 700;
}

body.book-page .ticker-pause-btn {
  min-width: 2.1rem;
  min-height: 2.1rem;
  border-radius: 50%;
  background: #16307a;
  border-color: #16307a;
  color: #fff;
  font-size: 0.9rem;
  line-height: 1;
}

/* Login page redesign (index.html) — reuses the same .book-header/
   .book-bands/.book-footer/.book-a11y-row "chrome" classes as book.html
   (all unscoped above) rather than rebuilding them, so the two pages read
   as one system. desk-7f3a9c.html deliberately keeps the plain .login-page
   look — it stays out of this scope by not carrying .book-login-page. */

body.book-login-page {
  --footer-height: 4rem;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f7fafe;
  font-family: "Assistant", system-ui, -apple-system, "Segoe UI", sans-serif;
}

html:has(body.book-login-page) {
  background: #f7fafe;
}

/* The old fixed-position lang toggle (see .lang-toggle-fixed, still used by
   desk-7f3a9c.html) moves into the header bar here, and the split panel
   becomes a flex child growing to fill whatever room is left between the
   header and the footer, instead of sizing itself off the viewport height
   directly. It's also a single centered column now (logo/title, then the
   sign-in button beneath) rather than a two-panel split — the divider and
   the form panel's white card are gone from the markup entirely. */
/* justify-content stays "center", but a big reserved chunk of bottom
   padding shrinks the box that's actually being centered within — so the
   logo/title/button cluster lands near the screen's vertical middle
   instead of the middle of the (visually lower-weighted) space below the
   header. */
body.book-login-page .login-split {
  flex: 1 1 auto;
  min-height: 0;
  flex-direction: column;
  gap: 1.75rem;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding-top: 0.5rem;
  padding-bottom: 16vh;
}

/* Same technique as book.html's .book-hero-photo (faded, desaturated,
   mask-fanned out) — a radial fade here instead of the side-panel linear
   one, since this is a single centered column rather than a photo bleeding
   off one edge. */
body.book-login-page .login-hero-photo {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: url("/images/campus.jpg");
  background-size: cover;
  background-position: center 30%;
  opacity: 0.16;
  filter: saturate(0.6) brightness(1.1);
  -webkit-mask-image: radial-gradient(60% 55% at 50% 38%, #000 0%, rgba(0, 0, 0, 0.5) 60%, transparent 100%);
  mask-image: radial-gradient(60% 55% at 50% 38%, #000 0%, rgba(0, 0, 0, 0.5) 60%, transparent 100%);
  pointer-events: none;
}

body.book-login-page .login-branding {
  max-width: 480px;
  padding-inline-end: 0;
}

body.book-login-page .login-branding h1 {
  color: var(--bp-navy);
}

/* No white card behind it any more — a plain pill button standing on its
   own, same blue as the rest of the system's primary actions. */
body.book-login-page .login-signin-btn {
  background: var(--bp-blue);
  border-color: var(--bp-blue);
  border-radius: 999px;
  font-weight: 700;
  width: 100%;
  max-width: 320px;
}
body.book-login-page .login-signin-btn:hover {
  background: var(--bp-navy);
  border-color: var(--bp-navy);
}

/* White (not the usual 4-color) G — the multicolor mark is meant for a
   white button per Google's own brand guidelines; a solid blue button
   wants the plain light variant instead. */
.login-google-icon {
  width: 1.15rem;
  height: 1.15rem;
  flex-shrink: 0;
}

body.book-login-page .book-a11y-row {
  padding: 1rem 1rem 0.5rem;
}

/* desk-7f3a9c.html (staff password sign-in) reuses this whole page's
   layout/chrome, but its card holds a real multi-field form (email,
   password, Turnstile, submit) rather than index.html's single button —
   too tall a cluster for the big reserved bottom padding tuned for that
   shorter one, so it's reset back to a plain centered layout here. */
body.book-login-page .desk-login-split {
  padding-bottom: 2rem;
}

body.book-login-page .desk-login-card {
  width: 100%;
  max-width: 380px;
}

body.book-login-page .desk-login-card .btn-primary {
  background: var(--bp-blue);
  border-color: var(--bp-blue);
  border-radius: 999px;
  font-weight: 700;
}
body.book-login-page .desk-login-card .btn-primary:hover {
  background: var(--bp-navy);
  border-color: var(--bp-navy);
}

/* Kiosk display redesign (display.html) — same reused .book-header/
   .book-bands/.book-footer "chrome" as book.html and index.html, laid over
   the kiosk's existing fixed-viewport flex-column skeleton rather than
   replacing it, since that skeleton (100vh, overflow: hidden, .kiosk-board
   filling whatever's left) is what keeps this page scroll-free on a TV. */

body.kiosk-page {
  --footer-height: 4.5rem;
  font-family: "Assistant", system-ui, -apple-system, "Segoe UI", sans-serif;
  background: #eef2fb;
}

.kiosk-page .book-header,
.kiosk-page .book-footer {
  flex-shrink: 0;
}

/* This page never scrolls (body.kiosk-page is pinned to one viewport height
   with overflow: hidden), so position: sticky has nothing to stick against.
   Swapped for position: relative — NOT static: .book-bands is
   position: absolute and needs an ancestor with a real positioning context
   to size itself against. With sticky (or, worse, static) removed that
   context, .book-bands' percentage top/bottom/width resolved against the
   viewport instead of the header bar, so the diagonal stripes stretched
   down over the whole board instead of staying inside it. relative keeps
   the containing block while still dropping the now-pointless sticky, and
   the explicit height below keeps the percentages resolving against a
   defined value rather than the header's own auto/content-based one. */
.kiosk-page .book-header {
  position: relative;
  height: 3.2rem;
}

.kiosk-page .book-header-inner {
  min-height: 3.2rem;
  padding-block: 0.3rem;
}

.kiosk-page .book-header-title {
  font-size: 1.35rem;
}

.kiosk-page .book-header-actions {
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: 0.35rem;
}

/* The live/offline pill and the admin nav links are plain .btn markup here
   (see display.html), not .book-header-btn — restyled as one group so every
   button in the header actions area reads as the same glassy-on-navy pill
   without editing each tag's classes individually. */
.kiosk-page .book-header-actions .btn {
  background: rgba(255, 255, 255, 0.13);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  border-radius: 999px;
  min-height: 36px;
  padding: 0.3rem 0.95rem;
  font-size: 0.85rem;
  font-weight: 600;
}
.kiosk-page .book-header-actions .btn:hover {
  background: rgba(255, 255, 255, 0.24);
  border-color: rgba(255, 255, 255, 0.55);
  color: #fff;
}
/* Keeps its own larger, TV-legible sizing (.kiosk-page .live-indicator
   above) — only the glassy-on-navy coloring is shared with the nav pills. */
.kiosk-page .live-indicator {
  background: rgba(255, 255, 255, 0.13);
  border-color: rgba(255, 255, 255, 0.28);
  color: #fff;
}
.kiosk-page .live-indicator.is-live {
  background: rgba(34, 197, 94, 0.22);
  border-color: rgba(134, 239, 172, 0.55);
  color: #eafff1;
}
.kiosk-page .live-indicator.is-offline {
  background: rgba(255, 255, 255, 0.13);
  border-color: rgba(255, 255, 255, 0.28);
  color: #fff;
}

.kiosk-page .legend-swatch {
  border-radius: 50%;
}

/* Footer bar — same navy strip as book.html/index.html, and now the same
   --footer-height (4.5rem, see above) rather than a shallower one: this bar
   is always on screen on a TV, not scrolled past on a phone, so there's no
   reason to keep it thin. No accessibility-statement link here — this is
   an unattended kiosk display, not a page anyone reads standing at. */
.kiosk-page .book-footer {
  padding-inline: 0.9rem;
  gap: 0.75rem;
}
.kiosk-page .book-powered-by {
  font-size: 1.05rem;
}

/* Room cards — same navy-gradient tab + pale palette as book.html's
   .room-card/.slot-btn, reimplemented under .kiosk-room/.kiosk-room .slot-btn
   instead: display.js renders plain non-interactive <div>s here (see the
   .kiosk-room .slot-btn.taken comment above), not the buttons book.js
   builds, so the two can't share one selector. */
.kiosk-page .kiosk-room {
  border: none;
  box-shadow: var(--bp-shadow);
  padding: 0;
}

/* Same tab shape as book.html's .room-card h3/.room-capacity pair — flat
   top corners matching the card's own radius, a broad shallow arc across
   the bottom — collapsed into one element since the kiosk only ever shows
   the room name, not a separate capacity line. */
.kiosk-page .kiosk-room h2 {
  margin: 0;
  padding: 0.5rem 0.9rem 0.7rem;
  background: linear-gradient(105deg, var(--bp-navy) 0%, var(--bp-blue) 100%);
  color: #fff;
  border-start-start-radius: var(--radius-lg);
  border-start-end-radius: var(--radius-lg);
  border-end-start-radius: 46% 22px;
  border-end-end-radius: 46% 22px;
  flex-shrink: 0;
}

.kiosk-page .kiosk-room .slot-grid {
  padding: 0.6rem 0.7rem 0.7rem;
}

.kiosk-page .kiosk-room .slot-btn {
  background: var(--bp-slot-bg);
  border: 1px solid var(--bp-slot-border);
  color: var(--bp-slot-text);
}

.kiosk-page .kiosk-room .slot-btn.taken {
  background-image: repeating-linear-gradient(
    45deg,
    var(--bp-taken-bg) 0,
    var(--bp-taken-bg) 6px,
    var(--bp-taken-border) 6px,
    var(--bp-taken-border) 7px
  );
  border-color: var(--bp-taken-border);
  color: var(--bp-taken-text);
}

.kiosk-page .kiosk-room .slot-btn.closed {
  background: var(--bp-closed-bg);
  border-color: var(--bp-card-border);
  color: var(--bp-closed-text);
}

/* Admin pages redesign (admin/rooms.html, admin/log.html,
   admin/analytics.html, admin/announcements.html, admin/calendar.html) —
   same reused .book-header/.book-bands/.book-footer/.book-a11y-row chrome
   as the other three pages, swapped in for the old .topbar/.app-footer.
   The nav — six links plus lang-toggle/sign-out — lives in
   .book-header-actions now instead of .topbar nav; #admin-nav-links itself
   is unrelated display-role-gating logic (see admin-*.js) and is untouched. */

body.admin-page {
  --footer-height: 4rem;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f7fafe;
  font-family: "Assistant", system-ui, -apple-system, "Segoe UI", sans-serif;
}

html:has(body.admin-page) {
  background: #f7fafe;
}

/* width: 100% is load-bearing, not decorative: .container's own
   margin: 0 auto centers it via auto cross-axis margins, and a flex item
   with auto margins on its cross axis (this is a flex column, so cross
   axis = horizontal) doesn't stretch to fill the container the normal
   align-items: stretch way — the auto margins absorb the free space
   instead, so the container quietly sized itself to its own content width
   (~720px) and centered *that*, no matter how wide .wide/.extra-wide's own
   max-width allowed it to go. An explicit width sidesteps that: it's no
   longer an auto-sized cross axis, so the auto margins just center the
   (now correctly wide) box the normal, non-flex way. */
.admin-page .container {
  flex: 1 1 auto;
  width: 100%;
}

.admin-page .book-header-actions {
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: 0.4rem;
}

.admin-page .book-header-actions .btn:not(.book-header-btn) {
  background: rgba(255, 255, 255, 0.13);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  border-radius: 999px;
  min-height: 36px;
  padding: 0.3rem 0.95rem;
  font-size: 0.85rem;
  font-weight: 600;
}
.admin-page .book-header-actions .btn:not(.book-header-btn):hover {
  background: rgba(255, 255, 255, 0.24);
  border-color: rgba(255, 255, 255, 0.55);
  color: #fff;
}

.admin-page .card {
  border: 1px solid var(--bp-card-border);
  border-top: 3px solid var(--bp-blue);
  box-shadow: var(--bp-shadow);
}

.admin-page .btn-primary {
  background: var(--bp-blue);
  border-color: var(--bp-blue);
  border-radius: 999px;
  font-weight: 700;
}
.admin-page .btn-primary:hover {
  background: var(--bp-navy);
  border-color: var(--bp-navy);
}

.admin-page .book-a11y-row {
  padding: 1rem 1rem 0.5rem;
}

/* Room/calendar sidebar selection, week grid, timeframe toggle and charts —
   all still var(--color-primary) (the app's original blue) rather than the
   redesign's --bp-blue/--bp-navy, so they read as a slightly different blue
   next to the new header/footer/cards. Recolored to match. */
.admin-page .room-list-item.selected {
  background: #eaf1fc;
  border-color: var(--bp-blue);
  color: var(--bp-navy);
}

.admin-page .week-day-head.is-today {
  background: #eaf1fc;
  border-bottom-color: var(--bp-blue);
}

.admin-page .week-cell.free:hover {
  background: #eaf1fc;
  color: var(--bp-blue);
}

.admin-page .timeframe-btn[aria-pressed="true"] {
  background: var(--bp-blue);
  border-color: var(--bp-navy);
  color: #fff;
}

.admin-page .chart-bar {
  fill: var(--bp-blue);
}
.admin-page .chart-bar.is-hovered,
.admin-page .chart-bar:focus-visible {
  fill: var(--bp-navy);
}
.admin-page .chart-bar:focus-visible {
  outline-color: var(--bp-navy);
}
.admin-page .chart-line {
  stroke: var(--bp-blue);
}
.admin-page .chart-area {
  fill: #eaf1fc;
}
.admin-page .chart-point {
  fill: var(--bp-blue);
}
.admin-page .chart-point.is-hovered {
  fill: var(--bp-navy);
}
.admin-page .chart-point-hit:focus-visible {
  outline-color: var(--bp-navy);
}

/* Announcements ticker — same "white pill floating over the footer bar's
   reading-start half" treatment as book.html's body.book-page version
   (mirrored rather than shared via a combined selector, matching this
   file's existing per-page-type duplication elsewhere, e.g. .kiosk-page vs
   .book-page). The admin pages didn't show announcements at all before this;
   .app-footer here is still the ordinary fixed footer (admin pages aren't
   kiosk-page), so the same fixed-position math applies unchanged. */
.admin-page .announcements-ticker {
  left: 0;
  right: 0;
  width: min(64%, 900px);
  margin-inline: auto;
  height: 2.4rem;
  bottom: calc((var(--footer-height) - 2.4rem) / 2);
  z-index: 41;
  background: #fff;
  border-top: none;
  border-radius: 999px;
  box-shadow: 0 2px 10px rgba(8, 33, 79, 0.28);
  padding: 0 0.4rem;
  gap: 0.5rem;
}

@media (max-width: 700px) {
  .admin-page .announcements-ticker {
    inset-inline: 0.6rem;
    width: auto;
    margin-inline: 0;
  }
  body.admin-page.has-announcements-ticker .book-powered-by {
    display: none;
  }
  .admin-page .book-footer .book-bands {
    display: none;
  }
  body.admin-page.has-announcements-ticker .book-footer-photo {
    display: none;
  }
}

/* body prefix matters here, not just style: without it this rule LOST
   (not tied) to the generic body.has-announcements-ticker:not(.kiosk-page)
   .app-footer rule elsewhere in this file on specificity —
   .admin-page.has-announcements-ticker .app-footer is three classes,
   (0,3,0), while the generic rule is body + three classes, (0,3,1). The
   generic one silently won and kept shifting the footer up to make room
   for a separate ticker bar underneath it — exactly the stacked-bars look
   this whole in-footer treatment exists to avoid. */
body.admin-page.has-announcements-ticker .app-footer {
  bottom: 0;
}

body.admin-page.has-announcements-ticker {
  padding-bottom: var(--footer-height);
}

.admin-page .ticker-track {
  color: #16307a;
  font-size: 0.85rem;
  font-weight: 700;
}

.admin-page .ticker-pause-btn {
  min-width: 1.75rem;
  min-height: 1.75rem;
  border-radius: 50%;
  background: #16307a;
  border-color: #16307a;
  color: #fff;
  font-size: 0.7rem;
  line-height: 1;
}

/* Info pages redesign (privacy.html, accessibility.html) — same reused
   chrome as everywhere else, just without a lang-toggle/sign-out (these
   two pages have no auth guard and no i18n toggle of their own to wire
   up), and a plain long-form article inside the card instead of app UI. */
body.book-info-page {
  --footer-height: 4rem;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: #f7fafe;
  font-family: "Assistant", system-ui, -apple-system, "Segoe UI", sans-serif;
}

html:has(body.book-info-page) {
  background: #f7fafe;
}

/* Collapses to a single right-aligned column only when there's nothing
   else in the header — privacy.html/accessibility.html have just the
   brand, but privacy-ack.html also has a sign-out button and wants the
   normal brand/actions two-ended layout every other page uses. */
.book-info-page .book-header-inner:not(:has(.book-header-actions)) {
  grid-template-columns: 1fr;
  justify-items: end;
}

.book-info-container {
  flex: 1 1 auto;
  width: 100%;
  max-width: 760px;
  padding-top: 2rem;
}

.book-info-page .card {
  border: 1px solid var(--bp-card-border);
  border-top: 3px solid var(--bp-blue);
  box-shadow: var(--bp-shadow);
  line-height: 1.65;
}

.book-info-page .card h2 {
  color: var(--bp-navy);
  margin-top: 1.5rem;
}

.book-info-page .card a {
  color: var(--bp-blue);
}

.book-info-page .book-a11y-row {
  padding: 1rem 1rem 0.5rem;
}

/* Login-stats card (admin/analytics.html) — two big bold numbers, same
   "boxed navy tile" treatment as book.html's .book-weekly-count rather
   than a chart, since there's nothing to plot for two scalar counts. */
.login-stats-card h2 {
  margin: 0 0 0.75rem;
}

.login-stats-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.login-stat-tile {
  flex: 1 1 160px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 1rem;
  border-radius: 12px;
  background: #eaf1fc;
}

.login-stat-value {
  font-size: 2rem;
  font-weight: 800;
  color: var(--bp-navy);
  line-height: 1;
}

.login-stat-label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--bp-blue);
}
