/* CAU Meets — the browser product.
   ============================================================================
   The same origin serves two different things. Inside Telegram the page is a Mini
   App: one column, phone width, sheets sliding up from the bottom. In a browser it
   has to be a web application — and specifically a *university* web application:
   the sort of internal tool a registrar or a dean's office already knows how to
   drive, not a phone screen stretched across a 27-inch monitor.

   WHAT THIS FILE IS, AFTER THE DESIGN-SYSTEM PASS

   Almost nothing. css/components.css owns the interface's vocabulary now — the
   button, the badge, the card, the list row, the table, the field, the skeleton,
   the empty state, the toast — and js/web.js writes `ds-` class names straight
   into its markup. What is left here is the part no shared component can own:

     * the shell — top bar, sidebar, drawer, tab bar, the page column;
     * the four layouts those pieces arrange (rooms grid, booking list, room
       detail split, sign-in card);
     * the handful of things this product has and the design system does not:
       the day strip, the occupancy bar, the in-flight spinner on a button, the
       offline banner;
     * hiding the Mini App.

   Everything that used to be a private copy of a shared idea — .w-btn, .w-st,
   .w-chip, .w-card, .w-row, .w-field, .w-state, .w-skel, .w-toast — is gone. It
   was ~700 lines saying, in this file's own words, what components.css already
   said in everybody's.

   THE LOOK, stated once because every rule below follows from it

     * Two planes. Chrome (top bar, sidebar) is --surface-solid; the page it
       frames is --bg. That single step is what makes a desk screen read as an
       application rather than as a long document.
     * Dense. A desk screen holds four times what a phone does, and somebody
       booking a room wants the whole day at once. Type tops out at --fs-20 (the
       page title) and everything else lives between --fs-11 and --fs-15.
     * Navy, white, neutral — gold only as a word or a line. Gold fills nothing
       on this page. It is the organisation's name in the top bar and on the
       sign-in card, and the hairline beside an offer. That is the whole budget.
     * One filled control per screen: .ds-btn--primary. Everything else is a
       bordered surface or nothing at all until pointed at.
     * Depth from hairlines, not shade. The only shadows on the page are the
       design system's own --shadow on a card and --shadow-2 on an overlay.

   WHY THERE ARE STILL `w-` CLASSES AND NO `ds-` SELECTORS HERE

   tests/test_web_shell.py requires every class selector in this file to start
   with `w-`: two products share one document, and a selector reaching the other
   one's markup is a bug nobody would find by looking at either file. So this
   file never *restyles* a ds- component. Where the shell needs a hook on one, it
   is a second class on the same element (.w-room-card, .w-btn-primary,
   .w-nav-count) — a marker this file owns, sitting next to the component the
   design system owns.

   Colours, sizes and radii all come from css/app.css's scale; the only literal
   pixels below are the shell's own geometry (bar height, sidebar width), which
   are declared once as --w-* on .w-root because nothing outside this file has a
   use for them.

   Breakpoints are the canon in app.css and nothing else: 1440 / 1024 / 768 / 430
   / 375. Four shapes come out of them — wide desk, laptop, tablet rail, phone.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   Which product is on screen.

   js/web.js writes data-shell="web" on <html> the moment it sees a browser. The
   Mini App's screens are then off for good: a `hidden` attribute would be a race
   — app.js calls show('gate') on its own timing — while a stylesheet rule wins
   whatever either script does to the attribute afterwards.
   --------------------------------------------------------------------------- */
html[data-shell="web"] #scr-boot,
html[data-shell="web"] #scr-gate,
html[data-shell="web"] #scr-fatal,
html[data-shell="web"] #app,
html[data-shell="web"] #panel,
html[data-shell="web"] #scrim,
html[data-shell="web"] #sheet-book,
html[data-shell="web"] #sheet-find,
html[data-shell="web"] #sheet-info,
html[data-shell="web"] #offline,
html[data-shell="web"] #toast { display: none !important; }

/* One exception to the line above, and it is the shared release sheet.
   js/release.js is loaded by both shells and builds `#sheet-release` over
   `#scrim` — the Mini App's backdrop. Hiding the backdrop while showing the
   sheet leaves a panel floating over live content with nothing saying it is
   modal and nothing to tap to dismiss it. `.on` is only ever set while a sheet
   is open, so this uncovers the backdrop exactly then. */
html[data-shell="web"] #scrim.on { display: block !important; }

/* The Mini App pins the page to phone width and stops it bouncing. A desktop
   window wants neither. `overflow-x: clip` is the last line of defence for the
   "no horizontal scroll at any width" rule — every layout below is built not to
   need it, but one long unbroken string must not put a scrollbar under the whole
   product. */
html[data-shell="web"] body { overscroll-behavior-y: auto; overflow-x: clip; }

/* ---------------------------------------------------------------------------
   Shell geometry. Sizes only — colours all come from app.css.
   --------------------------------------------------------------------------- */
.w-root {
  --w-topbar: 56px;
  --w-side: 236px;
  --w-rail: 68px;
  --w-tabbar: 56px;
  --w-pad: var(--space-24);
  --w-gap: var(--space-12);
  --w-max: 1320px;

  /* The section you are in, in the sidebar and in the phone tab bar. Dark CAU
     navy on a light page, the crest's gold on a dark one — the same pair the
     Mini App's bottom bar uses, so one person on two devices sees one product.
     It has to be a local name because --accent is not that pair: it flips to a
     pale navy in the dark palette, which is right behind white text on a filled
     button and wrong as a 10px label on a navy bar. No colour is invented here;
     both sides are app.css tokens. */
  --w-nav-on: var(--accent);

  position: relative;
  z-index: 1;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  font-family: var(--font);
  font-size: var(--fs-13);
  line-height: var(--lh-1-45);
  color: var(--text);
}

/* Both halves of the dark scheme, the pair css/app.css keeps for every token it
   flips: the default for a dark client that has never chosen, and the explicit
   choice, which has to be a rule of its own because a media query cannot hold
   the rule that must beat it. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) .w-root { --w-nav-on: var(--gold); }
}

html[data-theme="dark"] .w-root { --w-nav-on: var(--gold); }
html[data-theme="light"] .w-root { --w-nav-on: var(--accent); }

/* Numbers line up in columns or they are not a table. Every time, duration and
   count on the page is tabular; .ds-num is the same promise for a lone span. */
.w-root b,
.w-root time { font-variant-numeric: tabular-nums; }

/* The design system's card parts assume block boxes, and two of them land inside
   a <button> here (the room card is one click target, so everything in it is
   phrasing content). A <b> with a top margin and no display is a <b> with no top
   margin — this is the one line that fixes it, rather than a private copy of
   .ds-card__title with `display:block` added. */
