/* ==========================================================================
   FreeRoom - live classroom availability

   A monochrome system, warm-tinted so nothing is pure #000 / #fff, ported from
   the reference dashboard's tokens. With hue removed, emphasis is carried by
   weight, size, lightness and spacing.

   The page ground is white per the brief; --page and --surface are therefore
   the same value, and separation between the page and a card is carried by the
   card's border and shadow rather than by a tint difference.
   ========================================================================== */

:root {
  --page: #ffffff;
  --surface: #ffffff;

  /* Calendar grid metrics. Declared here rather than on `.timegrid` because the
     empty-state placeholder that stands in for the grid is not inside it, and
     the two have to agree to the pixel or the card resizes when data lands. */
  --timegrid-body-h: 620px;
  --timegrid-head-h: 58px;
  --timegrid-h: calc(var(--timegrid-body-h) + var(--timegrid-head-h));

  --ink-900: #171512;
  --ink-800: #2b2823;
  --ink-700: #423e37;
  --ink-600: #5c574e;
  --ink-500: #7a746a;
  --ink-400: #8b857a;
  --ink-300: #bdb8ae;
  --ink-200: #d0cbc0;
  --ink-150: #e6e2d9;
  --ink-100: #efece5;

  --border: rgba(23, 21, 18, 0.11);
  --border-strong: rgba(23, 21, 18, 0.2);

  --radius-sm: 4.5px;
  --radius: 4.5px;
  --radius-lg: 4.5px;

  --shadow-sm: 0 1px 2px rgba(23, 21, 18, 0.05);
  --shadow: 0 1px 3px rgba(23, 21, 18, 0.06), 0 8px 20px -10px rgba(23, 21, 18, 0.13);

  --font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --scrollbar-thumb: var(--ink-300);
  --scrollbar-thumb-hover: var(--ink-400);
  --scrollbar-track: var(--ink-100);

  /* Page-transition overlay cells - the animated-copy reveal's accent
     green (#abff02) from the marketing page, so the wipe reads as the
     same brand moment on both sides of a Demo/Problem-Solution
     navigation rather than switching between amber, ink and green. */
  --transition-cell-color: #abff02;
}

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

html {
  -webkit-text-size-adjust: 100%;
  color-scheme: light;
  background: var(--page);
  scrollbar-width: none;
  -ms-overflow-style: none;
}

/* Scrollbar hidden page-wide - scrolling still works, the track and
   thumb just never render. Firefox/IE are handled on html above;
   WebKit/Blink (Chrome, Safari, Edge) need the pseudo-element hidden
   on every scrollable element, not just html, or an inner scroller
   like .timegrid__body still shows its bar. */
::-webkit-scrollbar {
  display: none;
}

* {
  -webkit-tap-highlight-color: transparent;
}

::selection {
  background: var(--ink-200);
  color: var(--ink-900);
}

body {
  margin: 0;
  background: var(--page);
  color: var(--ink-900);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
p {
  margin: 0;
}

button {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--ink-900);
  outline-offset: 2px;
  border-radius: 4.5px;
}

.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;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: 10px 16px;
  background: var(--ink-900);
  color: var(--surface);
  text-decoration: none;
  border-radius: 0 0 4.5px 0;
}

.skip-link:focus {
  left: 0;
}

/* --- Shell ---------------------------------------------------------------- */

.shell {
  max-width: 1380px;
  margin: 0 auto;
  padding: 0 24px 40px;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 0;
  margin-bottom: 22px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}

.brand {
  display: flex;
  align-items: center;
  gap: 11px;
}

/* The same Bellefair wordmark the marketing page uses for "Vouch"
   (.site-nav__mark and .page-title h1 in index.html), so the mark is one
   thing across both pages rather than the header pulling in this file's
   UI sans. Bellefair is declared as an @font-face further down this file
   already, so nothing extra needs loading here.

   Matched to the nav mark rather than the footer's display h1: this is a
   small header mark at the same scale. That means 400 and uppercase with
   the tracking opened.

   The weight STAYS 400, and it is not an oversight. Bellefair ships a
   single 400 file (fonts/bellefair/ holds only bellefair-regular.ttf, and
   the Palace fallback is 400-only too), so asking for 600 or 700 does not
   load a heavier cut - the browser synthesises one by smearing the 400
   outlines, which reads as muddy rather than bold. The marketing page hit
   this and rejected it, along with -webkit-text-stroke, which paints half
   its width outside the glyph and fringes grey.

   So presence is bought with the levers that do exist on a 400-only face:
   a larger size, with the tracking pulled in slightly - at a bigger size
   the old 0.06em was spacing the letters apart faster than the size was
   adding weight. Colour is not one of those levers here: body already sets
   --ink-900, so the mark is at full ink and there is nothing to darken. */
.brand__name {
  font-family: "Bellefair", "Palace", serif;
  font-size: 21px;
  font-weight: 400;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  line-height: 1.2;
}

.brand__sub {
  font-size: 11.5px;
  color: var(--ink-400);
  line-height: 1.3;
}

.topbar__right {
  display: flex;
  align-items: center;
  gap: 18px;
}

/* --- Profile menu ---------------------------------------------------------
   The account control sits at the far right of the header, where it is looked
   for. Closed, it is a quiet row - avatar, name, chevron - with no border, so it
   does not compete with the filter buttons a line below it. Open, it becomes a
   proper surface: the same card treatment as every panel on the page, so the
   menu reads as part of the dashboard rather than as browser chrome. */

.profile {
  position: relative;
}

.profile__trigger {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 5px 9px 5px 5px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4.5px;
  cursor: pointer;
  text-align: left;
  transition: background 0.12s linear, border-color 0.12s linear;
}

.profile__trigger:hover {
  background: var(--ink-100);
  border-color: var(--border);
}

.profile__trigger[aria-expanded='true'] {
  background: var(--ink-100);
  border-color: var(--border-strong);
}

.profile__avatar {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--ink-100);
  color: var(--surface);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Initials, and the dark plate they are written on - set together so the plate
   and its text arrive as one thing. */
.profile__avatar[data-avatar-initials] {
  background: var(--ink-900);
  color: var(--surface);
}

.profile__avatar--lg {
  width: 38px;
  height: 38px;
  font-size: 13px;
}

.profile__id {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
  min-width: 0;
}

