:root {
  color-scheme: light;
  font-family: "Inter", sans-serif;
  --ink: oklch(0.14 0.01 250);
  --ink-strong: oklch(0.08 0.005 250);
  --muted: oklch(0.16 0.01 250);
  --glass: oklch(0.96 0.005 250 / 0.58);
  --glass-border: oklch(1 0 0 / 0.55);
  --blue: oklch(0.78 0.1 232);
  --red: oklch(0.58 0.22 25);
  /* .content-row/.content-left/.main-grid/.right-rail/.rail-subgrid/
     .profile-cards keep a fixed 22px/18px gap (not this variable) above
     STACK_BREAKPOINT because app.js's column-width math (GRID_GAP,
     PROFILE_CARD_GAP) assumes those exact values. Below the breakpoint
     that math doesn't run, so the @media (max-width: 900px) block
     switches all of them to this same fluid value as .kiosk-shell's own
     gap - otherwise, once every container stacks into one visual column
     of cards, the fixed-vs-fluid gaps next to each other look uneven.
     Keep this in sync with .kiosk-shell's "gap" value below. */
  --stack-gap: clamp(12px, 1.6vh, 22px);
  /* The ticket/maintenance/room rail and the announcement/news/quote
     stack both read as a tighter sub-group than the rest of the page by
     shaving 8px off --stack-gap (see .right-rail/.rail-subgrid below, and
     .news-row/.fact-row's matching margin-top). A flat -8px was fine
     while --stack-gap's own floor was 12px (net >= 4px), but the
     short-screen override below drops that floor to 8px - the same flat
     -8px against an 8px floor collapses the gap to 0, and the two
     sub-groups looked like they'd merged into one on a real laptop
     display. The max(4px, ...) floor keeps them visibly grouped without
     ever fully touching. */
  --group-gap: max(4px, calc(var(--stack-gap) - 8px));
  /* Dialogs sit on a near-white background, unlike the glass cards above -
     --ink/--muted are tuned for glass-over-photo contrast and read as
     near-black there, so dialogs get their own lighter gray scale. These
     consolidate ~8 near-duplicate one-off oklch() grays that were
     previously scattered across the dialog rules below. */
  --dialog-strong: oklch(0.2 0.02 250);
  --dialog-muted: oklch(0.45 0.02 250);
  --dialog-faint: oklch(0.5 0.02 250);
  --dialog-title-size: clamp(24px, 2.2vw, 32px);
  --dialog-label-size: clamp(11px, .8vw, 13px);
  --dialog-body-size: clamp(15px, 1.05vw, 18px);
  --dialog-meta-size: clamp(12px, .85vw, 15px);
}

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { position: relative; background: oklch(0.15 0.018 250); color: var(--ink); }
button { font: inherit; }

/* overflow-y: auto (not hidden) is a safety net: on very short/narrow
   screens where stacked widgets need more room than the viewport offers,
   the page scrolls instead of clipping content. */
.kiosk-viewport { position: fixed; inset: 0; overflow-x: hidden; overflow-y: auto; background: oklch(0.15 0.018 250); }

/* .kiosk-scale used to be a fixed 1920x1080 canvas, uniformly scaled via JS
   transform to fit the viewport (letterboxed). It's now a plain fluid
   container - the layout below reflows natively instead of being scaled. */
.kiosk-scale { position: relative; z-index: 2; width: 100%; height: auto; min-height: 100%; }

/* position: fixed (not absolute) is required here: .kiosk-viewport scrolls
   internally when stacked content is taller than the screen (see above),
   and an absolutely-positioned element scrolls along with its scrolling
   ancestor's content. Fixed positioning pins the background to the actual
   browser viewport instead, so it stays put and full-bleed no matter how
   far the content underneath scrolls. */
#background-layer {
  position: fixed;
  inset: 0;
  z-index: 0;
  background-color: oklch(0.15 0.018 250);
}
.bg-layer {
  position: absolute;
  inset: 0;
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
  opacity: 0;
  transition: opacity 1.2s ease;
}
.bg-layer.active { opacity: 1; }
.background-scrim { position: fixed; inset: 0; z-index: 1; pointer-events: none; background: linear-gradient(180deg, oklch(0.1 0.02 250 / 0.42), oklch(0.1 0.02 250 / 0.18) 35%, oklch(0.1 0.02 250 / 0.18) 65%, oklch(0.1 0.02 250 / 0.48)); }
.kiosk-shell {
  position: relative;
  width: 100%;
  height: auto;
  min-height: 100%;
  max-width: 2400px;
  margin: 0 auto;
  padding: clamp(18px, 2.6vw, 50px) clamp(18px, 3.1vw, 60px);
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap);
}

.datetime-header { display: flex; align-items: flex-end; justify-content: space-between; flex: 0 0 auto; flex-wrap: wrap; gap: 8px; }
#datetime-weekday { font-size: clamp(14px, 1.15vw, 22px); font-weight: 600; color: #fff; letter-spacing: .02em; text-shadow: 0 2px 12px oklch(0 0 0 / .5); }
#datetime-date { margin-top: 6px; font-size: clamp(20px, 1.875vw, 36px); font-weight: 700; color: #fff; letter-spacing: -.01em; text-shadow: 0 2px 12px oklch(0 0 0 / .5); }
#datetime-time { color: #fff; font-size: clamp(40px, 6.25vw, 120px); font-weight: 800; line-height: 1; letter-spacing: -.03em; font-variant-numeric: tabular-nums; text-shadow: 0 2px 16px oklch(0 0 0 / .5); }