.w-block { display: block; }

/* ---------------------------------------------------------------------------
   Top bar — brand, mode, language, who is signed in.

   56px, solid, one hairline underneath. Nothing sticky-blurred: a solid bar over
   a solid page needs no glass, and the blur cost a repaint on every scroll.
   --------------------------------------------------------------------------- */
.w-topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  height: var(--w-topbar);
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding: 0 var(--w-pad);
  background: var(--surface-solid);
  border-bottom: 1px solid var(--border);
}

.w-brand {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  min-width: 0;
  margin-right: auto;
}

.w-seal {
  width: var(--size-32);
  height: var(--size-32);
  flex: none;
  border-radius: 50%;
  display: block;
}

.w-brand-text { min-width: 0; }

.w-brand-text h1 {
  margin: 0;
  font-size: var(--fs-14);
  font-weight: var(--weight-680);
  letter-spacing: -.2px;
  line-height: var(--lh-1-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The formal line: small caps, gold, letterspaced — the one thing that makes the
   bar read as an institution rather than as a booking widget, and one of exactly
   two places gold appears on the whole page. */
.w-brand-org {
  margin: 0;
  font-size: var(--fs-9-5);
  font-weight: var(--weight-700);
  color: var(--gold);
  letter-spacing: 1.1px;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Layout hooks only; the look is .ds-segmented's. `.w-lang` is in the rail now
   and on the sign-in screen, which has no rail — the bar itself is down to the
   brand, the User/Admin switch and the person, and no longer needs a rule about
   which of its contents to drop when the row runs out of room. */
.w-lang,
.w-mode { flex: none; }

.w-who {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  flex: none;
  max-width: 220px;
  min-height: var(--size-40);
  padding: var(--space-4) var(--space-10) var(--space-4) var(--space-4);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--text);
  font: var(--weight-620) var(--fs-12)/var(--lh-1-25) var(--font);
  text-align: left;
  transition: background-color .16s, border-color .16s;
}

.w-who-text { min-width: 0; }

.w-who-text b {
  display: block;
  font-weight: var(--weight-640);
  white-space: nowrap;
  /* nowrap without a way out turns one long name into a horizontal scrollbar for
     the whole page. The parent already has min-width:0; this is the half that
     actually truncates. */
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Secondary, and no longer shouted. The upper case and the letter-spacing were
   doing the work of making a bare code like `it` look deliberate; the server
   resolves departments into their real words now, and «INFORMATION TECHNOLOGY»
   in capitals across the top of the screen is one of those words with the shape
   taken out of it. */
.w-who-text span {
  display: block;
  font-size: var(--fs-11);
  font-weight: var(--weight-620);
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The hamburger only exists on phones, where the sidebar has become a drawer. */
.w-burger {
  display: none;
  place-items: center;
  flex: none;
  padding: 0;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius-8);
  cursor: pointer;
  transition: background-color .16s, border-color .16s;
}

.w-burger svg { width: var(--size-18); height: var(--size-18); }

/* ---------------------------------------------------------------------------
   Body: sidebar + page.

   The shell is full-bleed — a sidebar that stops short of the window edge reads
   as a floating panel, not as the frame of an application. The *content* is what
   is capped, inside .w-page, so a 1920px monitor gets wider gutters rather than
   an 1800px-wide line of body text.
   --------------------------------------------------------------------------- */
.w-body {
  flex: 1;
  display: flex;
  align-items: stretch;
  min-height: 0;
  width: 100%;
}

.w-side {
  position: sticky;
  top: var(--w-topbar);
  align-self: flex-start;
  flex: none;
  width: var(--w-side);
  height: calc(100vh - var(--w-topbar));
  height: calc(100dvh - var(--w-topbar));
  display: flex;
  flex-direction: column;
  padding: var(--space-14) var(--space-12);
  background: var(--surface-solid);
  border-right: 1px solid var(--border);
}

.w-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* The rail is exactly as tall as the window, so a list that outgrows it has
     to scroll inside itself rather than off the end of it. Four items fit
     today; the item that does not fit tomorrow would simply be unreachable,
     and nothing on the screen would say so. */
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.w-nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-11);
  width: 100%;
  min-height: var(--size-40);
  padding: var(--space-9) var(--space-11);
  border: 0;
  border-radius: var(--radius-8);
  background: transparent;
  color: var(--text-dim);
  font: var(--weight-620) var(--fs-13)/var(--lh-1-25) var(--font);
  text-align: left;
  cursor: pointer;
  transition: background-color .16s, color .16s;
}

.w-nav-item svg {
  width: var(--size-18);
  height: var(--size-18);
  flex: none;
}

.w-nav-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The current section. A quiet fill and the accent on the icon and the word —
   no gold bar, no filled navy block: a sidebar of five items where one is a
   solid rectangle is a sidebar you read as a button bar. */
.w-nav-item.w-on {
  background: var(--accent-soft);
  color: var(--w-nav-on);
  font-weight: var(--weight-700);
}

/* The unread count is .ds-unread; this only says where it sits, and shrinks it
   into the corner of the icon once the labels are gone at 1024. */
.w-nav-count { margin-left: auto; }

/* A label over a group of nav items, not a heading anybody reads twice. */
.w-side-label {
  margin: 0 0 var(--space-6);
  padding: 0 var(--space-11);
  font-size: var(--fs-10);
  font-weight: var(--weight-700);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The foot of the rail, which used to print the university's name a second
   time and now holds the language choice. `margin-top: auto` is what pins it
   there: the nav above it grows and scrolls, this stays at the bottom. */
.w-side-lang {
  margin-top: auto;
  padding: var(--space-10) var(--space-11) 0;
  border-top: 1px solid var(--border);
}

.w-side-lang .w-side-label { margin-bottom: var(--space-8); padding: 0; }

/* Full width of the rail rather than hugging its own text: a pill track that
   stops two thirds of the way across reads as something half-drawn. */
.w-lang--rail { display: flex; width: 100%; }
/* `> *` rather than the item's own class: every child of a segmented track is
   one of its cells, and naming the design system's class here would make this
   the second stylesheet with an opinion about it — which is the rule
   tests/test_web_shell.py keeps, and the reason that control looks the same
   everywhere it appears. */
.w-lang--rail > * { flex: 1; padding: var(--space-8) var(--space-6); }

/* app.css styles the bare `main` element for the Mini App (flex column, 16px
   padding, 12px gap). This is a <main> too, so all three arrive uninvited —
   which is why the display is pinned rather than assumed. */
.w-main {
  display: block;
  flex: 1;
  min-width: 0;
  padding: var(--space-20) var(--w-pad) var(--space-48);
}

.w-page {
  max-width: var(--w-max);
  margin: 0 auto;
}

/* ---------------------------------------------------------------------------
   Page header — title, one line of explanation, actions on the right.
   --------------------------------------------------------------------------- */
.w-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-16);
  flex-wrap: wrap;
}