.profile__name {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.profile__role,
.profile__mail {
  font-size: 11.5px;
  color: var(--ink-400);
}

.profile__mail {
  margin-top: 2px;
}

/* The card's identity block, set to the same measure as the trigger's, so the
   name, the email and the role read as one stack rather than as three lines on
   different rhythms. */
.profile__card > div {
  line-height: 1.25;
}

/* Rotates to point up while the menu is open, so the control states which way it
   will move next rather than only that it is a menu. */
.profile__chevron {
  flex-shrink: 0;
  color: var(--ink-400);
  transition: transform 0.18s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.profile__trigger[aria-expanded='true'] .profile__chevron {
  transform: rotate(180deg);
}

/* Fixed rather than absolute - top/right are set inline in px by
   createProfileMenu's open() in switcher.js, from the trigger's own
   getBoundingClientRect().

   `position: fixed` is NOT what makes the stacking work, and it was a
   mistake to think so: a fixed element still resolves its z-index inside
   the nearest ancestor stacking context, so this menu kept painting under
   the page head's "Report a room" button while .topbar had one. The actual
   fix is that .topbar no longer opens a stacking context at all - see the
   note on its entrance animation below. Fixed positioning stays because it
   is the right way to anchor a menu that must escape the header's box
   without being clipped by it. */
.profile__menu {
  position: fixed;
  /* Above .bk-pop (120), the booking popover it can overlap, and below
     .toast (500), which is a confirmation and should never be covered. */
  z-index: 300;
  width: 244px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);

  /* Grows out of the corner it is pinned to. */
  transform-origin: 100% 0;
  animation: profile-menu-in 0.18s cubic-bezier(0.22, 0.61, 0.36, 1);
}

@keyframes profile-menu-in {
  from {
    opacity: 0;
    transform: scale(0.97) translateY(-4px);
  }
}

.profile__card {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 10px 12px;
}

.profile__group {
  padding-top: 5px;
  border-top: 1px solid var(--border);
}

.profile__item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: 4.5px;
  font: inherit;
  font-size: 13px;
  color: var(--ink-700);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s linear, color 0.12s linear;
}

.profile__item svg {
  flex-shrink: 0;
  color: var(--ink-400);
  transition: color 0.12s linear;
}

.profile__item:hover,
.profile__item:focus-visible {
  background: var(--ink-100);
  color: var(--ink-900);
}

.profile__item:hover svg,
.profile__item:focus-visible svg {
  color: var(--ink-900);
}

.profile__group--last {
  margin-top: 5px;
}

/* --- Section switcher -----------------------------------------------------
   The switcher nav (Free now / Today / This week / Month / My seats) was
   removed - calendar granularity is still reachable via the Day/3 day/Week/
   Month segmented control in the page head. */

/* The topbar's own entrance animation, previously shared with .switcher via
   the same keyframes so the header read as one piece arriving.

   Animates `margin-top` only - NOT `transform` and NOT `opacity`. Both of
   those open a stacking context for as long as the animation engine is
   tracking the element (in most browsers, for the animation's full fill-mode
   duration, `both` here), even once the value has settled on something
   visually inert - an identity matrix, or a fully opaque `opacity: 1`.
   Either one silently traps .profile__menu inside .topbar's now-local
   stacking order, where its z-index is resolved among .topbar's children
   instead of at the document root, so it can no longer out-rank
   .cta-button ("Report a room" in the page head below) - a different part
   of the DOM entirely, painting later in document order.

   The transform was removed here once for exactly this reason; the fade was
   left behind and kept the bug alive on its own. So the fade now lives on
   the topbar's children, which have no positioned descendants that need to
   escape their box, and .topbar itself animates only `margin-top` - a
   property that creates no stacking context. Same entrance, no trap. */
.topbar {
  animation: switcher-in 0.34s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

@keyframes switcher-in {
  from {
    margin-top: -3px;
  }
}

/* The fade, moved off .topbar itself. `.profile` is excluded deliberately -
   it is the menu's offset parent and the one child that must not open a
   stacking context of its own. It still arrives with the rest via the
   margin-top settle above. */
.topbar > .brand,
.topbar__right > :not(.profile) {
  animation: topbar-fade-in 0.34s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

@keyframes topbar-fade-in {
  from {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .topbar,
  .topbar > .brand,
  .topbar__right > :not(.profile) {
    animation: none;
  }
}

/* --- Page head ------------------------------------------------------------ */

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.page-head h1 {
  font-size: 23px;
  font-weight: 600;
  letter-spacing: -0.022em;
}

.page-head p {
  color: var(--ink-500);
  font-size: 13.5px;
  margin-top: 3px;
}

.controls {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Prev / Today / Next as one wrapping unit.

   Loose in .controls these were three independent flex items sharing its
   10px gap. That is invisible on desktop, where the whole row fits on one
   line, but at phone width the segmented switcher takes the full first
   line and the three buttons break onto a second one - and since
   .card__head above is `justify-content: space-between` with wrapping, a
   lone second line has no alignment of its own and reads as centred,
   detached from the switcher it belongs to.

   Grouping them means .controls wraps ONE item instead of three, so the
   stepper stays a single intact block wherever it lands. The 10px gap is
   repeated here so the spacing between the three buttons is unchanged
   from when .controls owned it - desktop rendering is identical. */
.stepper {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* --- Controls ------------------------------------------------------------- */

.segmented {
  position: relative;
  display: inline-flex;
  padding: 3px;
  background: var(--ink-100);
  border-radius: 4.5px;
  gap: 2px;
}

.segmented button {
  position: relative;
  z-index: 1;
  border: 0;
  background: transparent;
  padding: 6px 13px;
  border-radius: 3.5px;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-500);
  cursor: pointer;
  transition: color 0.16s linear, background 0.16s linear;
}

.segmented button:hover {
  color: var(--ink-900);
}

.segmented button[aria-pressed='true'] {
  color: var(--ink-900);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.ghost-button {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 4.5px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s linear, border-color 0.12s linear;
}

.ghost-button:hover {
  background: var(--ink-100);
  border-color: var(--ink-400);
}

.ghost-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ghost-button[aria-pressed='true'] {
  background: var(--ink-900);
  border-color: var(--ink-900);
  color: var(--surface);
}

.solid-button {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 15px;
  background: var(--ink-900);
  border: 1px solid var(--ink-900);
  border-radius: 4.5px;
  font-size: 13px;
  font-weight: 500;
  color: var(--surface);
  cursor: pointer;
  transition: background 0.12s linear, border-color 0.12s linear;
}

.solid-button:hover {
  background: var(--ink-800);
  border-color: var(--ink-800);
}

/* Matches the marketing page's "View Live" CTA (.comparison-cta in
   problem-statement.css) - same chamfered bottom-right corner, uppercase
   bold label and hover lift, so the two products' primary actions read
   as one system rather than two different button languages. Its own
   class rather than restyling .solid-button, since that class is shared
   with "Report free" and "Going" and this look is heavier than either
   of those wants. */
.cta-button {
  --cta-cut: 7px;

  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  background: var(--ink-900);
  border: none;
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(100% - var(--cta-cut)),
    calc(100% - var(--cta-cut)) 100%,
    0 100%
  );
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--surface);
  cursor: pointer;
  transition: background-color 220ms ease, transform 220ms ease, box-shadow 220ms ease;
  box-shadow: 0 1px 2px rgba(23, 21, 18, 0.14);
}

.cta-button:hover {
  background: var(--ink-800);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px -4px rgba(23, 21, 18, 0.32);
}

.cta-button:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(23, 21, 18, 0.14);
}

.cta-button svg {
  transition: transform 220ms ease;
}

.cta-button:hover svg {
  transform: translateX(3px);
}

.cta-button:focus-visible {
  outline: 2px solid var(--ink-900);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .cta-button,
  .cta-button:hover,
  .cta-button svg,
  .cta-button:hover svg {
    transition: none;
  }
}

.icon-button {
  display: inline-grid;
  place-items: center;
  width: 31px;
  height: 31px;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 4.5px;
  color: var(--ink-700);
  cursor: pointer;
  transition: background 0.12s linear, border-color 0.12s linear;
}

.icon-button:hover {
  background: var(--ink-100);
  border-color: var(--ink-400);
}

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

.grid {
  display: grid;
  gap: 16px;
  margin-bottom: 16px;
}

.grid--split {
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  align-items: start;
}

@media (max-width: 1080px) {
  .grid--split {
    grid-template-columns: minmax(0, 1fr);
  }
}

.card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.card--flush {
  overflow: hidden;
}

.card--flush .card__head {
  padding-bottom: 15px;
  border-bottom: 1px solid var(--border);
}

.card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 17px 20px 0;
  flex-wrap: wrap;
}

.card__title {
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.card__sub {
  font-size: 12.5px;
  color: var(--ink-400);
  margin-top: 2px;
}

.card__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 14px 20px 18px;
}

/* --- Insight -------------------------------------------------------------- */

.insight {
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-600);
  text-align: center;
}