.content-row { flex: 1; min-height: 0; display: flex; gap: 22px; }
.content-left { flex: 4 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 22px; }
.main-grid { flex: 1; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr); gap: 22px; }
.glass-card, .rail-card { color: var(--ink); background: var(--glass); border: 1px solid var(--glass-border); backdrop-filter: blur(14px) saturate(1.15); }
.primary-card { position: relative; min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; padding: clamp(16px, 1.7vw, 32px); border-radius: 28px; box-shadow: 0 20px 60px oklch(0 0 0 / .35); }
.card-glow { position: absolute; right: -50px; bottom: -60px; width: 180px; height: 180px; border-radius: 50%; pointer-events: none; }
.climate-card .card-glow { background: radial-gradient(circle, oklch(0.7 0.13 232 / .16), transparent 68%); }
.transit-card .card-glow { background: radial-gradient(circle, oklch(0.72 0.12 155 / .16), transparent 68%); }
.weather-card .card-glow { top: -60px; bottom: auto; width: 200px; height: 200px; background: radial-gradient(circle, oklch(0.78 0.13 75 / .16), transparent 68%); }
.card-heading { position: relative; z-index: 1; display: flex; align-items: center; gap: 14px; }
.card-heading h2 { margin: 0; font-size: clamp(16px, 1.15vw, 22px); font-weight: 700; }
.card-heading p { margin: 2px 0 0; color: var(--muted); font-size: clamp(12px, .8vw, 15px); }
.transit-route { position: relative; display: grid; grid-template-columns: 10px 1fr; align-items: center; row-gap: 4px; column-gap: 10px; }
.transit-route::before { content: ""; position: absolute; left: 4px; top: 14px; bottom: 10px; width: 0; border-left: 2px dotted var(--muted); opacity: .5; }
.transit-route-dot { position: relative; z-index: 1; box-sizing: border-box; width: 8px; height: 8px; border-radius: 50%; justify-self: center; }
.transit-route-dot--origin { background: var(--ink); }
.transit-route-dot--dest { background: transparent; border: 2px solid var(--muted); }
.transit-route-name { line-height: 1.15; }
.transit-route-name--origin { color: var(--ink); font-size: clamp(16px, 1.15vw, 22px); font-weight: 700; }
.transit-route-name--dest { color: var(--muted); font-size: clamp(12px, .8vw, 15px); font-weight: 500; }
.large-icon { position: relative; width: clamp(38px, 2.7vw, 52px); height: clamp(38px, 2.7vw, 52px); flex: 0 0 clamp(38px, 2.7vw, 52px); display: grid; place-items: center; border-radius: 16px; }
.climate-icon { color: oklch(0.78 0.1 232); background: oklch(0.4 0.09 232 / .35); }
.climate-icon span { position: relative; width: 11px; height: 24px; border: 3px solid currentColor; border-radius: 8px 8px 5px 5px; }
.climate-icon span::after { content: ""; position: absolute; width: 15px; height: 15px; left: -5px; bottom: -8px; border-radius: 50%; background: currentColor; }
.transit-icon { color: oklch(0.8 0.1 155); background: oklch(0.42 0.09 155 / .35); }
.transit-icon span { position: relative; width: 24px; height: 20px; border: 3px solid currentColor; border-radius: 6px; }
.transit-icon span::after { content: ""; position: absolute; left: 2px; bottom: -6px; width: 5px; height: 5px; border-radius: 50%; background: currentColor; box-shadow: 12px 0 currentColor; }
.weather-chip { background: oklch(0.44 0.1 75 / .35); }
.weather-chip img { width: 30px; height: 30px; }
.primary-value-block { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; justify-content: center; }
.primary-value { font-size: clamp(30px, 3.75vw, 72px); font-weight: 800; letter-spacing: -.02em; white-space: nowrap; }
.primary-sub { margin-top: 8px; color: var(--muted); font-size: clamp(14px, 1.15vw, 22px); }
.departure-list { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 14px; font-variant-numeric: tabular-nums; }
.departure-list > div { display: flex; justify-content: space-between; padding-bottom: 12px; border-bottom: 1px solid oklch(1 0 0 / .08); font-size: clamp(16px, 1.46vw, 28px); font-weight: 600; }
.departure-list > div:last-child { padding-bottom: 0; border-bottom: 0; }
.weather-card, #transit-widget { cursor: pointer; }
.weather-card:focus-visible, .rail-card:focus-visible, .profile-card:focus-visible, #transit-widget:focus-visible, .rotating-bar:focus-visible, .news-item:focus-visible { outline: 3px solid oklch(0.78 0.1 232); outline-offset: -4px; }
.weather-current { position: relative; z-index: 1; flex: 1; display: flex; align-items: center; gap: clamp(14px, 1.46vw, 28px); }
.weather-icon { width: clamp(56px, 5.7vw, 110px); height: clamp(56px, 5.7vw, 110px); }
.weather-readout { min-width: 0; flex: 1 1 0; }
.weather-value { font-size: clamp(32px, 3.96vw, 76px); font-weight: 800; line-height: 1; letter-spacing: -.02em; overflow-wrap: break-word; }
.weather-mini-forecast { position: relative; z-index: 1; display: flex; gap: clamp(10px, 1.25vw, 24px); min-height: 66px; padding-top: 20px; border-top: 1px solid oklch(1 0 0 / .08); }
.mini-forecast-day { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; }
.mini-forecast-day img { width: 30px; height: 30px; }
.mini-forecast-label { color: var(--muted); font-size: 15px; }
.mini-forecast-temp { margin-top: 2px; font-size: 19px; font-weight: 600; overflow-wrap: break-word; }

/* gap uses --group-gap (rather than the shared 22px GRID_GAP - unlike
   .content-row/.main-grid, nothing in app.js's column-width math depends
   on this one) so the ticket/maintenance/room-live cards read as just as
   tight a group as the announcement/news/quote stack, at any viewport
   height. .rail-subgrid's value is read live by app.js's layoutRailCards()
   rather than duplicated as a constant there, so it can't drift out of
   sync. */
.right-rail { flex: .95 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: var(--group-gap); }
.rail-subgrid { min-height: 0; display: flex; flex-direction: column; gap: var(--group-gap); }
.rail-card { width: 100%; height: 100%; min-height: 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; justify-content: center; gap: 8px; padding: clamp(14px, 1.35vw, 26px); border-radius: 24px; text-align: left; cursor: pointer; }
.room-live-card, .maintenance-live-card, .ticket-live-card { padding: clamp(12px, .94vw, 18px) clamp(14px, 1.15vw, 22px); }
/* Keep the maintenance status at a stable vertical position. The base card
   centers all content as one group, which made the status jump when the
   description below it wrapped onto a different number of lines. */