.w-page-head-text { flex: 1 1 260px; min-width: 0; }

.w-page-head h2 {
  margin: 0;
  font-size: var(--fs-20);
  font-weight: var(--weight-680);
  letter-spacing: -.4px;
  line-height: var(--lh-1-2);
}

.w-page-head p {
  margin: var(--space-4) 0 0;
  font-size: var(--fs-12-5);
  line-height: var(--lh-1-5);
  color: var(--text-dim);
  max-width: 68ch;
}

.w-page-actions {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  flex: none;
  flex-wrap: wrap;
}

/* .ds-divider under the title, with this page's own rhythm around it. */
.w-rule { margin: var(--space-12) 0 var(--space-16); }

/* A section of a page: a micro-label and whatever it labels. The label is not a
   heading anybody navigates by, which is why it is 11px uppercase rather than a
   second page title competing with .w-page-head. */
.w-sec > h4 {
  margin: 0 0 var(--space-8);
  font: var(--weight-700) var(--fs-11)/var(--lh-1-2) var(--font);
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--text-faint);
}

.w-sec-top { margin-top: var(--space-24); }

/* ---------------------------------------------------------------------------
   Two things the design system has no opinion about, because only this product
   has them: a control that is waiting on the network, and a page-level notice.
   --------------------------------------------------------------------------- */

/* A button that has fired a request says so where the eye already is: on the
   button. No spinner overlay, no toast that arrives after the fact.

   The ring is NOT drawn in currentColor: the rule turns the label transparent to
   make room for it, and currentColor would take the ring with it — a busy button
   with an invisible spinner reads as a control that has simply gone blank. */
.w-busy { position: relative; color: transparent; }

.w-busy::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--size-14);
  height: var(--size-14);
  margin: calc(var(--size-14) / -2) 0 0 calc(var(--size-14) / -2);
  border: 2px solid var(--text-dim);
  border-top-color: transparent;
  border-radius: 50%;
  animation: w-spin .7s linear infinite;
}

@keyframes w-spin { to { transform: rotate(360deg); } }

/* The one filled control on a screen is .ds-btn--primary; this marker is how a
   `w-` selector reaches it (see the header — no ds- selector may appear here),
   and it is what tests/test_login_page.py reads to prove that Microsoft, and not
   the password route, is the filled button on the sign-in card. */
.w-btn-primary.w-busy::after {
  border-color: var(--on-accent);
  border-top-color: transparent;
}

/* Releasing a room, signing out, declining an offer. The design system's
   --danger is a *filled* button on purpose (a destructive confirm must not look
   like a cancel); these three are not confirmations, they are one of several
   choices in a row, so they stay bordered and only the word turns red. */
.w-btn-danger {
  color: var(--danger);
  border-color: var(--danger-soft);
}

.w-btn-danger.w-busy::after { border-color: var(--danger); border-top-color: transparent; }

/* A notice about the page, not about a field: the day is closed, the catalogue
   is empty in this building. One coloured rule and a line of text — a filled
   alert box is exactly the thing this page is not. */
.w-warn {
  margin: 0 0 var(--space-12);
  padding: var(--space-8) var(--space-12);
  border-left: 2px solid var(--warning);
  border-radius: 0 var(--radius-6) var(--radius-6) 0;
  background: var(--warning-soft);
  color: var(--text);
  font: var(--weight-600) var(--fs-12)/var(--lh-1-5) var(--font);
}

.w-hint {
  margin: var(--space-8) 0 var(--space-6);
  font-size: var(--fs-11-5);
  line-height: var(--lh-1-45);
  color: var(--text-faint);
}

/* ---------------------------------------------------------------------------
   Sign-in page.
   --------------------------------------------------------------------------- */
.w-login {
  flex: 1;
  display: grid;
  place-items: center;
  padding: var(--space-48) var(--space-20);
}

.w-login-card {
  width: 100%;
  max-width: 380px;
  padding: var(--space-26);
  background: var(--surface-solid);
  text-align: left;
}

/* Every direct child button of the card is full width and stacks — the two
   doors, and the submit inside the form, all line up without each one having to
   remember .ds-btn--block. */
.w-login-card > button {
  display: flex;
  width: 100%;
  margin-bottom: var(--space-8);
}

.w-login-seal {
  width: var(--size-56);
  height: var(--size-56);
  display: block;
  margin: 0 auto var(--space-14);
  border-radius: 50%;
}

.w-login-title {
  margin: 0;
  font-size: var(--fs-18);
  font-weight: var(--weight-680);
  letter-spacing: -.3px;
  text-align: center;
}

.w-login-org {
  margin: var(--space-5) 0 0;
  font-size: var(--fs-10);
  font-weight: var(--weight-700);
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--gold);
  text-align: center;
}

.w-login-lead {
  margin: var(--space-10) 0 var(--space-18);
  font-size: var(--fs-12-5);
  line-height: var(--lh-1-5);
  color: var(--text-dim);
  text-align: center;
}

.w-or {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  margin: var(--space-16) 0;
  font: var(--weight-700) var(--fs-10)/var(--lh-1) var(--font);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-faint);
}

.w-or::before,
.w-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* The Microsoft round trip came back refused. One banner, in words, with the two
   ways forward as real buttons — never the callback's raw JSON in a bare tab. */
.w-auth-fail {
  margin: 0 0 var(--space-14);
  padding: var(--space-11) var(--space-12);
  border: 1px solid var(--border);
  border-left: 2px solid var(--danger);
  border-radius: 0 var(--radius-11) var(--radius-11) 0;
  background: var(--danger-soft);
}

.w-auth-fail b {
  display: block;
  font-size: var(--fs-12-5);
  font-weight: var(--weight-680);
  color: var(--text);
}

.w-auth-fail p {
  margin: var(--space-5) 0 0;
  font-size: var(--fs-12);
  line-height: var(--lh-1-5);
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/* The provider's own code, small and copyable — it is what IT will ask for. */
.w-auth-code {
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

.w-auth-actions { margin-top: var(--space-10); }

/* Disabled and honest about it: an unconfigured tenant must not be a button that
   looks alive and fails on the far side of a redirect. */
.w-provider-off {
  margin: 0 0 var(--space-10);
  padding: var(--space-9) var(--space-11);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-8);
  font-size: var(--fs-11-5);
  line-height: var(--lh-1-45);
  color: var(--text-faint);
}

.w-login-foot {
  margin: var(--space-16) 0 0;
  padding-top: var(--space-12);
  border-top: 1px solid var(--border);
  font-size: var(--fs-11);
  line-height: var(--lh-1-5);
  color: var(--text-faint);
  text-align: center;
}

/* ---------------------------------------------------------------------------
   Toolbar: the row of controls under a page header — the day strip, the building
   filter, the view switch. One rhythm for all three.
   --------------------------------------------------------------------------- */
.w-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  flex-wrap: wrap;
  margin-bottom: var(--space-14);
}