.insight strong {
  color: var(--ink-900);
  font-weight: 600;
}

/* --- Status banner -------------------------------------------------------- */

.toast:empty {
  display: none;
}

/* A floating confirmation rather than an inline bar - the old version sat in
   normal flow above the grid, so reporting a room pushed the whole page down
   for five seconds and back up when the message cleared. Fixed and centred
   at the top of the viewport instead, so it reads as a passing confirmation
   layered over the page rather than as content the layout has to make room
   for. */
.toast {
  position: fixed;
  top: 18px;
  left: 50%;
  z-index: 500;
  max-width: min(420px, calc(100vw - 32px));
  padding: 12px 16px;
  border-radius: 8px;
  background: var(--ink-900);
  color: var(--surface);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.4;
  text-align: center;
  box-shadow: 0 10px 30px -8px rgba(23, 21, 18, 0.35), 0 2px 8px rgba(23, 21, 18, 0.12);
  pointer-events: none;
  animation: toast-in 0.28s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translate(-50%, -10px) scale(0.96);
  }
  to {
    opacity: 1;
    transform: translate(-50%, 0) scale(1);
  }
}

.toast[data-tone='success'] {
  background: var(--ink-900);
}

/* Errors get a warmer, lighter surface rather than the near-black default -
   still monochrome, but a status the reader needs to actually read (an
   export with nothing in it, an empty "My seats") should not look
   identical to a routine success confirmation. */
.toast[data-tone='error'] {
  background: var(--ink-100);
  color: var(--ink-900);
  box-shadow: 0 10px 30px -8px rgba(23, 21, 18, 0.22), 0 2px 8px rgba(23, 21, 18, 0.1),
    inset 0 0 0 1px var(--border-strong);
}

@media (prefers-reduced-motion: reduce) {
  .toast {
    animation: none;
  }
}

@media (max-width: 720px) {
  .toast {
    top: 12px;
    font-size: 13px;
  }
}

/* --- Time grid ------------------------------------------------------------
   A day/week time grid showing free windows rather than bookings. Every
   vertical measurement derives from `--hour-px`, and the JS reads that same
   value back rather than keeping a second copy that can drift. */

.timegrid {
  --hour-px: 52px;
  --gutter-w: 58px;
  --timegrid-sb-w: 11px;

  height: var(--timegrid-h);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
  user-select: none;
}

.timegrid__head {
  display: grid;
  grid-template-columns: var(--gutter-w) repeat(var(--cols, 7), minmax(0, 1fr));
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.timegrid__head-gutter {
  border-right: 1px solid var(--border);
}

.timegrid__head-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 4px 9px;
  border-right: 1px solid var(--border);
  border-top: 0;
  border-bottom: 0;
  border-left: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  transition: background 0.12s linear;
}

.timegrid__head-day:last-child {
  border-right: 0;
}

.timegrid__head-day:hover {
  background: var(--ink-100);
}

.timegrid__head-name {
  font-size: 10.5px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-400);
}

.timegrid__head-num {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 25px;
  height: 25px;
  border-radius: 50%;
  font-size: 13.5px;
  font-weight: 550;
  font-variant-numeric: tabular-nums;
  color: var(--ink-800);
}

.timegrid__head-day--today .timegrid__head-num {
  background: var(--ink-900);
  color: var(--surface);
}

.timegrid__head-day--today .timegrid__head-name {
  color: var(--ink-900);
}

.timegrid__head-day--weekend .timegrid__head-name {
  color: var(--ink-300);
}

#cal-surface {
  min-height: var(--timegrid-h);
}

.timegrid__body {
  position: relative;
  padding-left: calc(var(--gutter-w) - var(--timegrid-sb-w));
  max-height: var(--timegrid-body-h);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  /* Puts the scrollbar on the left, under the hour rail. The two children flip
     straight back to LTR, so only the bar moves - every offset in here is a
     physical property and so unaffected either way. */
  direction: rtl;
}

.timegrid__gutter,
.timegrid__cols {
  direction: ltr;
}

.timegrid__gutter {
  position: absolute;
  z-index: 2;
  top: 0;
  min-height: 100%;
  left: calc(-1 * var(--timegrid-sb-w));
  width: var(--gutter-w);
  border-right: 1px solid var(--border);
  background: var(--surface);
}

.timegrid__gutter-cell {
  position: relative;
  height: var(--hour-px);
}

.timegrid__gutter-label {
  position: absolute;
  top: -6px;
  right: 7px;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-400);
  white-space: nowrap;
}

.timegrid__gutter-cell:first-child .timegrid__gutter-label {
  display: none;
}

.timegrid__cols {
  display: grid;
  grid-template-columns: repeat(var(--cols, 7), minmax(0, 1fr));
}

.timegrid__day {
  position: relative;
  border-right: 1px solid var(--border);
  cursor: cell;
}

.timegrid__day:last-child {
  border-right: 0;
}

/* While a drag is live the column takes a resize cursor and stops the browser
   scrolling the page out from under the pointer on touch. */
.timegrid__day--dragging {
  cursor: ns-resize;
  touch-action: none;
}

/* `pan-y` by default on mobile - vertical scroll is native and free right up to
   the moment a long-press actually settles, at which point `startDrag` adds
   `--touch-hold` and this drops to `none`. That switch only ever happens with
   the finger stationary: the browser has not yet had cause to commit to a pan
   from a touch that has not moved, which is the one case a `touch-action` change
   lands cleanly instead of being read as a value moving under a gesture already
   in flight and cancelled for it. */
@media (max-width: 768px) {
  .timegrid__day {
    touch-action: pan-y;
  }

  .timegrid__day--touch-hold {
    touch-action: none;
  }
}

.timegrid__hour {
  height: var(--hour-px);
  border-top: 1px solid var(--border);
  transition: background 0.1s linear;
}

.timegrid__day:not(.timegrid__day--dragging) .timegrid__hour:hover {
  background: var(--ink-100);
}

.timegrid__hour:first-child {
  border-top: 0;
}