.maintenance-live-card { justify-content: flex-start; }
.rail-heading { display: flex; align-items: center; gap: 10px; }
.rail-icon { width: 34px; height: 34px; flex: 0 0 34px; display: grid; place-items: center; border-radius: 10px; }
.room-icon { color: oklch(0.78 0.1 232); background: oklch(0.4 0.09 232 / .35); }
.room-icon span { position: relative; width: 16px; height: 14px; border: 2px solid currentColor; border-radius: 4px; }
.room-icon span::before { content: ""; position: absolute; left: -2px; right: -2px; top: -5px; border-top: 2px solid currentColor; }
.maintenance-icon { color: oklch(0.82 0.1 75); background: oklch(0.45 0.11 75 / .35); }
.ticket-icon { color: oklch(0.8 0.12 300); background: oklch(0.42 0.14 300 / .35); }
.ticket-icon span { position: relative; width: 17px; height: 12px; border: 2px solid currentColor; border-radius: 3px; }
.ticket-icon span::before, .ticket-icon span::after { content: ""; position: absolute; top: 3px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
.ticket-icon span::before { left: -4px; } .ticket-icon span::after { right: -4px; }
.rail-label { color: var(--muted); font-size: clamp(11px, .73vw, 14px); text-transform: uppercase; letter-spacing: .06em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.rail-dots { display: flex; gap: 6px; flex: 0 0 auto; margin-left: auto; }
.rail-dots .dot { width: 6px; height: 6px; border-radius: 50%; background: oklch(0.16 0.01 250 / .18); }
.rail-dots .dot.active { background: var(--red); }
.rail-card strong { font-size: clamp(15px, 1.25vw, 24px); font-weight: 700; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
.room-live-card.free strong { color: var(--muted); font-size: clamp(14px, 1.04vw, 20px); font-weight: 600; }
.room-live-card:disabled { cursor: default; }
.time-pill { width: max-content; padding: 5px 12px; border-radius: 999px; color: var(--red); background: oklch(0.93 0.06 25); font-size: clamp(12px, .83vw, 16px); font-weight: 700; font-variant-numeric: tabular-nums; }
.rail-sub { color: var(--muted); font-size: clamp(12px, .78vw, 15px); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
/* Wraps instead of truncating: a fixed-height card (see layoutRailCards in
   app.js) has the vertical room for a short phrase like "Overdue by 17
   days" to wrap onto a second line, which reads better than an ellipsis
   mid-word and reflows more predictably while the column is resized. This
   is also a <strong> inside .rail-card, so it must explicitly undo the
   .rail-card strong line-clamp above (display/overflow/-webkit-line-clamp)
   or that rule would silently clamp it too. */
#maintenance-live-value { color: var(--red); font-size: clamp(14px, 1.15vw, 22px); font-weight: 700; white-space: normal; overflow-wrap: break-word; display: block; overflow: visible; -webkit-line-clamp: none; }
.ticket-live-card strong { font-size: clamp(14px, 1.15vw, 22px); color: var(--red); }

.profile-cards { flex: 0 0 190px; display: flex; gap: 18px; overflow-x: auto; overflow-y: hidden; }
.profile-card { flex: 0 0 min(364px, 86vw); min-width: 0; display: flex; flex-direction: column; gap: 10px; padding: 20px 22px; border-radius: 26px; color: var(--ink); cursor: pointer; background: oklch(0.96 0.005 250 / .58); border: 1px solid oklch(1 0 0 / .5); box-shadow: 0 16px 40px oklch(0 0 0 / .18); text-align: left; backdrop-filter: blur(14px) saturate(1.15); }
.profile-top { display: flex; align-items: center; gap: 14px; }
.avatar { width: 56px; height: 56px; flex: 0 0 56px; overflow: hidden; display: grid; place-items: center; border: 2px solid oklch(1 0 0 / .7); border-radius: 50%; color: oklch(0.98 0.005 250); background: oklch(0.4 0.09 232 / .4); font-size: 24px; font-weight: 700; }
.avatar-tone-2 { background: oklch(0.55 0.11 155 / .4); }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.profile-info { min-width: 0; }
.profile-eyebrow { overflow: hidden; color: oklch(0.16 0.1 232); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; white-space: nowrap; text-overflow: ellipsis; }
.profile-name { margin-top: 4px; overflow: hidden; color: var(--ink-strong); font-size: 24px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
/* min-height (not just font-size) because this line is sometimes empty
   (visitor with no company, onboarding profile with no role) - an empty
   block collapses to 0 height, which shifts the divider below it up and
   breaks row-to-row alignment across the profile-cards row. */
.profile-role { margin-top: 2px; color: var(--ink); font-size: 16px; line-height: 1.2; min-height: 1.2em; }
.profile-fact { padding-top: 8px; border-top: 1px solid oklch(0.14 0.01 250 / .16); color: oklch(0.12 0.01 250); font-size: 15px; font-style: italic; }
.visitor-eyebrow { color: oklch(0.5 0.19 25); }
.visitor-details { display: flex; flex-direction: column; gap: 6px; padding-top: 8px; border-top: 1px solid oklch(0.14 0.01 250 / .16); }
.visitor-host-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
.visitor-host-label { color: var(--muted); font-size: 15px; font-weight: 600; }
.visitor-host-value { min-width: 0; color: var(--ink-strong); font-size: 18px; font-weight: 600; }
.visitor-details .profile-fact { padding-top: 0; border-top: none; }
.visitor-card .avatar { background: oklch(0.58 0.22 25 / .35); }

/* margin-top (on top of .kiosk-shell's own gap) separates this from
   .content-row above, and the negative margin-top on .news-row/.fact-row
   below pulls the three ticker widgets closer together - together these
   read as one distinct group under the main dashboard grid instead of
   being evenly spaced like every other row in the stack. */
.announcement-row { flex: 0 0 clamp(60px, 7.4vh, 80px); display: flex; margin-top: 14px; }
.rotating-bar { flex: 1; display: flex; align-items: center; gap: clamp(10px, 1.85vw, 20px); min-width: 0; padding: clamp(10px, 1.3vw, 14px) clamp(16px, 3vw, 32px); border-radius: 24px; background: oklch(0.42 0.1 232 / .55); border: 1px solid oklch(1 0 0 / .45); overflow: hidden; cursor: pointer; backdrop-filter: blur(14px) saturate(1.15); }
.rotating-icon { font-size: 22px; line-height: 1; }
.rotating-icon-chip { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 14px; background: oklch(0.4 0.09 232 / .4); display: grid; place-items: center; }
.rotating-eyebrow { flex: 0 0 auto; color: #fff; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.rotating-divider { width: 1px; align-self: stretch; background: oklch(1 0 0 / .25); flex: 0 0 1px; }
.rotating-message { flex: 1; min-width: 0; color: #fff; font-size: clamp(18px, 1.67vw, 32px); font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; opacity: 1; transition: opacity .3s ease; text-shadow: 0 1px 8px oklch(0 0 0 / .35); }
.rotating-message.fading { opacity: 0; }
.rotating-message.warning { color: var(--red); }
.rotating-dots { display: flex; flex: 0 0 auto; gap: 8px; }
.rotating-dots .dot { width: 8px; height: 8px; border-radius: 50%; background: oklch(1 0 0 / .3); }
.rotating-dots .dot.active { background: #fff; }

/* Sits between the announcement bar and the fact row - deliberately
   shorter than both, since it only ever needs to hold a topic label, a
   headline clamped to 2 lines, and a source/time meta line. flex-basis is
   vw-driven (not vh, unlike the other ticker rows) because the content
   that determines its height - .news-headline's font-size and .news-panel's
   padding - are themselves vw-clamped; a vh-based basis drifted out of
   sync with that and measured 10-24px too short for a real 2-line headline
   (Playwright, 1024-1920px wide), so a long headline forced the row taller
   than its flex-basis and pushed the fact row down. overflow: hidden is a
   backstop so that can't happen again even outside the measured range. */
.news-row { flex: 0 0 clamp(90px, calc(80px + 3vw), 138px); display: flex; margin-top: calc(var(--group-gap) - var(--stack-gap)); overflow: hidden; }
.news-panel { flex: 1; display: flex; align-items: center; gap: clamp(14px, 2vw, 28px); min-width: 0; padding: clamp(12px, 1.6vw, 20px) clamp(18px, 3vw, 32px); border-radius: 24px; background: var(--glass); border: 1px solid var(--glass-border); backdrop-filter: blur(14px) saturate(1.15); }
.news-icon-chip { flex: 0 0 40px; width: 40px; height: 40px; border-radius: 14px; background: oklch(0.4 0.1 170 / .35); color: oklch(0.3 0.12 170); display: grid; place-items: center; }
.news-item { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 3px; cursor: pointer; border-radius: 12px; }
.news-eyebrow { color: oklch(0.32 0.12 170); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; }
.news-headline { color: var(--ink); font-weight: 600; line-height: 1.25; font-size: clamp(16px, 1.2vw, 21px); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; opacity: 1; transition: opacity .3s ease; }
.news-headline.fading { opacity: 0; }
.news-meta { color: var(--muted); font-size: clamp(11px, .75vw, 13px); }
.news-divider { width: 1px; align-self: stretch; background: oklch(0.14 0.01 250 / .22); flex: 0 0 1px; }

.fact-row { flex: 0 0 clamp(140px, 18vh, 196px); display: flex; position: relative; margin-top: calc(var(--group-gap) - var(--stack-gap)); }
/* justify-content: flex-start (not center) so the label's position depends
   only on the panel's fixed padding, never on how many lines the following
   text wraps to — centering here caused the label to jump between facts
   of different lengths. */
.fact-panel { flex: 1; cursor: pointer; text-align: left; border: 1px solid oklch(1 0 0 / .45); background: var(--glass); display: flex; flex-direction: column; justify-content: flex-start; min-width: 0; padding: clamp(14px, 1.85vw, 20px) clamp(18px, 3vw, 32px); padding-top: clamp(20px, 2.4vw, 28px); border-radius: 24px; overflow: hidden; font: inherit; color: inherit; backdrop-filter: blur(14px) saturate(1.15); }
.fact-label { color: oklch(0.22 0.14 300); font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; margin-bottom: 12px; }
.fact-text { color: var(--ink); font-weight: 500; line-height: 1.35; font-size: clamp(17px, 1.4vw, 22px); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; opacity: 1; transition: opacity .3s ease; margin-bottom: 8px; }
.fact-text.fading { opacity: 0; }
/* margin-top: auto pushes the sub-line to consume all remaining flex space
   above it, anchoring it to the panel's bottom-right corner for both fact
   types alike (quote attribution and on-this-day's usually-empty sub);
   margin-bottom clears #fact-credit's pill, which sits absolutely
   positioned in that same corner (see #fact-credit below). */
.fact-sub { color: var(--muted); font-size: 16px; margin-top: auto; margin-bottom: 28px; text-align: right; }
.fact-expand-hint { font-size: 12px; font-weight: 600; color: oklch(0.35 0.14 300); }

/* Quote of the Day and On This Day share the same layout (label always on
   top) so the widget doesn't jump when it rotates between fact types; they're
   distinguished only by style: quote text is italic, and the on-this-day
   label uses --blue's hue rather than the quote label's violet. */
.fact-panel[data-type="quote"] .fact-text { font-style: italic; }
.fact-panel[data-type="onthisday"] .fact-label { color: oklch(0.22 0.14 232); }

/* ZenQuotes free-tier terms require attribution with a link back to
   zenquotes.io; sibling of the fact-panel button (not nested inside it,
   since buttons can't legally contain interactive <a> elements) but
   visually anchored to its corner, same subtle "fine print" idea as
   #background-credit below. */
#fact-credit { position: absolute; right: clamp(14px, 1.8vw, 22px); bottom: clamp(10px, 1.3vw, 14px); z-index: 2; padding: 3px 9px; border-radius: 999px; background: oklch(1 0 0 / .55); color: var(--muted); font-size: 11px; line-height: 1.3; backdrop-filter: blur(6px); }
#fact-credit a { color: inherit; text-decoration-color: oklch(0.16 0.01 250 / .45); }
#fact-credit:empty { display: none; }

.detail-dialog { width: min(680px, 92vw); max-height: 88vh; overflow: auto; padding: 0 clamp(22px, 4.2vw, 40px) clamp(22px, 3.75vw, 36px); border: 0; border-radius: 26px; color: var(--ink); background: oklch(0.98 0.005 250); box-shadow: 0 30px 90px oklch(0 0 0 / .4); }
.detail-dialog::backdrop { background: oklch(0.1 0.02 250 / .55); }
.dialog-header { position: sticky; top: 0; z-index: 5; display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; padding-top: clamp(22px, 3.75vw, 36px); padding-bottom: 14px; border-bottom: 1px solid oklch(0.14 0.01 250 / .08); background: oklch(0.98 0.005 250); }
.dialog-heading { display: flex; align-items: center; gap: 14px; min-width: 0; }
.dialog-heading > div { min-width: 0; }
.dialog-icon-chip { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 14px; background: oklch(0.94 0.015 250); display: grid; place-items: center; font-size: 22px; }
.dialog-icon-chip:empty { display: none; }
.dialog-label { color: var(--dialog-muted); font-size: var(--dialog-label-size); font-weight: 700; text-transform: uppercase; letter-spacing: .07em; }
.dialog-header h2 { margin: 6px 0 0; font-size: var(--dialog-title-size); overflow-wrap: anywhere; }
.dialog-close { width: 44px; height: 44px; flex: 0 0 44px; border: 1.5px solid oklch(0.14 0.01 250 / .25); border-radius: 50%; background: transparent; color: var(--dialog-strong); font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.detail-list { margin-top: 26px; display: grid; gap: 14px; }
/* Prepended/appended into #operational-dialog-body only by showNewsDetails()
   - every other dialog (transit, rooms, tickets, etc.) only ever populates
   .detail-list rows, so these rules have no effect on them. */
.news-dialog-snippet { margin: 0 0 22px; font-size: var(--dialog-body-size); line-height: 1.55; color: var(--dialog-strong); }
.news-dialog-snippet em { font-style: normal; font-weight: 700; background: oklch(0.86 0.14 95 / .55); padding: 0 3px; border-radius: 4px; }
.news-dialog-snippet-empty { color: var(--dialog-faint); font-style: italic; }
.news-dialog-qr { margin: 4px 0 22px; display: flex; align-items: center; gap: 16px; }
.news-dialog-qr img { border-radius: 8px; background: #fff; padding: 6px; }
.news-dialog-qr-caption { margin: 0; font-size: var(--dialog-meta-size); color: var(--dialog-muted); }
.detail-row, .profile-detail { display: flex; justify-content: space-between; gap: 20px; padding-bottom: 12px; border-bottom: 1px solid oklch(0.14 0.01 250 / .1); font-size: var(--dialog-body-size); }
.detail-row span, .profile-detail span { color: var(--dialog-muted); }
.detail-row strong, .profile-detail strong { text-align: right; }
.weather-dialog { width: min(760px, 92vw); }
.forecast-list { margin-top: 22px; }
.forecast-row { display: grid; grid-template-columns: 100px 36px minmax(0, 1fr) 118px 100px; gap: 14px; align-items: center; min-height: 62px; border-bottom: 1px solid oklch(0.14 0.01 250 / .1); }
.forecast-row img { width: 38px; height: 38px; }
.forecast-condition { color: var(--dialog-muted); }
.forecast-temperatures { width: 118px; display: grid; grid-template-columns: 52px 8px 52px; align-items: baseline; justify-content: end; gap: 3px; color: var(--dialog-strong); text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.forecast-temp { display: inline-flex; align-items: baseline; gap: 3px; }
.forecast-high, .forecast-low { justify-content: flex-end; }
.forecast-temp-label { color: var(--dialog-faint); font-size: 13px; font-weight: 700; line-height: 1; }
.forecast-temp strong { color: var(--dialog-strong); font-size: var(--dialog-body-size); line-height: 1; }
.forecast-temp-divider { color: oklch(0.14 0.01 250 / .28); font-weight: 700; }
.forecast-low strong { color: var(--dialog-muted); font-weight: 600; }
.forecast-rain { text-align: right; }
.profile-dialog-identity { display: flex; align-items: center; gap: 16px; }
.profile-dialog-photo { width: 72px; height: 72px; flex: 0 0 72px; overflow: hidden; display: grid; place-items: center; border-radius: 50%; color: #fff; background: oklch(0.5 0.14 232); font-size: 28px; font-weight: 700; }
.profile-dialog-photo img { width: 100%; height: 100%; object-fit: cover; }
.profile-dialog-role { margin-top: 3px; color: var(--dialog-muted); }
.profile-dialog-fact { margin-top: 24px; padding: 16px; border-radius: 12px; background: oklch(0.95 0.005 250); font-size: var(--dialog-body-size); }
.profile-dialog-details { grid-template-columns: 1fr; }
.ticket-dialog { width: min(860px, 92vw); max-height: 80vh; overflow: auto; }
/* The rail icon is designed for a dark photo-backed card. In the light
   dialogs that same translucent palette made the ticket outline nearly
   disappear, so the dialog version uses a stronger violet foreground. */
.ticket-dialog .ticket-icon, .ticket-item-dialog .ticket-icon { color: oklch(0.48 0.2 300); background: oklch(0.9 0.065 300); }
.ticket-detail-list { margin-top: 24px; display: grid; gap: 12px; }
.ticket-detail-row { width: 100%; display: grid; grid-template-columns: 110px 1fr auto; gap: 16px; align-items: center; padding: 14px 0; border: 0; border-bottom: 1px solid oklch(0.14 0.01 250 / .1); color: inherit; background: transparent; text-align: left; cursor: pointer; }
.ticket-detail-row:last-child { border-bottom: 0; }
.ticket-detail-row:hover { background: oklch(0.94 0.02 232 / .65); }
.ticket-detail-row:focus-visible { outline: 3px solid oklch(0.7 0.13 232); outline-offset: 2px; }
.ticket-number { color: var(--dialog-muted); font-size: var(--dialog-meta-size); font-weight: 700; }
.ticket-copy { min-width: 0; }
.ticket-title { display: -webkit-box; overflow: hidden; font-size: var(--dialog-body-size); font-weight: 700; line-height: 1.25; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
.ticket-meta { margin-top: 4px; color: var(--dialog-muted); font-size: var(--dialog-meta-size); }
.ticket-badge { padding: 5px 10px; border-radius: 999px; background: oklch(0.93 0.025 250); font-size: var(--dialog-meta-size); font-weight: 700; white-space: nowrap; }
.ticket-badge.overdue { color: oklch(0.48 0.2 25); background: oklch(0.93 0.06 25); }
.ticket-badge.due_today { color: oklch(0.48 0.14 65); background: oklch(0.94 0.06 75); }
.ticket-item-dialog { width: min(720px, 92vw); overflow: auto; border-top: 7px solid oklch(0.65 0.04 250); }
.ticket-item-dialog.ticket-priority-urgent { border-top-color: oklch(0.58 0.22 25); }
.ticket-item-dialog.ticket-priority-high { border-top-color: oklch(0.67 0.17 50); }
.ticket-item-dialog.ticket-priority-medium { border-top-color: oklch(0.78 0.15 85); }
.ticket-item-dialog.ticket-priority-low { border-top-color: oklch(0.66 0.1 232); }
.ticket-item-dialog .dialog-label { color: oklch(0.43 0.08 250); font-size: var(--dialog-label-size); }
.ticket-item-dialog .dialog-header h2 { max-width: 610px; margin-top: 10px; color: oklch(0.14 0.02 250); font-size: var(--dialog-title-size); line-height: 1.2; overflow-wrap: anywhere; }
.ticket-item-badges { margin-top: 22px; display: flex; flex-wrap: wrap; gap: 10px; padding-bottom: 22px; border-bottom: 1px solid oklch(0.14 0.01 250 / .1); }
.ticket-state-badge { display: inline-flex; align-items: center; gap: 9px; padding: 8px 14px; border: 1px solid transparent; border-radius: 999px; font-size: var(--dialog-meta-size); font-weight: 750; }
.ticket-state-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 3px currentColor; }
.ticket-state-badge.status-open { color: oklch(0.52 0.2 25); background: oklch(0.96 0.035 25); border-color: oklch(0.82 0.1 25); }
.ticket-state-badge.status-in_progress { color: oklch(0.48 0.14 245); background: oklch(0.95 0.035 245); border-color: oklch(0.82 0.08 245); }
.ticket-state-badge.status-waiting { color: oklch(0.54 0.14 75); background: oklch(0.96 0.045 80); border-color: oklch(0.84 0.1 80); }
.ticket-state-badge.status-closed { color: oklch(0.47 0.12 155); background: oklch(0.95 0.035 155); border-color: oklch(0.82 0.08 155); }
.ticket-state-badge.priority-urgent { color: oklch(0.5 0.22 25); background: oklch(0.94 0.055 25); border-color: oklch(0.78 0.14 25); }
.ticket-state-badge.priority-high { color: oklch(0.53 0.16 50); background: oklch(0.96 0.05 60); border-color: oklch(0.83 0.11 55); }
.ticket-state-badge.priority-medium { color: oklch(0.51 0.13 80); background: oklch(0.97 0.05 88); border-color: oklch(0.84 0.11 85); }
.ticket-state-badge.priority-low { color: oklch(0.45 0.11 232); background: oklch(0.96 0.035 232); border-color: oklch(0.82 0.08 232); }
.ticket-item-dialog .detail-list { gap: 0; }
.ticket-item-dialog .detail-row { align-items: center; min-height: 54px; padding: 13px 4px; }
.ticket-item-dialog .detail-row span { font-size: var(--dialog-meta-size); font-weight: 600; text-transform: uppercase; letter-spacing: .045em; }
.ticket-item-dialog .detail-row strong { max-width: 70%; color: var(--dialog-strong); font-size: var(--dialog-body-size); }

/* Both dialogs are width: min(..., 92vw), so their available content width
   tracks viewport width directly - below this, the fixed-px columns above
   (110px for tickets, 100px+36px for forecast) leave too little room for
   the 1fr text column to stay readable. Instead of shrinking further,
   each row becomes two lines: a compact top line for the fixed-width bits
   (ticket number + badge, or date + icon + rain%), and the full-width
   text (title, or condition + temperatures) on its own line below. */
@media (max-width: 480px) {
  .ticket-detail-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "number . badge" "copy copy copy";
    row-gap: 6px;
  }
  .ticket-number { grid-area: number; }
  .ticket-badge { grid-area: badge; }
  .ticket-copy { grid-area: copy; }

  .forecast-row {
    grid-template-columns: 32px 1fr auto;
    grid-template-areas: "icon date rain" "icon condition temps";
    row-gap: 4px;
    align-items: start;
    padding: 10px 0;
  }
  .forecast-row img { grid-area: icon; align-self: center; width: 28px; height: 28px; }
  .forecast-row > strong:first-child { grid-area: date; }
  .forecast-condition { grid-area: condition; }
  .forecast-temperatures { grid-area: temps; justify-content: flex-end; text-align: right; }
  .forecast-rain { grid-area: rain; text-align: right; font-size: 13px; }
}

#background-credit {
  position: fixed;
  right: clamp(10px, 1.67vw, 32px);
  bottom: clamp(10px, 1.25vw, 24px);
  z-index: 4;
  max-width: min(420px, 70vw);
  padding: 6px 10px;
  border-radius: 8px;
  color: oklch(1 0 0 / .92);
  background: oklch(0.08 0.01 250 / .58);
  box-shadow: 0 3px 12px oklch(0 0 0 / .2);
  font-size: 12px;
  line-height: 1.3;
  white-space: normal;
  backdrop-filter: blur(5px);
}
#background-credit:empty { display: none; }
#background-credit a { color: inherit; text-decoration-color: oklch(1 0 0 / .5); }
.hidden { display: none !important; }

/* Below ~900px there isn't room for the weighted side-by-side columns
   app.js computes (see updateMainGridColumns) to stay readable, so this
   breakpoint switches to a single stacked column instead. app.js checks
   the same 900px threshold and clears its inline width/flex overrides in
   that mode so these rules (rather than leftover inline styles) apply. */
@media (max-width: 900px) {
  .content-row { flex-direction: column; }
  .content-left, .right-rail { flex: 1 1 auto; width: 100%; }
  .main-grid { display: flex; flex-direction: column; }
  .primary-card { min-height: 190px; }
  .rail-card { min-height: 90px; }

  /* Every one of these containers stacks into what reads as a single
     vertical column of cards down here, so their gaps need to visually
     match each other and .kiosk-shell's own gap - otherwise the fixed
     22px/18px they use above the breakpoint (kept fixed there to match
     app.js's GRID_GAP/PROFILE_CARD_GAP column-width math) looks uneven
     next to the fluid gap everywhere else once that math no longer runs. */
  .content-row, .content-left, .main-grid, .right-rail, .rail-subgrid, .profile-cards {
    gap: var(--stack-gap);
  }

  /* .right-rail/.rail-subgrid's own gap just switched from the tightened
     --group-gap to the plain .kiosk-shell-matching value above - undo
     the matching --group-gap margin-top squeeze on .news-row/.fact-row here too,
     so the announcement/news/quote group keeps pace with that same switch
     instead of staying tighter than the rail group only at these widths. */
  .news-row, .fact-row { margin-top: 0; }

  /* Only one profile/visitor card is shown at a time down here (see
     profileRowCapacity in app.js) - let it take the full row width and
     match the other stacked cards' height instead of staying pinned to
     the short 190px strip it uses in the multi-card desktop row.
     flex-basis (set via the "flex" shorthand on the base .profile-card
     rule) wins over a plain "width" override for a flex item's size, so
     this has to override "flex" itself - a bare "width: 100%" here would
     be silently ignored and the card would stay at its desktop
     min(364px, 86vw) size. */
  .profile-cards { flex: 0 0 auto; overflow: visible; }
  .profile-card { flex: 1 1 100%; min-height: 190px; }

  /* The eyebrow label and divider are fixed chrome that eats a big chunk
     of a narrow phone's width before the actual message even starts,
     leaving too little room for it to stay readable. Dropping them (the
     icon alone still signals "this is an announcement") gives the message
     the width it needs, and the row allows up to 3 wrapped lines instead
     of clipping at a fixed short height. Height is still a fixed px (not
     "auto") sized for that worst case - measured live via Playwright at
     320-900px, a forced 3-line message tops out at 97px - so the row
     never grows/shrinks between a one-line and three-line message; short
     messages just leave more empty space instead of shrinking the row. */
  .rotating-eyebrow, .rotating-divider { display: none; }
  .announcement-row { flex: 0 0 104px; padding: 6px 0; }
  .rotating-message { -webkit-line-clamp: 3; }

  /* Only room for one headline down here - the second slot and its divider
     drop out rather than squeezing two illegibly narrow columns side by
     side, and the remaining slot gets an extra clamped line since there's
     no neighbor competing for width. Same fixed-height reasoning as
     .announcement-row above: a forced 3-line headline measured 121px
     across 320-900px widths (font is already pinned to its 16px clamp
     floor down here), so the row is fixed at that worst case rather than
     "auto" - otherwise the widget visibly grows/shrinks depending on
     which headline happens to be showing. */
  .news-item-secondary, .news-divider { display: none; }
  .news-row { flex: 0 0 128px; }
  .news-headline { -webkit-line-clamp: 3; }
}

/* 600-900px (portrait tablets, e.g. a 810x1080 iPad) still can't fit
   app.js's weighted 4-column split (see STACK_BREAKPOINT in app.js) any
   better than a phone can, but they have far more width to spare than a
   phone does - the phone-tuned single-column stack above piles all 6
   cards (3 main + 3 rail) into one column and pushes ~800px of content
   below the fold. Rearranging each group into rows (without touching
   app.js's column math, which stays off for the whole sub-900px range)
   recovers most of that height inside a viewport whose width alone is
   routinely 700-830px in this range. */
@media (min-width: 600px) and (max-width: 900px) {
  /* Climate/transit pair up top; weather gets the full-width second row
     to itself instead of a 1/3 column - its mini-forecast needs three
     day-columns of its own (icon + label + temps) and overlapped/clipped
     text at a plain 1/3 share. */
  .main-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .weather-card { grid-column: 1 / -1; }
  .primary-card { min-height: 0; }

  /* display: contents unwraps .rail-subgrid so its two children
     (maintenance/room) join .ticket-live-card as flat siblings in the
     grid below - without it, the grid would only ever see two items
     (.ticket-live-card and the .rail-subgrid box itself) and give the
     nested maintenance/room pair a column each of its own instead of
     lining up with the main cards' row. */
  .right-rail { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--stack-gap); }
  .rail-subgrid { display: contents; }
  .rail-card { min-height: 0; }

  /* Both trims below only shave the slack a short tablet viewport doesn't
     have room for - neither lowers a safety floor tuned for content
     (.fact-row's 140px minimum, .kiosk-shell's own gap on wider/shorter
     screens), so nothing here can newly clip text that fit before. */
  .kiosk-shell { gap: clamp(8px, 1.1vh, 22px); }
  .fact-row { flex-basis: clamp(140px, 13vh, 196px); }
}

/* A landscape tablet (e.g. a 1180x820 iPad) is wide enough to keep
   app.js's normal >900px side-by-side columns - the widget stacking above
   is irrelevant here - but is just as short as a portrait one, so the same
   two height-driven trims apply on their own, independent of width. Kept
   as a separate query (rather than folded into the one above) since this
   one must also reach the >900px band the width query intentionally
   excludes. */
@media (max-height: 900px) {
  /* Redefines the --stack-gap custom property itself, inherited by every
     descendant below (.right-rail/.rail-subgrid's own calc(--stack-gap -
     8px) rule, and the max-width:900px block's plain var(--stack-gap)
     rule), rather than overriding .kiosk-shell's own "gap" property with
     its own separate clamp() literal - the rail/ticker spacing match this
     file otherwise goes out of its way to keep (see --stack-gap's own
     definition comment above) depends on every consumer of "the current
     stack gap" reading the exact same value, at every combination of
     width and height, not a hand-copied literal that can drift out of
     sync with it. */
  .kiosk-shell { --stack-gap: clamp(8px, 1.1vh, 22px); padding-top: clamp(10px, 2vh, 30px); padding-bottom: clamp(10px, 2vh, 30px); }

  /* Everything below this point was still sized purely off vw (width),
     never vh - fine for a short-but-narrow phone, where the max-width:900px
     block above already shrinks it, but a short-and-WIDE screen (a laptop's
     built-in display, no external monitor) has plenty of width to keep
     these at their large vw-driven size while having no vertical room to
     spare, so the page overflowed the window and fell back to the
     .kiosk-viewport scroll safety net even at ordinary desktop widths.
     Measured live at 1440x818 (a 13" MacBook window minus title bar): the
     six rows below totalled 950px against an 818px viewport before this
     block, 132px of forced scrolling. These clamp()s reuse the vh-based
     approach above instead so a short window shrinks them too, regardless
     of width. */
  #datetime-time { font-size: clamp(32px, 8vh, 120px); }
  /* padding/.large-icon/.primary-value use clamp(floor, calc(100vh +
     originalVwTerm - 900px), ceiling) rather than a flat vh value - climate
     ("Office climate") and transit ("Uster, Linde") were the two cards
     reported as visibly snapping on resize (2026-08-14), and this is why:
     a flat vh value is a *different formula* than the un-queried vw-based
     rule, so it only ever agreed with it by coincidence at one specific
     height, and visibly jumped everywhere else when a resize crossed the
     900px line. calc(100vh + originalVwTerm - 900px) instead is
     guaranteed to equal exactly originalVwTerm when height is exactly
     900px, for any width (100vh is then literally 900px, so it cancels
     out) - below 900px it's smaller, so clamp()'s floor catches it, and
     stays pinned at that floor until height gets within
     ceiling-floor of 900px. Climate/transit/weather sit in one CSS Grid
     row and stretch to match whichever is tallest (default grid item
     align-items:stretch) - so even though only climate/transit's own
     content was reported as jumpy, weather's still-unfixed icon/value
     below kept dragging their rendered *box* height along with it
     regardless (confirmed live: climate/transit's own font/padding/icon
     stopped jumping first, but their card height didn't, until weather's
     were fixed too) - all three have to move together here for the same
     reason .content-row's own stretch to .right-rail required the rail
     fix earlier. */
  .primary-card { padding: clamp(12px, calc(100vh + 1.7vw - 900px), 32px); }
  .large-icon { width: clamp(30px, calc(100vh + 2.7vw - 900px), 52px); height: clamp(30px, calc(100vh + 2.7vw - 900px), 52px); flex-basis: clamp(30px, calc(100vh + 2.7vw - 900px), 52px); }
  .weather-icon { width: clamp(40px, calc(100vh + 5.7vw - 900px), 110px); height: clamp(40px, calc(100vh + 5.7vw - 900px), 110px); }
  .weather-value { font-size: clamp(26px, calc(100vh + 3.96vw - 900px), 76px); }
  .weather-mini-forecast { min-height: clamp(48px, calc(100vh - 834px), 66px); padding-top: clamp(12px, calc(100vh - 880px), 20px); }
  /* .primary-value (climate/transit's big readout, e.g. "31.4°C") is
     .weather-value's twin on the other two primary cards - same vw-only
     blind spot, and the most visible jump of the three (biggest font on
     the card), which is why it's fixed here even though .weather-value
     itself isn't yet. */
  .primary-value { font-size: clamp(22px, calc(100vh + 3.75vw - 900px), 72px); }

  /* The climate/transit/weather primary cards sit in one CSS Grid row and
     stretch to match whichever is tallest (default grid item
     align-items:stretch) - .departure-list (3 bus rows) turned out to be
     that tallest one, and entirely vw/fixed-px sized (185px measured at
     1366x768), taller than either of the other two cards' own content even
     after the trims above. Shrinking it is what actually caps the whole
     card row's height on a short screen, more than any single property on
     weather/climate did. Same calc()-ramp technique as above for the same
     reason - .departure-list is transit's ("Uster, Linde") own content. */
  .departure-list { gap: clamp(6px, calc(100vh - 886px), 14px); }
  .departure-list > div { padding-bottom: clamp(6px, calc(100vh - 888px), 12px); font-size: clamp(14px, calc(100vh + 1.46vw - 900px), 28px); }

  /* .news-row keeps its own overflow:hidden backstop (see the comment on
     its base rule) so a too-tall headline clips instead of pushing the
     fact row down - but that only stays invisible if .news-headline's font
     shrinks in step with the row's own basis. Both are pinned to the same
     8.5vh mid-value for that reason - don't change one without the other. */
  .news-row { flex-basis: clamp(64px, 8.5vh, 138px); }
  .news-panel { padding: clamp(8px, 1.6vh, 20px) clamp(14px, 2.4vh, 32px); }
  .news-icon-chip { flex-basis: 32px; width: 32px; height: 32px; }
  .news-headline { font-size: clamp(13px, 1.9vh, 21px); }
  .news-meta { font-size: 11px; }

  /* .fact-row (below) has no overflow:hidden of its own, so unlike
     .news-row it can't just clip - its rendered height is always at least
     its content's, no matter how small the flex-basis claims to be. So the
     basis alone (already vh-based above the -8.5vh news-row addition) was
     never the real ceiling; the content sizing here is. */
  .fact-row { flex-basis: clamp(96px, 13vh, 196px); }
  .fact-panel { padding: clamp(10px, 1.6vh, 20px) clamp(14px, 2.4vh, 32px); padding-top: clamp(12px, 2vh, 28px); }
  .fact-label { margin-bottom: 6px; }
  .fact-text { font-size: clamp(13px, 1.9vh, 22px); margin-bottom: 4px; }
  /* #fact-credit's own size/position isn't touched by this query (still
     11px font, 14px bottom offset), so its ~20px pill height still needs
     the same clearance as before - shrinking this past ~26px lets the
     attribution line's box overlap the credit pill (measured 4px of
     overlap at 1440x818 with a 16px margin). */
  .fact-sub { margin-bottom: 26px; }

  /* Rail-card padding/gap/icon/text - the cards' own height still comes
     from app.js's layoutRailCards(), which reads window.innerHeight
     against the same 900px threshold and swaps in a shorter _COMPACT
     height constant (98px) sized for this tighter padding. The two must
     move together: shrinking padding without also shrinking the JS-set
     height (or vice versa) either wastes the padding cut or clips
     content. Because .content-row stretches .content-left to match
     .right-rail's height (default flex align-items:stretch), this rail
     height is also the single biggest lever on the *whole page's* height
     on a short screen - more than any property on the main cards/profile
     row above, confirmed by forcing it smaller directly on a live page
     and watching the whole page shrink in step. Icon/label/strong/sub
     were still their large kiosk-tuned sizes even at the 129px card
     height this replaced; a real laptop window (measured live at
     1366x768 and 1920x700-800) still needed the .kiosk-viewport scroll
     fallback with those, so they shrink here too, not just the
     padding/gap. */
  .room-live-card, .maintenance-live-card, .ticket-live-card { padding: 8px 12px; }
  .rail-card { gap: 3px; }
  .rail-icon { width: 26px; height: 26px; flex: 0 0 26px; }
  .rail-label { font-size: 10px; }
  .rail-card strong { font-size: clamp(13px, 1.25vw, 24px); }
  .room-live-card.free strong { font-size: clamp(12px, 1.04vw, 20px); }
  .rail-sub { font-size: 11px; }
  .ticket-live-card strong { font-size: clamp(12px, 1.15vw, 22px); }

  /* Same reasoning as .fact-row above: profile-cards' fixed 190px was
     sized for a large kiosk display, never revisited for a short one.
     120px is empirically enough for the shrunk avatar + two text lines +
     fact line - checked visually at 744-834px tall, not just derived
     from the numbers below. Also shrinks .profile-name/.profile-role,
     which were previously fixed px (24px/16px) regardless of screen
     height - the biggest remaining vh-blind sizes in this card, same
     "wide-but-short" gap as the other rules in this block. Same
     calc()-ramp shape as the rest of this block (flat px in the base
     rule here, not vw, so no vw term to fold into the calc()) - matters
     more than it looks: .content-left (this card's row plus the main
     climate/transit/weather row above) is stretched to match
     .right-rail's height, so when this used to jump 135px->190px at the
     boundary, the *climate/transit/weather row* had to visibly jump the
     opposite direction to compensate and keep .content-left's total
     pinned to the rail - the two reported-jumpy cards were still jumping
     even after their own fonts/icons/padding above stopped, until this
     was fixed too. */
  .profile-cards { flex-basis: clamp(120px, calc(100vh - 710px), 190px); }
  .profile-card { padding: clamp(10px, calc(100vh - 880px), 20px) clamp(14px, calc(100vh - 878px), 22px); gap: clamp(4px, calc(100vh - 890px), 10px); }
  .avatar { width: clamp(36px, calc(100vh - 844px), 56px); height: clamp(36px, calc(100vh - 844px), 56px); flex-basis: clamp(36px, calc(100vh - 844px), 56px); font-size: clamp(15px, calc(100vh - 876px), 24px); }
  .profile-name { font-size: clamp(18px, calc(100vh - 876px), 24px); margin-top: clamp(2px, calc(100vh - 896px), 4px); }
  .profile-role { font-size: clamp(13px, calc(100vh - 884px), 16px); }
}