.w-rail {
  display: flex;
  gap: var(--space-4);
  padding-bottom: var(--space-4);
  margin-bottom: var(--space-14);
}

.w-day {
  flex: none;
  min-width: var(--size-52);
  min-height: var(--size-44);
  padding: var(--space-5) var(--space-8);
  border: 1px solid var(--border);
  border-radius: var(--radius-8);
  background: var(--surface);
  color: var(--text);
  text-align: center;
  cursor: pointer;
  transition: background-color .16s, border-color .16s, color .16s;
}

.w-day em {
  display: block;
  font-style: normal;
  font-size: var(--fs-10);
  font-weight: var(--weight-700);
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--text-faint);
}

.w-day b {
  display: block;
  font-size: var(--fs-15);
  font-weight: var(--weight-660);
  letter-spacing: -.3px;
}

.w-day.w-off { opacity: .4; cursor: not-allowed; }

/* Selected: a tint and the accent border, never a filled navy block. The strip
   is scanned, and twenty filled blocks with one different one is harder to read
   than twenty outlines with one tinted. */
.w-day.w-on {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

.w-day.w-on em { color: var(--accent); }

/* Buildings: the filter row. These chips are this file's fallback; when
   js/roomsui.js is loaded its BuildingFilter mounts in the same spot and these
   rules simply have nothing to match. */
.w-bfilter {
  display: flex;
  gap: var(--space-6);
  flex-wrap: wrap;
}

.w-bchip {
  flex: none;
  min-height: var(--size-34);
  padding: var(--space-7) var(--space-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-dim);
  font: var(--weight-620) var(--fs-12)/var(--lh-1) var(--font);
  cursor: pointer;
  white-space: nowrap;
  transition: background-color .16s, border-color .16s, color .16s;
}

.w-bchip.w-on {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
  font-weight: var(--weight-700);
}

/* A building heading is a label over a grid, not a second page title. */
.w-bldg {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  margin: var(--space-24) 0 var(--space-10);
  font-size: var(--fs-11);
  font-weight: var(--weight-700);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-faint);
}

.w-bldg::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.w-bldg:first-of-type { margin-top: 0; }

.w-bldg-count {
  font-weight: var(--weight-600);
  letter-spacing: .2px;
  text-transform: none;
  color: var(--text-faint);
}

/* ---------------------------------------------------------------------------
   Rooms.

   The card itself is .ds-card / .ds-card--interactive, and its insides are the
   design system's card parts. What is here is the grid they sit in, the fact
   that this particular card is a <button> (so it needs its own font and its own
   column flow), and the day-load bar, which no shared component draws.
   --------------------------------------------------------------------------- */
.w-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: var(--w-gap);
}

.w-room-card {
  display: flex;
  flex-direction: column;
  /* Small, because two of the three children bring their own top margin from
     the design system; this only has to separate the head from the strip. */
  gap: var(--space-6);
  width: 100%;
  text-align: left;
  color: var(--text);
  /* A <button> does not inherit the page font — say it, or the cards come out in
     the browser's own UI face while everything around them is the crest's. */
  font: var(--weight-500) var(--fs-13)/var(--lh-1-45) var(--font);
}

/* Day load: one bar per slot, the same reading as the Mini App's strip. */
.w-tl {
  display: flex;
  gap: var(--space-1-5);
  height: var(--size-6);
}

.w-tl i {
  flex: 1;
  border-radius: var(--radius-2);
  background: var(--tl-empty);
  transition: background-color .2s;
}

.w-tl i.w-b { background: var(--busy); }
.w-tl i.w-w { background: var(--wait); }
.w-tl i.w-m { background: var(--accent); }

.w-tl-x {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-4);
  font-size: var(--fs-10);
  font-variant-numeric: tabular-nums;
  letter-spacing: .3px;
  color: var(--text-faint);
}

.w-legend {
  display: flex;
  gap: var(--space-12);
  flex-wrap: wrap;
  margin-top: var(--space-12);
  font-size: var(--fs-11);
  color: var(--text-faint);
}

.w-legend span { display: inline-flex; align-items: center; gap: var(--space-5); }
.w-legend i { width: var(--size-9); height: var(--size-9); border-radius: var(--radius-2); }

/* ---------------------------------------------------------------------------
   Bookings.

   One .ds-card each, laid out as time · what · status, with the actions on their
   own line underneath. Everything that is longer than a line — the reason a
   booking was refused, the room offered instead — is a block below the card's
   own row rather than squeezed into the middle of it: somebody who has just been
   told no needs the reason in the same glance as the word.
   --------------------------------------------------------------------------- */
.w-booking-side {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.w-actions { margin-top: var(--space-12); }

.w-reason,
.w-offer { margin-top: var(--space-12); }

.w-reason {
  padding: var(--space-10) var(--space-12);
  border: 1px solid var(--border);
  border-left: 2px solid var(--danger);
  border-radius: 0 var(--radius-11) var(--radius-11) 0;
  background: var(--danger-soft);
}

.w-reason b {
  display: block;
  font-size: var(--fs-11);
  font-weight: var(--weight-700);
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--danger);
}

.w-reason p {
  margin: var(--space-5) 0 0;
  font-size: var(--fs-12-5);
  line-height: var(--lh-1-5);
  color: var(--text);
  overflow-wrap: anywhere;
}

/* Who ended it and when, under the reason and the comment. Dimmed, because it
   is a fact about the decision rather than a second alarm about it — the red
   edge of the block has already said what happened. */
.w-reason p.w-reason-who {
  color: var(--text-dim);
  font-size: var(--fs-11-5);
}

/* Активные | Архив above the list. The control itself is .ds-segmented from
   css/components.css; this is only where it sits, and it scrolls rather than
   squeezing on a phone — two labels plus their counts are wider than a 375px
   column once the words are Uzbek. */
.w-mine-tabs {
  margin: 0 0 var(--space-16);
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.w-mine-tabs::-webkit-scrollbar { display: none; }

/* An administrator has proposed a different room. Everything needed to say yes
   or no is in the block: where it is, when, and how many it seats. Gold is the
   hairline on its leading edge and nothing else — the crest's ring used as a
   ring, which is the only way gold appears anywhere on this page. */
.w-offer { border-left: 2px solid var(--gold-line); }

.w-offer b {
  display: block;
  font-size: var(--fs-11);
  font-weight: var(--weight-700);
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--gold);
}