/* The half-hour rule, lighter than the hour so the grid reads as a hierarchy
   rather than as forty-eight equal lines. */
.timegrid__hour::after {
  content: '';
  display: block;
  height: calc(var(--hour-px) / 2);
  border-bottom: 1px dashed rgba(23, 21, 18, 0.055);
}

/* Hours already gone. Marked twice over so it reads at a glance: a dashed edge
   where "now" cuts across the column, and a faint diagonal hatch through the
   shaded area. Pastel red rather than the ink palette - a deliberate accent
   so the past reads as a distinct zone at a glance instead of blending into
   the grid's neutral lines. */
/* Behind the cards, never over them. The shading says "this time has gone", but
   a window in the past is still a window the reader may want to read - drawn on
   top it tints the card's own text and the card reads as half-erased rather than
   as finished. `z-index: 0` puts it under every card (which start at 3) while
   still sitting over the plain hour rules. */
.timegrid__past {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 0;
  background:
    repeating-linear-gradient(
      -45deg,
      rgba(217, 60, 60, 0.05),
      rgba(217, 60, 60, 0.05) 5px,
      transparent 5px,
      transparent 11px
    ),
    rgba(217, 60, 60, 0.055);
  border-bottom: 1.5px dashed rgba(217, 60, 60, 0.35);
  pointer-events: none;
}

.timegrid__nowline {
  position: absolute;
  left: 0;
  right: 0;
  height: 0;
  border-top: 1.5px solid var(--ink-900);
  pointer-events: none;
  z-index: 5;
}

.timegrid__nowline::before {
  content: '';
  position: absolute;
  left: -3.5px;
  top: -4px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-900);
}

/* --- Free-window cards --- */

.timegrid__event {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 4px 6px;
  overflow: hidden;
  background: var(--ink-100);
  border: 1px solid var(--border);
  border-left: 2.5px solid var(--ink-700);
  border-radius: 3px;
  font: inherit;
  text-align: left;
  color: var(--ink-800);
  cursor: pointer;
  transition: box-shadow 0.12s ease, background 0.12s linear;
}

.timegrid__event:hover,
.timegrid__event:focus-visible {
  background: var(--ink-150);
  box-shadow: var(--shadow);
  z-index: 8 !important;
}

.timegrid__event:focus-visible {
  outline: 2px solid var(--ink-900);
  outline-offset: 1px;
}

/* --- Colour by building ---------------------------------------------------
   The one attribute that tells the reader where to walk, so it is the one that
   earns colour. Three buildings, three hues, each carried on the left rule with
   only a breath of it in the fill - the rule is the signal, the fill just keeps
   the card from reading as plain white.

   Colour is never the only channel: the building's short name is printed on the
   card, "Ask first" stays dashed, and the status still reads by weight and
   border exactly as before. Someone who cannot separate these hues loses nothing
   they could not already read in words. */

.timegrid__event--dic {
  --room-ink: #2f6f7d;
  --room-wash: #f0f6f7;
}

.timegrid__event--eng {
  --room-ink: #8a5a2b;
  --room-wash: #faf4ed;
}

.timegrid__event--cre {
  --room-ink: #6b4a86;
  --room-wash: #f6f2f9;
}

.timegrid__event--dic,
.timegrid__event--eng,
.timegrid__event--cre {
  background: var(--room-wash);
  border-left-color: var(--room-ink);
}

.timegrid__event--dic .timegrid__event-title,
.timegrid__event--eng .timegrid__event-title,
.timegrid__event--cre .timegrid__event-title {
  color: var(--room-ink);
}

/* Hover deepens the wash rather than swapping to the neutral grey the base rule
   would otherwise reassert. */
.timegrid__event--dic:hover,
.timegrid__event--eng:hover,
.timegrid__event--cre:hover,
.timegrid__event--dic:focus-visible,
.timegrid__event--eng:focus-visible,
.timegrid__event--cre:focus-visible {
  background: var(--room-wash);
  filter: brightness(0.965);
}

/* --- Colour by the reporting student's own tag -----------------------------
   A student reporting a room free can tag their own card with a colour (see
   REPORT_COLORS / colorHexFor in app.js). It plugs into the exact same
   --room-ink / --room-wash pair the building colours above use - the
   difference is these two are set inline per-card from the chosen hex
   (style="--room-ink: …; --room-wash: …") rather than fixed in a class
   here, since the palette is open-ended. Comes after .timegrid__event--dic
   /--eng/--cre in source order so a personal tag always wins over the
   building wash on the same card. */
.timegrid__event--tagged {
  background: var(--room-wash);
  border-left-color: var(--room-ink);
}

.timegrid__event--tagged .timegrid__event-title {
  color: var(--room-ink);
}

.timegrid__event--tagged:hover,
.timegrid__event--tagged:focus-visible {
  background: var(--room-wash);
  filter: brightness(0.965);
}

/* Open right now: full ink on the rule and the heavier title, so a window you
   can walk into this minute separates from one later today by two channels
   rather than one. Comes after --tagged, so "free to walk in this minute"
   still outranks a decorative colour choice on the same card. */
.timegrid__event--now {
  background: var(--surface);
  border-left-color: var(--ink-900);
  border-left-width: 3px;
}

.timegrid__event--now .timegrid__event-title {
  font-weight: 650;
  color: var(--ink-900);
}

/* "Ask first" rooms are dashed - the permission layer is a real difference in
   what the reader can do, so it is drawn, not just written.

   Deliberately no `border-left-color` here. This rule follows the building
   colours in source order at equal specificity, so setting one would win - and
   it did: Engineering and Creative are both "ask first" rooms, so two of the
   three buildings silently lost their hue to grey and only Main stayed coloured.
   The dash carries the permission, the rule colour carries the building, and the
   two channels no longer fight over the same property. */
.timegrid__event--ask {
  border-style: dashed;
  border-left-style: solid;
}

/* A window that has already closed - stays grey/ink (unlike the zone
   shading below it, which is pastel red): a card fading toward neutral
   reads as "done", where red on the card itself would compete with a red
   colour tag or read as an error rather than as finished. */
.timegrid__event--past {
  background: var(--surface);
  border-left-color: var(--ink-200);
  color: var(--ink-400);
}

.timegrid__event--past .timegrid__event-title {
  color: var(--ink-400);
}