.w-offer-what {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-6);
  font-size: var(--fs-13);
  font-weight: var(--weight-620);
  color: var(--text);
}

.w-offer-what span { display: inline-flex; align-items: center; white-space: nowrap; }
/* A middot between facts, drawn rather than typed: it never ends up dangling at
   the end of a line when one of the facts is missing. */
.w-offer-what span + span::before { content: "\00b7"; margin: 0 var(--space-6); opacity: .6; }

/* ---------------------------------------------------------------------------
   Room detail: what the room is doing on the left, the booking form on the
   right. The same split carries the Account page.
   --------------------------------------------------------------------------- */
.w-split {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(300px, .9fr);
  gap: var(--space-24);
  align-items: start;
}

/* ---------------------------------------------------------------------------
   Find a free room. A panel under the filter, not a modal: the room list stays
   visible underneath, which is what somebody comparing an answer against the
   grid actually wants.
   --------------------------------------------------------------------------- */
.w-find { margin-bottom: var(--space-16); }
.w-find-lead { margin: 0 0 var(--space-12); }
/* A `w-` marker beside the component's own class rather than a `.ds-field`
   selector: two products share this document and a rule in this file reaching
   the other one's markup is the bug tests/test_web_shell.py exists to stop. */
.w-find-row + .w-find-row { margin-top: var(--space-12); }
.w-find-rows { margin-top: var(--space-16); }

/* Wrapping, because five durations and five group sizes do not fit one line on
   a 375-pt phone and a row that scrolls sideways hides the last option. */
.w-fchips { display: flex; flex-wrap: wrap; gap: var(--space-8); }
.w-fchip {
  min-height: var(--size-36);
  padding: 0 var(--space-14);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text);
  font: var(--weight-500) var(--fs-13)/1 var(--font);
  cursor: pointer;
  transition: background-color .14s, border-color .14s, color .14s;
}
.w-fchip:hover { background: var(--hover); }
.w-fchip.w-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.w-find-open.w-on { background: var(--hover); }

/* The room's photographs, above everything the page says about time.
   `.ru-gal` is `position:absolute; inset:0` — it is built to fill a frame the
   host owns — so the frame is here: the same 16:10 the cards use, for the same
   reason, which is that the box is this tall before the bytes arrive and
   exactly this tall afterwards. Nothing below it moves as pictures load. */
.w-gallery {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: var(--r-card);
  border: 1px solid var(--border);
  background: var(--surface-2);
}

/* A list of occupied slots is .ds-rows, but here it sits under its own heading
   rather than inside a card, so it drops the container the component brings and
   keeps only the hairlines between rows. */
.w-slots {
  border: 0;
  border-radius: 0;
  background: transparent;
}

.w-meta {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-12);
  font-size: var(--fs-11-5);
  color: var(--text-faint);
}

.w-meta span { display: inline-flex; align-items: center; white-space: nowrap; }
.w-meta span + span::before { content: "\00b7"; margin: 0 var(--space-6); opacity: .6; }

/* Two selects side by side; .ds-field's own bottom margin belongs to the pair,
   not to each half of it. */
.w-field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-10);
  margin-bottom: var(--space-14);
}

.w-field-row > label { margin-bottom: 0; }

/* --- the time block: start, how long, and therefore when it ends ---------- */

/* Read as one sentence — «начинается в 14:00, идёт 45 минут, кончается в
   14:45» — so the three parts sit on one line with the length in the middle,
   between the two clock times it joins. The two wheels this replaces made
   somebody work the middle clause out for themselves.

   `baseline` and not `center`: the two times are large and their captions are
   small, and aligning the boxes would leave the three numbers sitting at three
   different heights. */
.w-start { margin-bottom: var(--space-14); }

/* Set off from the fields below it: the start and the length are the booking,
   and «сколько человек» and «что нужно для встречи» are details about one. */
.w-dur {
  margin-bottom: var(--space-18);
  padding: var(--space-14) var(--space-16) var(--space-16);
  border: 1px solid var(--border);
  border-radius: var(--radius-14);
  background: var(--surface-2);
}

.w-dur__ends {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: baseline;
  gap: var(--space-10);
  margin-bottom: var(--space-12);
}
.w-dur__ends > span { font-size: var(--fs-11-5); color: var(--text-faint); }
.w-dur__ends > span:first-child { text-align: left; }
.w-dur__ends > span:last-child { text-align: right; }
.w-dur__ends b {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--text);
  font-size: var(--fs-17);
  font-weight: var(--weight-640);
  font-variant-numeric: tabular-nums;
}
/* The one number somebody is actually choosing, and the largest thing in the
   block for that reason. The two clock times beside it are consequences. */
/* `.w-dur__ends > span` is one specificity point heavier than a bare class, so
   the caption rule above was winning and this read at 11.5px in text-faint —
   the smallest thing in a block it is supposed to lead. */
.w-dur__ends > .w-dur__value {
  justify-self: center;
  padding: var(--space-2) var(--space-12);
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-20);
  font-weight: var(--weight-700);
  letter-spacing: -.3px;
  white-space: nowrap;
}

/* The track is drawn on the whole duration scale and the input is laid over only
   the part of it this room actually has — `durationSliderHTML` sets that width
   and the matching `max`. So the grey tail is a fact on display rather than a
   region to drag into, and there is no value that springs back, which is what
   a disabled range reads as. */
.w-dur__track {
  position: relative;
  height: var(--space-28);
}
.w-dur__track::before {
  content: "";
  position: absolute;
  inset: calc(50% - var(--size-6) / 2) 0 auto 0;
  height: var(--size-6);
  border-radius: var(--radius-pill);
  background: var(--border);
}

/* Every fifteen minutes, so the scale is countable without a number under each
   mark. Dimmed past the ceiling: the same tick, in the part of the afternoon
   the room does not have. Never a hit target — the input above owns the
   pointer. */
/* What the room still has from this start, and how much of it is taken by the
   meeting as it stands. Both measured against the whole duration scale, so the
   chosen bar ends under the thumb and the grey beyond is what this afternoon
   does not have. */
.w-dur__avail,
.w-dur__fill {
  position: absolute;
  top: calc(50% - var(--size-6) / 2);
  left: 0;
  height: var(--size-6);
  border-radius: var(--radius-pill);
  pointer-events: none;
}
/* The accent at a third of its strength rather than `--accent-soft`: against
   `--surface-2` the soft token and the grey track are within a few points of
   each other in both themes, and the whole point of this bar is that a reader
   can tell the three regions apart at a glance. */
.w-dur__avail { background: var(--accent); opacity: .3; }
.w-dur__fill { background: var(--accent); }