.timegrid__event-title {
  font-size: 11.5px;
  font-weight: 550;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.timegrid__event-meta {
  font-size: 10.5px;
  line-height: 1.35;
  color: var(--ink-500);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.timegrid__event--past .timegrid__event-meta {
  color: var(--ink-300);
}

.timegrid__event--compact {
  padding: 2px 5px;
}

.timegrid__event--compact .timegrid__event-title {
  font-size: 11px;
}

/* --- Resize handles ------------------------------------------------------- */

.timegrid__resize {
  position: absolute;
  left: 0;
  right: 0;
  height: 7px;
  cursor: ns-resize;
  z-index: 2;
}

.timegrid__resize--top {
  top: -1px;
}

.timegrid__resize--bottom {
  bottom: -1px;
}

/* Drawn only on hover: a permanent grip on every card would turn the grid into
   a field of handles. */
.timegrid__event:hover .timegrid__resize::after {
  content: '';
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: var(--ink-400);
}

.timegrid__event:hover .timegrid__resize--top::after {
  top: 2px;
}

.timegrid__event:hover .timegrid__resize--bottom::after {
  bottom: 2px;
}

/* The source card stays in place at low opacity as a reference for where it came
   from, while the ghost shows where it will land. */
.timegrid__event--dragging {
  opacity: 0.32;
  cursor: grabbing;
}

/* --- Drag overlays --------------------------------------------------------
   Drawn with borders and lightness rather than colour, so they stay legible in
   the monochrome system: a ghost is a dashed outline, a live selection is
   filled ink. */

/* Where a dragged or resized card will land. Dashed and barely filled, so the
   grid underneath stays readable through it. */
.timegrid__ghost {
  position: absolute;
  left: 1px;
  right: 1px;
  padding: 3px 6px;
  border: 1.5px dashed var(--ink-700);
  border-radius: 3px;
  background: rgba(23, 21, 18, 0.05);
  pointer-events: none;
  overflow: hidden;
  z-index: 9;
}

.timegrid__ghost-title {
  font-size: 11.5px;
  font-weight: 550;
  color: var(--ink-900);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.timegrid__ghost-time {
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-600);
}

/* A new report being dragged out of empty space. Filled ink, because unlike the
   move ghost it is not standing in for something that already exists - it is the
   thing being made. */
/* The live preview while dragging out a new window - a provisional shape,
   not a committed card, so it reads as a sketch: a dashed outline over a
   faint tint rather than the solid fill a real card gets once saved.
   Grey by default; once the create form is open, picking a colour swatch
   sets --band-tint inline on this element so the preview updates to match
   in real time (see the swatch click handler in openCreatePopover). */
.timegrid__band {
  position: absolute;
  left: 1px;
  right: 1px;
  padding: 3px 6px;
  border-radius: 3px;
  background: var(--band-tint, rgba(23, 21, 18, 0.06));
  border: 1.5px dashed var(--ink-700);
  color: var(--ink-900);
  pointer-events: none;
  overflow: hidden;
  z-index: 9;
  transition: background-color 0.12s linear;
}

.timegrid__band-time {
  font-size: 10.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- Create form ---------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 14px;
}

.field__label {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ink-700);
}

/* Colour swatch picker for a reported room's optional tag. Buttons rather
   than radio inputs so the swatch itself (a background painted from
   --swatch) can be the whole hit target, with no separate visible input
   to style around. */
.bk-pop__colors {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 2px;
}

.bk-pop__color {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: var(--swatch);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--surface);
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.bk-pop__color:hover {
  transform: scale(1.08);
}

.bk-pop__color.is-selected {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 3.5px var(--ink-900);
}

.bk-pop__color:focus-visible {
  outline: 2px solid var(--ink-900);
  outline-offset: 2px;
}

/* The "no colour" option - dashed ring rather than a fill, so it reads as
   an eraser rather than as one more colour in the row. */
.bk-pop__color[data-color=""] {
  border-style: dashed;
  color: var(--ink-500);
}

.field__input {
  width: 100%;
  padding: 9px 11px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 4.5px;
  font: inherit;
  font-size: 13px;
  color: var(--ink-900);
  transition: border-color 0.12s linear;
}

.field__input:hover {
  border-color: var(--ink-400);
}

/* The ring replaces the global focus outline here: on a bordered input an offset
   outline reads as a second, detached box around the field. */
.field__input:focus,
.field__input:focus-visible {
  outline: none;
  border-color: var(--ink-900);
  box-shadow: 0 0 0 3px rgba(23, 21, 18, 0.08);
}

.bk-pop__row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.bk-pop__row-label {
  font-size: 12.5px;
  font-weight: 550;
}

.bk-pop__time {
  width: auto;
  flex: 1;
  font-variant-numeric: tabular-nums;
}

/* --- Hint bar under the grid --- */

.timegrid-hints {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 16px;
  margin-top: 11px;
  font-size: 11px;
  color: var(--ink-400);
}

.timegrid-hints__key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 17px;
  padding: 1px 4px;
  margin-right: 3px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: 3px;
  font-size: 10px;
  font-weight: 600;
  color: var(--ink-600);
}

/* --- Month grid ----------------------------------------------------------- */

.calendar__weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  margin-bottom: 6px;
}

.calendar__weekday {
  text-align: center;
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-400);
}

.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.calendar__day {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-height: 104px;
  padding: 6px 7px 8px;
  border: 0;
  background: var(--surface);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.12s linear;
}

.calendar__day:hover {
  background: var(--ink-100);
}

.calendar__day--outside {
  background: var(--page);
  color: var(--ink-300);
}

.calendar__date {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-700);
}

.calendar__day--outside .calendar__date {
  color: var(--ink-300);
}

.calendar__day--today .calendar__date {
  font-weight: 700;
  color: var(--ink-900);
}

.calendar__count {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 16px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--ink-150);
  color: var(--ink-600);
  font-size: 10px;
  font-weight: 650;
}

.calendar__chip {
  display: block;
  padding: 2px 5px;
  border-radius: 3px;
  background: var(--ink-100);
  border-left: 2px solid var(--ink-700);
  font-size: 10px;
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ink-700);
}

/* The month view's chips take the same colours, so a building reads the same in
   both views rather than being colour-coded in one and grey in the other.

   These have to sit *after* `.calendar__chip` rather than beside the timegrid
   colours further up: the base rule sets background, border and colour at equal
   specificity, so from above it won every one of them and the whole month came
   out uniformly beige. */
.calendar__chip--dic {
  border-left-color: #2f6f7d;
  background: #f0f6f7;
  color: #2f6f7d;
}

.calendar__chip--eng {
  border-left-color: #8a5a2b;
  background: #faf4ed;
  color: #8a5a2b;
}

.calendar__chip--cre {
  border-left-color: #6b4a86;
  background: #f6f2f9;
  color: #6b4a86;
}

/* Dash only - the colour belongs to the building, exactly as on the grid. */
.calendar__chip--ask {
  border-left-style: dashed;
}

.calendar__more {
  font-size: 10px;
  color: var(--ink-400);
}

/* --- Mini month picker ---------------------------------------------------- */

.minical__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}

.minical__title {
  font-size: 12.5px;
  font-weight: 550;
}

.minical {
  --minical-gap: 1px;
  --minical-weekday-line: 14.25px;
  --minical-weekday-pad: 4px;
}

.minical__grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--minical-gap);
}

.minical__weekday {
  padding-bottom: var(--minical-weekday-pad);
  text-align: center;
  font-size: 9.5px;
  line-height: var(--minical-weekday-line);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-400);
}

.minical__day {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 3px;
  background: transparent;
  font: inherit;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-700);
  cursor: pointer;
  transition: background 0.12s linear;
}

.minical__day:hover {
  background: var(--ink-150);
}

.minical__day--outside {
  color: var(--ink-300);
}

.minical__day--today {
  font-weight: 700;
  color: var(--ink-900);
}

.minical__day--inrange {
  background: var(--ink-100);
}