.w-dur__tick {
  position: absolute;
  top: calc(50% - var(--space-5));
  width: var(--size-2);
  height: var(--space-10);
  margin-left: calc(var(--size-2) / -2);
  border-radius: var(--radius-2);
  background: var(--navy-300);
  pointer-events: none;
  z-index: 2;
}
.w-dur__tick--off { background: var(--border-strong); }

.w-dur__input {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  margin: 0;
  padding: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
  z-index: 1;
}
/* The input paints nothing but its thumb: the bar under it is the two flat
   fills above, which every browser draws the same way. */
.w-dur__input::-webkit-slider-runnable-track { height: var(--size-6); background: none; }
.w-dur__input::-moz-range-track { height: var(--size-6); background: none; }
.w-dur__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--space-24);
  height: var(--space-24);
  margin-top: calc((var(--space-24) - var(--size-6)) / -2);
  border: var(--space-3) solid var(--surface);
  border-radius: var(--radius-pill);
  background: var(--accent);
  box-shadow: var(--shadow-2);
  cursor: grab;
}
.w-dur__input::-moz-range-thumb {
  width: var(--space-24);
  height: var(--space-24);
  box-sizing: border-box;
  border: var(--space-3) solid var(--surface);
  border-radius: var(--radius-pill);
  background: var(--accent);
  box-shadow: var(--shadow-2);
  cursor: grab;
}
.w-dur__input:focus { outline: none; }
.w-dur__input:focus-visible::-webkit-slider-thumb {
  outline: var(--size-2) solid var(--focus);
  outline-offset: var(--space-2);
}
.w-dur__input:focus-visible::-moz-range-thumb {
  outline: var(--size-2) solid var(--focus);
  outline-offset: var(--space-2);
}

.w-dur__note { margin: var(--space-8) 0 0; }

/* «Время встречи изменилось» — somebody took part of the window between this
   form being drawn and the submit. Above the time block and nowhere else:
   everything else the person typed is still on screen and still theirs, and
   the one thing that needs changing is right below this. */
.w-squeezed {
  margin: 0 0 var(--space-14);
  padding: var(--space-14);
  border: 1px solid var(--warning);
  border-radius: var(--radius-14);
  background: var(--warning-soft);
  color: var(--text);
}
.w-squeezed p { margin: 0 0 var(--space-8); font-size: var(--fs-13-5); }
.w-squeezed__title {
  color: var(--gold);
  font-size: var(--fs-14);
  font-weight: var(--weight-650);
}

.w-sticky { position: sticky; top: calc(var(--w-topbar) + var(--space-20)); }

/* The Account profile is a .ds-table; only the label column's width is this
   page's business. 1% is the standard way of saying "as narrow as the content
   allows" to a table layout algorithm. */
.w-kv th { width: 1%; }

/* ---------------------------------------------------------------------------
   Transient chrome.
   --------------------------------------------------------------------------- */
.w-offline {
  position: fixed;
  left: 50%;
  top: var(--space-12);
  z-index: 90;
  transform: translate(-50%, -200%);
  padding: var(--space-7) var(--space-14);
  border-radius: var(--radius-pill);
  background: var(--danger);
  color: var(--on-accent);
  font: var(--weight-700) var(--fs-11-5)/var(--lh-1-35) var(--font);
  transition: transform .22s;
}

.w-offline.w-on { transform: translate(-50%, 0); }

/* Bottom tab bar: phones only, see the media query below. */
.w-tabbar {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  height: calc(var(--w-tabbar) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--surface-solid);
  border-top: 1px solid var(--border);
}

.w-tab {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  border: 0;
  background: transparent;
  color: var(--text-faint);
  font: var(--weight-640) var(--fs-10)/var(--lh-1-2) var(--font);
  letter-spacing: .2px;
  cursor: pointer;
  transition: color .16s;
}

.w-tab span {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 0 var(--space-2);
}

.w-tab svg { width: var(--size-18); height: var(--size-18); }
.w-tab.w-on { color: var(--w-nav-on); }

/* The unread dot rides on the icon rather than beside the word: down here the
   word is 10px and there is no room next to it. */
.w-tab-dot {
  position: absolute;
  top: var(--space-8);
  left: 50%;
  margin-left: var(--space-8);
}

/* ---------------------------------------------------------------------------
   Pointer refinements. Hover is a mouse idea; on a touch screen these would
   stick to the last thing tapped, which is why they live behind the capability
   query rather than next to the components — the same arrangement
   css/components.css uses.
   --------------------------------------------------------------------------- */
@media (hover: hover) {
  .w-who:hover { background: var(--surface-2); border-color: var(--border); }
  .w-burger:hover { border-color: var(--border-strong); }
  .w-nav-item:hover { background: var(--hover); color: var(--text); }
  .w-nav-item.w-on:hover { color: var(--w-nav-on); }
  .w-day:hover { border-color: var(--border-strong); }
  .w-bchip:hover { border-color: var(--border-strong); color: var(--text); }
  .w-tab:hover { color: var(--text); }
  /* Beats components.css's own .ds-btn--secondary:hover, which would otherwise
     turn a Release button blue on the way to being clicked. */
  .w-btn-danger:hover { border-color: var(--danger); color: var(--danger); }
}

/* ---------------------------------------------------------------------------
   Responsive.

   Four shapes, one shell:
     >=1440   — the gutters grow; the content column has already stopped;
     1024     — sidebar keeps its labels, the room detail splits in two;
     <=1024   — sidebar collapses to an icon rail, the split stacks;
     <=768    — sidebar becomes a drawer plus a bottom tab bar;
     <=430    — everything tightens by one step;
     <=375    — the brand wordmark steps aside for the controls.
   --------------------------------------------------------------------------- */
@media (min-width: 1440px) {
  .w-root { --w-pad: var(--space-32); }
  .w-grid { grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); }
}

@media (max-width: 1024px) {
  .w-root { --w-side: var(--w-rail); --w-pad: var(--space-18); }
  .w-split { grid-template-columns: minmax(0, 1fr); gap: var(--space-20); }
  .w-sticky { position: static; }
  .w-nav-item { justify-content: center; padding: var(--space-9) 0; }
  .w-nav-label { display: none; }
  .w-nav-count {
    position: absolute;
    top: var(--space-4);
    right: var(--space-8);
    margin-left: 0;
  }
  .w-side-label { display: none; }
  /* Three two-letter pills will not sit side by side in a rail this narrow, so
     they stack — which is the shape the choice takes when the labels beside the
     icons have already gone. */
  .w-lang--rail { flex-direction: column; }
}