.minical__day--selected {
  background: var(--ink-900);
  color: var(--surface);
}

.minical__day--has::after {
  content: '';
  position: absolute;
  bottom: 3px;
  left: 50%;
  transform: translateX(-50%);
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--ink-400);
}

.minical__day--selected.minical__day--has::after {
  background: var(--surface);
}

/* --- Pills ---------------------------------------------------------------- */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 10.5px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--ink-700);
}

.pill--on {
  background: var(--ink-900);
  border-color: var(--ink-900);
  color: var(--surface);
}

.pill--wait {
  background: var(--surface);
  border-style: dashed;
  color: var(--ink-600);
}

.pill--off {
  background: var(--ink-100);
  border-color: var(--border);
  color: var(--ink-400);
}

/* --- Empty state ---------------------------------------------------------- */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  text-align: center;
  color: var(--ink-500);
}

.empty-state__mark {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--ink-100);
  color: var(--ink-400);
}

.empty-state__title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-800);
}

.empty-state__hint {
  font-size: 12px;
  max-width: 34ch;
}

/* --- Chart ---------------------------------------------------------------- */

.chart {
  flex: 1;
  min-height: 190px;
  /* The tooltip below is positioned absolutely and is appended into this
     element, so this is the box it anchors against. Without it the popup
     would resolve against the page and sit wherever the card happens to
     be scrolled to. */
  position: relative;
}

.chart svg {
  display: block;
  overflow: visible;
}

.chart-empty {
  font-size: 12.5px;
  color: var(--ink-400);
}

.chart-column {
  cursor: default;
  transition: opacity 0.12s linear;
}

.chart-column:hover {
  opacity: 0.78;
}

.chart-column:focus,
.chart-column:focus-visible {
  outline: none;
}

/* --- Chart tooltip --------------------------------------------------------
   The hover popup the column chart raises (makeTooltip/bindTooltip in
   chart-core.js). Same rules as the figures on the index page, where they
   live scoped under .problem-detail - unscoped here because this page has
   no equivalent wrapper and .chart is already the anchor. Every token used
   below is defined at the top of this file. */

.chart-tooltip {
  position: absolute;
  left: 0;
  top: 0;
  width: max-content;
  pointer-events: none;
  will-change: transform;
  z-index: 5;
}

.chart-tooltip__box {
  position: relative;
  min-width: 172px;
  max-width: 260px;
  padding: 9px 11px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 4.5px;
  box-shadow: var(--shadow);
  transform-origin: var(--ox, 0%) var(--oy, 100%);
  opacity: 0;
  transform: scale(1.012, 0.93) translateY(5px);
  transition: opacity 0.16s ease, transform 0.16s ease;
}

.chart-tooltip.is-visible .chart-tooltip__box {
  opacity: 1;
  transform: scale(1, 1) translateY(0);
  transition: opacity 0.3s cubic-bezier(0.3, 0.6, 0.3, 1),
    transform 0.42s cubic-bezier(0.18, 0.9, 0.26, 1.02);
}

/* An adopted tooltip is one already on screen being re-pointed at the next
   mark - it must not replay its arrival, or crossing between columns reads
   as a close and a reopen. */
.chart-tooltip.is-adopting .chart-tooltip__box,
.chart-tooltip.is-adopting .chart-tooltip__layer > * {
  transition: none !important;
  animation: none !important;
}

.chart-tooltip__layer--ghost {
  position: absolute;
  left: 11px;
  right: 11px;
  top: 9px;
  opacity: 0;
  pointer-events: none;
}

.chart-tooltip__title {
  font-size: 12px;
  font-weight: 600;
  margin-bottom: 6px;
}

.chart-tooltip__row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  margin-top: 3px;
}

.chart-tooltip__name {
  color: var(--ink-500);
}

.chart-tooltip__value {
  margin-left: auto;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.chart-tooltip__meta {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
  font-size: 11.5px;
  color: var(--ink-400);
}

/* --- Popover -------------------------------------------------------------- */

.bk-pop {
  position: fixed;
  z-index: 120;
  width: 330px;
  max-width: calc(100vw - 20px);
  max-height: calc(100vh - 20px);
  overflow-y: auto;
  padding: 16px 18px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  animation: bk-pop-in 0.14s cubic-bezier(0.22, 0.61, 0.36, 1);
}

@keyframes bk-pop-in {
  from {
    opacity: 0;
    transform: scale(0.97) translateY(4px);
  }
}

.bk-pop__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.bk-pop__title {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-900);
}

.bk-pop__sub {
  margin-top: 3px;
  font-size: 12px;
  color: var(--ink-500);
  font-variant-numeric: tabular-nums;
}

.bk-pop__facts {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}

.bk-pop__fact {
  font-size: 12.5px;
  color: var(--ink-800);
}

.bk-pop__fact-label {
  display: inline-block;
  min-width: 74px;
  margin-right: 4px;
  font-size: 11px;
  color: var(--ink-400);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.bk-pop__note {
  margin: 0 0 14px;
  padding: 9px 11px;
  background: var(--ink-100);
  border-radius: 4.5px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-600);
}

.bk-pop__actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.bk-pop__actions .solid-button,
.bk-pop__actions .ghost-button {
  flex: 1;
  justify-content: center;
}

/* --- The student report layer --------------------------------------------
   The timetable says a room should be free; only a student standing in it
   knows whether it is. These are the two votes and the confidence they add up
   to - the part of the product that is powered by the people using it. */

.report {
  margin-bottom: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.report__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}

.report__title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-400);
}

.report__count {
  font-size: 11.5px;
  color: var(--ink-500);
  font-variant-numeric: tabular-nums;
}

/* The confidence bar. Length is the encoding, and the figure is written out
   beside it - the bar alone is never the only way to read the number. */
.report__bar {
  position: relative;
  height: 6px;
  border-radius: 999px;
  background: var(--ink-150);
  overflow: hidden;
  margin-bottom: 9px;
}

.report__fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--ink-900);
  transition: width 0.25s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.report__fill--low {
  background: repeating-linear-gradient(
    45deg,
    var(--ink-400),
    var(--ink-400) 3px,
    var(--ink-300) 3px,
    var(--ink-300) 6px
  );
}

.report__votes {
  display: flex;
  gap: 8px;
}

.report__votes .ghost-button {
  flex: 1;
  justify-content: center;
  padding: 7px 10px;
  font-size: 12.5px;
}

/* --- Room list rail ------------------------------------------------------- */

.roomlist {
  display: flex;
  flex-direction: column;
  max-height: 340px;
  overflow-y: auto;

  /* The page's own 10px bar with its 2px track is right against a full-height
     document and far too heavy inside a card rail - it reads as a second border
     down the edge of the panel. Narrowed to a trackless thumb, the same
     treatment the time grid's inner scroller already uses. */
  scrollbar-width: thin;
  scrollbar-color: var(--ink-200) transparent;
}

.roomlist::-webkit-scrollbar {
  width: 6px;
}

.roomlist::-webkit-scrollbar-track {
  background: transparent;
}

.roomlist::-webkit-scrollbar-thumb {
  background: var(--ink-200);
  border-radius: 999px;
}

.roomlist::-webkit-scrollbar-thumb:hover {
  background: var(--ink-300);
}

.roomlist__item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid var(--border);
}

.roomlist__item:last-child {
  border-bottom: 0;
}

.roomlist__main {
  min-width: 0;
}

.roomlist__name {
  font-size: 12.5px;
  font-weight: 600;
}

/* The line the row exists for: when this room is next actually usable. Given the
   ink and the weight, because it is what the reader is deciding on - the total
   beside it is context, not the answer. */
.roomlist__when {
  margin-top: 2px;
  font-size: 12px;
  font-weight: 550;
  color: var(--ink-800);
  font-variant-numeric: tabular-nums;
}

.roomlist__now {
  font-weight: 700;
  color: var(--ink-900);
}

.roomlist__gone {
  font-weight: 400;
  color: var(--ink-400);
}

.roomlist__meta {
  margin-top: 2px;
  font-size: 11.5px;
  color: var(--ink-400);
}

/* The aggregate, demoted. It used to be the only number in the row and read as
   one continuous sitting; it now sits quietly to the side and says what it is
   made of. */
.roomlist__total {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  flex-shrink: 0;
}

.roomlist__hours {
  font-size: 12.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-600);
  white-space: nowrap;
}

.roomlist__windows {
  margin-top: 1px;
  font-size: 10px;
  line-height: 1.3;
  color: var(--ink-400);
}

/* --- Avatar stacks --------------------------------------------------------
   Who else is going. Overlapped rather than spaced, so a full stack costs barely
   more width than a single face and the card's own text keeps its room. */

/* `margin-top: auto` alone pushes the stack to the card's foot only when the
   flex column has slack to give. A short card has none, so the faces landed
   jammed against the text above them - and on the shortest cards they sat up at
   the top edge instead of the bottom. The explicit gap is the floor that holds
   in both cases. */
.avatars {
  display: flex;
  align-items: center;
  margin-top: auto;
  padding-top: 5px;
}

.avatars__face {
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  margin-right: -5px;
  border-radius: 50%;
  background: var(--ink-150);
  /* The ring is the card's own background, so overlapping faces stay separate
     shapes rather than merging into one grey blob. */
  border: 1.5px solid var(--surface);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink-600);
  flex-shrink: 0;
}

/* One fixed tone per student, so the same face is the same colour everywhere -
   a stack of initials is only scannable if the colours hold still across rows.
   Kept as tinted fills with darker ink on top rather than saturated discs: the
   page is a quiet monochrome system with three building hues already doing
   work, and eight loud avatars would out-shout the cards they sit on.

   Colour is never the only channel here either - the initials identify the
   person, and the popover lists every name in full. */
.avatars__face--a {
  background: #dfeaf2;
  color: #2c5875;
}

.avatars__face--b {
  background: #f6e5da;
  color: #8a4f2b;
}

.avatars__face--c {
  background: #e2eede;
  color: #406338;
}

.avatars__face--d {
  background: #ece2f1;
  color: #64447c;
}

.avatars__face--e {
  background: #fae3e6;
  color: #8c3f4c;
}

.avatars__face--f {
  background: #f6eeda;
  color: #7a6224;
}

.avatars__face--g {
  background: #d9ecea;
  color: #2b6360;
}

.avatars__face--h {
  background: #e6e4f2;
  color: #4c4a80;
}

/* The reader's own face, filled so they can find themselves in the stack at a
   glance without reading initials. Outside the palette above on purpose - it
   marks "you", which is a different kind of fact from "which student". */
.avatars__face--me {
  background: var(--ink-900);
  color: var(--surface);
}

.avatars__more {
  margin-left: 8px;
  font-size: 9.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--ink-500);
}

/* --- Who is going (popover) ----------------------------------------------- */

.going {
  margin-bottom: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.going__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 9px;
}

.going__title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-400);
}

.going__count {
  font-size: 11.5px;
  color: var(--ink-500);
  font-variant-numeric: tabular-nums;
}

.going__people {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 11px;
}

/* Named, not just a face. The stack on the card answers "how many"; the popover
   is where "who" belongs, and initials alone do not answer it. */
.going__person {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px 3px 3px;
  background: var(--ink-100);
  border-radius: 999px;
  font-size: 11.5px;
  color: var(--ink-700);
}

.going__person .avatars__face {
  margin-right: 0;
  border-color: var(--ink-100);
}

.going__empty {
  margin: 0 0 11px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-500);
}

.going__book {
  width: 100%;
  justify-content: center;
}

.going__book:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* --- Footer --------------------------------------------------------------- */

/* --- Mobile --------------------------------------------------------------- */