@media (max-width: 768px) {
  .w-root { --w-pad: var(--space-14); }
  .w-main { padding: var(--space-16) var(--w-pad) calc(var(--w-tabbar) + var(--space-32)); }
  .w-burger { display: grid; }
  .w-who-text { display: none; }
  .w-who { padding: var(--space-4); max-width: none; }
  .w-page-head h2 { font-size: var(--fs-18); }
  .w-grid { grid-template-columns: minmax(0, 1fr); }
  .w-field-row { grid-template-columns: minmax(0, 1fr); }
  .w-tabbar { display: flex; }

  /* Hands the overscroll gesture to the page. Chrome on Android otherwise
     reloads the whole document on a pull — throwing away the shell, the session
     read and the scroll position to fetch what four small requests would have.
     Only here, at the width where the listeners in js/web.js are the ones that
     answer it; on a desktop the browser keeps its own behaviour. */
  html[data-shell="web"] body { overscroll-behavior-y: contain; }

  /* The sidebar is still the same nav, moved into a drawer so the labels come
     back on a narrow screen — an icon rail on a phone is a guessing game. It
     sits above .ds-scrim (z-index 80), which is what dims the page behind it. */
  .w-side {
    position: fixed;
    top: var(--w-topbar);
    left: 0;
    z-index: 81;
    width: min(78vw, 280px);
    height: calc(100vh - var(--w-topbar));
    height: calc(100dvh - var(--w-topbar));
    transform: translateX(-102%);
    visibility: hidden;
    transition: transform .24s, visibility .24s;
  }
  .w-side.w-open { transform: none; visibility: visible; }
  .w-side .w-nav-item { justify-content: flex-start; padding: var(--space-9) var(--space-11); }
  .w-side .w-nav-label { display: block; }
  .w-side .w-nav-count { position: static; margin-left: auto; }
  .w-side-label { display: block; }
  /* Inside the drawer there is room for the row again. */
  .w-side .w-lang--rail { flex-direction: row; }
}

@media (max-width: 430px) {
  .w-root { --w-pad: var(--space-12); --w-gap: var(--space-10); }
  .w-topbar { gap: var(--space-8); }
  .w-day { min-width: var(--size-44); padding: var(--space-5) var(--space-4); }
  .w-login { padding: var(--space-20) var(--space-12) var(--space-48); }
  .w-login-card { padding: var(--space-20) var(--space-16); }
}

@media (max-width: 375px) {
  /* An admin's top bar carries one more control than everybody else's, and a
     375px row cannot hold all of them. The mode switch is never the thing that
     gives way — it is the way back out of the admin panel — so the wordmark
     goes, and the language pill with it on an admin's screen. Language still
     lives on the Account page, which is two taps away. */
  .w-brand-text { display: none; }

}

/* Somebody has asked their system to stop things moving. Honour it: nothing here
   carries meaning that only the motion conveys. */
@media (prefers-reduced-motion: reduce) {
  .w-root *,
  .w-side,
  .w-offline { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  .w-busy::after { animation: none; }
}

/* The pull-to-refresh dial, borrowed whole from the Mini App (css/telegram.css)
   so the gesture looks the same in both shells. The only difference is where it
   parks: this shell has a top bar over the page, and a dial that slides out from
   behind it reads as part of the page rather than as something arriving. */
.w-ptr {
  top: calc(var(--w-topbar) + var(--space-8));
  z-index: 41;
}

/* The profile picture and its two controls, on the account screen.

   The circle beside the buttons rather than above them: the picture is what the
   buttons are about, and a column puts a gap between a thing and the way to
   change it. */
.w-photo {
  display: flex;
  align-items: center;
  gap: var(--space-14);
}

.w-photo-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  min-width: 0;
}

/* A label around a hidden input is how a file picker opens without an inline
   handler — the CSP allows no inline script. `display: none` rather than a
   visually-hidden input: a hidden-but-clickable input inside its own label
   receives the click twice and opens the picker twice. */
.w-photo-pick { cursor: pointer; }
.w-photo-pick input { display: none; }

/* --- the room condition, before a booking is made ------------------------ */

/* A real dialogue rather than `confirm()`.
 *
 * This is a promise about how somebody's booking may be treated, the words are
 * the room's own, and a browser prompt cannot carry the checkbox — which is the
 * part that makes accepting deliberate rather than reflexive. */
.w-modal-scrim {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-16);
  background: rgba(0, 0, 0, 0.45);
}
.w-modal {
  /* The same width as .ds-modal, written here because this sheet is not one:
     it is drawn inside the page's single innerHTML rather than mounted in the
     dialog host. 150px — the spacing token this once used — made a column of
     one word per line. */
  width: min(480px, calc(100vw - 32px));
  max-height: 86vh;
  overflow-y: auto;
  padding: var(--space-20);
  border-radius: var(--radius-14);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-2);
}
.w-modal__title { margin: 0 0 var(--space-4); font-size: var(--fs-16); }
.w-modal__sub {
  margin: 0 0 var(--space-8);
  font-size: var(--fs-13);
  font-weight: 600;
  color: var(--warning);
}
.w-modal__text { margin: 0 0 var(--space-12); font-size: var(--fs-13); line-height: 1.5; }
.w-modal__agree {
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  font-size: var(--fs-13);
}
.w-modal__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-8);
  margin: var(--space-16) 0 0;
}

/* --- the room-size advice, the condition chip and the release banner ------ */

/* Advice under «Количество участников». Two tones because the two sentences
   mean opposite things and a reader scanning the form should not have to read
   both to find out which one this is. */
.w-fit { margin: var(--space-6) 0 0; }
.w-fit--over { color: var(--warning); }
.w-fit--ok { color: var(--success); }

/* «Приоритет ректора» on a room card: the room's own words, not a status. The
   seven status badges mean seven things about a booking and this means none of
   them, so it is its own chip rather than a borrowed one. */
.w-cond-chip {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-8);
  border: 1px solid var(--gold-line);
  border-radius: var(--radius-12);
  background: var(--gold-soft);
  color: var(--gold);
  font-size: var(--fs-12);
  font-weight: 600;
  line-height: 1.7;
}

/* An administrator's demand, across the top of whatever page is open. Loud on
   purpose, and never used for a colleague's request. */
.w-alert {
  margin: 0 0 var(--space-16);
  padding: var(--space-16);
  border: 1px solid var(--danger);
  border-radius: var(--radius-14);
  background: var(--danger-soft);
  color: var(--text);
}
.w-alert__title {
  display: block;
  margin: 0 0 var(--space-8);
  color: var(--danger);
  font-size: var(--fs-15);
}
.w-alert__line {
  margin: 0 0 var(--space-6);
  font-size: var(--fs-13);
  line-height: 1.5;
}
.w-alert__line em { font-style: normal; color: var(--text-dim); }
.w-alert__ok { margin: var(--space-8) 0 0; }

/* One booked hour on a room's page. Wide enough on a desktop; on a phone the
   trailing controls take their own line rather than hang off the right edge. */
@media (max-width: 430px) {
  .w-slot { flex-wrap: wrap; }
  .w-slot-trail { flex-basis: 100%; justify-content: flex-end; }
}

/* What IT was asked to prepare, on one's own booking card. A quiet line: it is
   a reminder of what was asked for, not a status. */
.w-prep { color: var(--text-dim); }

/* The booking summary: label and value per row, stacking on a narrow screen
   where two columns would leave four characters a side. Every value here is a
   token this stylesheet already uses; nothing invents one. */
.w-review{margin:var(--space-12) 0 0;display:grid;gap:var(--space-10)}
.w-review__title{margin-bottom:var(--space-4)}
.w-review__row{display:flex;flex-wrap:wrap;gap:var(--space-8);
  justify-content:space-between;align-items:baseline;
  padding-bottom:var(--space-10);border-bottom:1px solid var(--border)}
.w-review__row:last-child{border-bottom:0;padding-bottom:0}
.w-review__row dt{color:var(--text-dim)}
.w-review__row dd{margin:0;font-weight:var(--weight-700);text-align:right}
.w-review__actions{display:flex;gap:var(--space-8);margin-top:var(--space-16)}
.w-review__btn{flex:1}

/* ---------------------------------------------------------------------------
   The Messages page is a column the height of the viewport, not a document.

   Every other page here grows downwards and the window scrolls it. A chat
   cannot: its stream has to scroll inside itself so that the composer stays on
   screen, and a stream can only scroll inside something that has a height. The
   whole of that height arrives here, and the component is told it has been
   given one (`fill`, in js/messaging.js) rather than guessing.

   `100dvh` after `100vh`: on a phone the address bar is part of the viewport
   until it is not, and a chat is exactly the screen where the composer ends up
   underneath it.
   --------------------------------------------------------------------------- */
/* The height is taken once, here, and divided by flex from there down. An
   earlier version subtracted this column's padding in the calc instead — which
   was wrong the moment the viewport narrowed, because below 1024px the padding
   grows by the height of the tab bar and the arithmetic knew nothing about it.
   `border-box` (app.css) puts the padding inside this height whatever it is. */
.w-main--tall {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--w-topbar));
  height: calc(100dvh - var(--w-topbar));
}
/* `min-width: 0` on both, and it is not decoration. A flex item will not
   shrink below the widest thing inside it unless it is told it may, and the
   widest thing in a conversation list is a line of somebody's message. Without
   it this column grew past the <main> holding it and the page scrolled
   sideways on a phone — which is how a block element becoming a flex column
   turns into a horizontal scrollbar. */
.w-page--tall {
  display: flex;
  flex-direction: column;
  flex: 1;
  /* `.w-page` centres itself with `margin: 0 auto`, and an auto margin on the
     cross axis of a flex container switches stretching off — the column then
     takes its content's width and sits in the middle of the screen, which is
     what it did until this line. A definite width restores the fill; the
     `max-width` above still decides where the centring begins. */
  width: 100%;
  min-width: 0;
  min-height: 0;
}
.w-msg-mount {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

/* On a phone this page is the conversation and nothing else, so the line of
   explanation under the title gives its room to the messages. Written as a
   min-width query over a hidden default rather than a max-width one: the
   breakpoint canon this project keeps (tests/test_responsive_qa.py) is a list
   of widths things turn *on* at, and 767.98 is not one of them. */
.w-page--tall .w-page-head p { display: none; }
@media (min-width: 768px) {
  .w-page--tall .w-page-head p { display: block; }
}

/* ---------------------------------------------------------------------------
   The instruction page and the order sheet.

   Both are small; neither invents a value. The order rows are a two-column grid
   rather than a flex row so the quantity boxes line up with each other — four
   drinks whose names are different lengths would otherwise step across the
   sheet.
   --------------------------------------------------------------------------- */
.w-guide { display: grid; gap: var(--space-14); }
.w-guide__play { justify-self: start; }
.w-guide__text { display: grid; gap: var(--space-10); line-height: var(--lh-1-5); }
.w-guide__text p { margin: 0; }

/* ---------------------------------------------------------------------------
   The duty board.

   Three headed groups rather than one list: «сейчас», «сегодня», «дальше».
   Nothing here invents a value, and nothing here is a control except the one
   button on each card — the screen is read far more often than it is used.
   --------------------------------------------------------------------------- */
/* Calling the Coffee Lady group: four options and a line of state.
   The state line is not a status chip — a call is a service attached to a
   booking, not something that happened to the booking. */
/* The two steps before a room goes: the three reasons, then the facts she is
   confirming. Stacked buttons rather than a drop-down — there are three, and a
   control somebody has to open is one more tap than a row. */
.w-duty-why { display: grid; gap: var(--space-8); margin: var(--space-12) 0; }
.w-duty-why__opt { justify-content: flex-start; text-align: left; }
.w-duty-facts { margin: var(--space-10) 0; }
.w-duty-facts p { margin: 0 0 var(--space-4); font-size: var(--fs-13); }

.w-call-opts { display: grid; gap: var(--space-8); margin: var(--space-12) 0; }
.w-call-opt { justify-content: flex-start; text-align: left; }
.w-call-opt.w-on { border-color: var(--accent); color: var(--accent); }
.w-call-note { margin: 0 0 var(--space-4); }
.w-call-state {
  margin: var(--space-6) 0 0;
  font-size: var(--fs-13);
  color: var(--text-faint);
}
.w-call-state--done { color: var(--free); }

.w-duty { display: grid; gap: var(--space-16); }
.w-duty__h {
  margin: 0;
  font-size: var(--fs-13);
  font-weight: var(--weight-620);
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--text-faint);
}
.w-duty__rows { display: grid; gap: var(--space-10); margin-top: var(--space-8); }
.w-duty__card { display: grid; gap: var(--space-8); }
.w-duty__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-10);
}
.w-duty__room { font-size: var(--fs-15); }
.w-duty__note, .w-duty__who { margin: 0; }
.w-duty__who { color: var(--text-faint); font-size: var(--fs-13); }
.w-duty__go { justify-self: start; }

.w-coffee { display: grid; gap: var(--space-8); margin: var(--space-12) 0; }
.w-coffee__row {
  display: grid;
  grid-template-columns: 1fr var(--size-96);
  gap: var(--space-10);
  align-items: center;
}
.w-coffee__name { min-width: 0; }
.w-coffee__qty { text-align: center; }

/* One column on a phone would stack a label above its own number box, which
   reads as two separate fields. They stay side by side and the number box gives
   up width instead. */
@media (max-width: 375px) {
  .w-coffee__row { grid-template-columns: 1fr var(--space-64); }
}