@media (max-width: 720px) {
  .shell {
    padding: 0 14px 30px;
  }

  /* The controls row wraps here: the segmented switcher holds the first
     line and .stepper drops to a second. Anchor both to the left edge so
     the stepper sits under the switcher's start rather than floating in
     the middle of the card - .card__head's space-between leaves a lone
     wrapped line looking centred otherwise.

     .controls also goes full-width so its own left edge is the card's,
     not the shrink-to-fit width of the switcher above it. */
  .controls {
    width: 100%;
    justify-content: flex-start;
  }

  /* Month is a full-grid overview - useful with room to scan a whole page of
     cells, but that grid is what gets crushed first on a phone. Day/3 day/
     Week all still fit the segmented control; Month's button is hidden
     rather than removed, so a reader already in month view (from the `m`
     keyboard shortcut, say) keeps working, they just cannot switch INTO it
     from here. */
  [aria-label="Calendar view"] [data-view="month"] {
    display: none;
  }

  .timegrid {
    --hour-px: 44px !important;
    --gutter-w: 44px;
  }

  :root {
    --timegrid-body-h: calc(44px * 24);
  }

  /* No inner scrollbar: the day is short enough at this density that the page's
     own scroll carries the reader to the rest of it. Both axes go visible -
     CSS forces the paired axis into `auto` the instant either is non-visible,
     which would silently clip cards back off. */
  .timegrid__body {
    overflow: visible;
    padding-left: var(--gutter-w);
  }

  .timegrid__gutter {
    left: 0;
  }

  .calendar__day {
    min-height: 74px;
  }

  /* The keyboard/pointer hint row is desktop affordance copy: it names a
     hover-and-drag vocabulary ("drag empty time", "edges to resize", the
     D/W/M/T/Esc keys) that a phone has no way to perform, and the one
     line that does apply on touch ("hold to drag") is already discovered
     by holding. Seven wrapped items pushed the insight and the cards
     below it down a full screenful for guidance that cannot be acted on
     here, so the row is dropped at this width. */
  .timegrid-hints {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ============================================================
   Footer - ported from ../../css/footer.css so this page closes
   exactly the way the marketing page does.

   Ported rather than linked: footer.css opens with a global
   `* { margin: 0; padding: 0 }` reset and a body / h1-h6 typographic
   override, both of which would land on this app's own layout if the
   stylesheet were pulled in whole. Everything below is therefore scoped
   under .footer, and the font paths are rewritten - this file sits one
   directory deeper than footer.css, so ../fonts becomes ../../fonts.
   ============================================================ */

/* Sohne carries the strapline and the bottom bar. On the marketing page
   these faces are declared in problem-statement.css, which this page does
   not load - without them both lines would quietly fall back to Helvetica
   and the footer would not actually match. The heavier cut is its own
   family rather than a weight of "sohne" (that is how the other
   stylesheets declare it), so it has to be named to be used. */
@font-face {
  font-family: "sohne";
  src: url("../../problem-statement/font/sohne--normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "sohne-medium";
  src: url("../../problem-statement/font/sohne--medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Bellefair";
  src: url("../../fonts/bellefair/bellefair-regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "DMMono";
  src: url("../../fonts/dm-mono/dm-mono-medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Palace";
  src: url("../../fonts/palace/palace-regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* The two ink tokens the footer rules below are written against. footer.css
   declares these on :root for the marketing page; this page's own palette
   is the --ink-* ramp, so they are scoped to the footer rather than added
   globally where they would sit unused next to a second, conflicting set. */
.footer {
  --base-400: #171717;
  --base-500: #0f0f0f;

  position: relative;
  width: 100%;
  height: 100svh;
  padding: 0.75rem;
  overflow: hidden;
  background-color: #ffffff;
  color: var(--base-500);
}

/* The accent-green glow, bleeding up from the bottom edge - the mirror of
   the marketing page's hero glow. */
.footer::before {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 60%;
  background: radial-gradient(
      ellipse 45% 100% at 0% 100%,
      rgba(171, 255, 2, 0.32) 0%,
      rgba(171, 255, 2, 0.22) 20%,
      rgba(171, 255, 2, 0.12) 40%,
      rgba(171, 255, 2, 0.05) 60%,
      transparent 80%
    ),
    radial-gradient(
      ellipse 45% 100% at 100% 100%,
      rgba(171, 255, 2, 0.32) 0%,
      rgba(171, 255, 2, 0.22) 20%,
      rgba(171, 255, 2, 0.12) 40%,
      rgba(171, 255, 2, 0.05) 60%,
      transparent 80%
    );
  filter: blur(40px);
  pointer-events: none;
  z-index: 0;
}

.footer-wrapper {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 5000px;
  height: 100%;
  margin: 0 auto;
  padding: 0.75rem 0.75rem 0 0.75rem;
}

.footer-arc {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 120%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

.footer-arc-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.footer-arc-text {
  font-family: "Palace", serif;
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 5px;
  text-transform: uppercase;
  fill: var(--base-400);
}

.footer-arc-dot {
  fill: #7a9d1a;
  /* Matches .page-arc-dot - see the note there on why the star is
     stepped down, and on the fallback font this glyph actually
     renders in. */
  font-size: 0.70em;
}

/* `margin-top: auto` in a column flex container absorbs exactly the free
   space left after the content and bottom bar are laid out, which is what
   drops this below the arc's ascending tail without ever pushing the
   bottom bar past the footer's edge. */
.footer-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
  width: 100%;
  max-width: 2000px;
  margin: auto auto 0 auto;
  padding: 0.75rem;
}

/* Lifts the wordmark into the arc's bowl. A transform, not a negative
   margin: the parent sits on `margin-top: auto`, so the free space above
   would simply grow to re-absorb a margin and the mark would not move. */
.footer-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
  transform: translateY(clamp(-38rem, -58vh, -12.5rem));
}

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

/* Bellefair, and sized to match .page-title h1 on the marketing page. The
   uppercasing and tracking come from footer.css's global h1 rule there;
   this file has no global h1, so they are restated here. */
.footer-brand h1 {
  margin: 0;
  font-family: "Bellefair", "Palace", serif;
  font-size: clamp(2.75rem, 6.5vw, 6.5rem);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -3%;
  text-transform: uppercase;
  color: var(--base-500);
}

/* Sohne medium rather than the DM Mono the .mono class would otherwise
   give: the strapline sits directly under the wordmark, and a third
   typeface there reads as an accident. The selector carries .mono.sm to
   outrank the utilities below on specificity. */
.footer p.mono.sm.footer-brand-subtitle {
  margin-top: 0.5rem;
  /* The regular cut, not "sohne-medium" - matching footer.css, which this
     block is a port of. The Sohne weights are declared as SEPARATE
     families (sohne / sohne-medium / sohne-bold), not as weights of one,
     so the medium family here rendered a visibly heavier, larger-looking
     line than the same 1.1rem on the marketing page. Dropping
     font-weight alone would not fix it - the lighter family has to be
     named. */
  font-family: "sohne", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 1.1rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0.04em;
  color: var(--base-400);
  opacity: 0.7;
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  max-width: 2000px;
  margin: 0 auto;
  padding: 1.125rem 0;
  border-top: 1px solid var(--base-500);
}

.footer-bottom p {
  margin: 0;
  font-family: "sohne-medium", "sohne", "Helvetica Neue", Helvetica, Arial,
    sans-serif;
  font-weight: 500;
  font-size: 0.95rem;
  letter-spacing: 0.01em;
  color: var(--base-500);
}

/* Short viewports have a shallow bowl in absolute px while a vh-based lift
   is still large, so the full desktop pull drives the wordmark up through
   the crown. Eased off for them. */
@media (max-height: 800px) {
  .footer-center {
    transform: translateY(clamp(-22rem, -34vh, -7rem));
  }
}

@media (max-width: 1000px) {
  .footer {
    height: 65svh;
  }

  .footer::before {
    background: radial-gradient(
        ellipse 28% 100% at 0% 100%,
        rgba(171, 255, 2, 0.32) 0%,
        rgba(171, 255, 2, 0.22) 20%,
        rgba(171, 255, 2, 0.12) 40%,
        rgba(171, 255, 2, 0.05) 60%,
        transparent 80%
      ),
      radial-gradient(
        ellipse 28% 100% at 100% 100%,
        rgba(171, 255, 2, 0.32) 0%,
        rgba(171, 255, 2, 0.22) 20%,
        rgba(171, 255, 2, 0.12) 40%,
        rgba(171, 255, 2, 0.05) 60%,
        transparent 80%
      );
  }

  .footer-arc {
    top: 50%;
    width: 180%;
  }

  .footer-arc-text {
    font-size: 2rem;
  }

  /* The strapline is sized against the wordmark above it, and that mark
     shrinks with the viewport - so the 1.1rem that reads as a caption on
     desktop reads as body copy here. Ported from footer.css's rule at the
     same breakpoint, which this block was missing: without it the live
     page held 1.1rem on phones while the marketing page had already
     stepped down, and the two straplines no longer matched.

     Carries the same .mono.sm classes as the base rule: without them
     p.mono.sm (0,2,1) outranks a plain descendant and the size silently
     stays put. */
  .footer p.mono.sm.footer-brand-subtitle {
    font-size: 0.9rem;
    letter-spacing: 0.03em;
  }

  .footer-bottom p {
    font-size: 0.8rem;
  }

  .footer-content {
    gap: 3rem;
  }

  .footer-center {
    transform: translateY(clamp(-16rem, -24vh, -7rem));
  }
}
