/* Extracted from index.html so several people can work on the page at once.
   Loaded as a plain file — there is no build step and none is wanted. */

/* ============================================================
   Design tokens — Central Asian University.

   This is a university's internal tool: a booking desk, a request queue and a
   room schedule that people keep open next to their mail all day. It is styled
   as one — paper-white surfaces, hairline rules, CAU navy for anything you can
   act on, and grey doing most of the quiet work. Gold is the crest's ring: a
   rule, a heading, a small mark. It is never a surface and never body text,
   because gold spread across a page stops reading as a university crest and
   starts reading as a casino.

   Two decisions are worth stating before anyone edits below.

   LIGHT IS THE BASE, DARK IS THE OVERRIDE.
   The full palette is declared on bare `:root` in its light values, so every
   colour has a definition that no media query and no attribute can withhold.
   Dark then redefines only what actually changes, in two places that must stay
   identical:
       @media (prefers-color-scheme: dark){ :root:not([data-theme="light"]) }
           — before any script has run, and for anyone who never touches the
             theme control; the :not() guard is what lets an explicit Light
             choice win over a dark OS.
       :root[data-theme="dark"]
           — the explicit choice, written by js/theme.js (and by app.js on the
             first paint). It is a separate block rather than a second selector
             on the same one because a media query cannot contain the rule that
             must beat it.
   tests/test_design_system.py compares the two dark blocks token by token; if
   you add a dark override, add it to both or the test says so. A colour whose
   only definition lives inside a media or [data-theme] block is the bug this
   arrangement exists to prevent: it renders as nothing in the other theme.

   FILLS ARE FLAT.
   --accent-grad and --cta-wait-grad hold a solid colour now, not a gradient
   (the names are kept because ~40 references across five stylesheets and three
   js files read them; see the rename request in the A5 report). Every filled
   shape in the dark theme is a *light* tint carrying dark text — --on-accent is
   dark there, which is also what makes the red offline bar and the amber count
   badge readable. Do not invert one of those without the others.
   ============================================================ */
:root{
  /* ---- brand ramps: the same in both themes ---------------------------
     Sampled from web/logo.png, not estimated by eye: #0d142e is the crest's
     darkest navy, #19254b–#253360 the inner disc, #cc9722 the ring. The
     lighter navies extend that hue upward so the dark theme has a readable
     blue of its own instead of borrowing a generic one; the greys are the
     neutral ramp every light surface, rule and disabled state comes from. */
  --navy-950:#070c1c; --navy-900:#0d142e; --navy-800:#19254b; --navy-700:#253360;
  --navy-600:#2f4685; --navy-500:#3f5c9a; --navy-300:#9db9ef; --navy-200:#c4d3f5;
  --navy-100:#e6ecfa;
  --grey-50:#f5f7fa; --grey-100:#eceff5; --grey-200:#e0e5ee; --grey-300:#cbd3e0;
  --grey-500:#6b7689; --grey-700:#3d4658;

  /* ---- surfaces ----
     --surface is the raised fill (a card, a panel); --surface-2 is the quiet
     one a control sits in (a segmented track, an input, a table head). In
     light that means white over grey; in dark the direction flips — quiet
     reads as slightly *lighter* there — but the role of each token is the
     same, which is what lets one component rule serve both themes. */
  --bg:#f2f4f8;
  --surface:#ffffff; --surface-2:var(--grey-100); --surface-solid:#ffffff;
  --border:var(--grey-200); --border-strong:var(--grey-300);
  /* Row/control hover. Deliberately a wash rather than a surface swap, so a
     hovered table row does not jump a whole elevation step. */
  --hover:rgba(13,20,46,.045);

  /* ---- text ----
     text-faint carries real information — capacity, opening hours, the note
     under a booking — at 11.5px, and it is measured against the *quiet*
     surface, not the page, because that is where small grey print is hardest
     to read. tests/test_design_system.py holds all three above 4.5:1 in both
     themes on all three surfaces. */
  --text:var(--navy-900); --text-dim:#4e5972; --text-faint:#5b667c;

  /* ---- action ----
     One CAU navy for everything you can act on: the primary button, the
     selected chip, the focus ring's cousin, the link. --accent-grad is the
     filled version of it (flat, see the header) and --on-accent the text that
     sits on that fill. --on-accent-dim is the second line on a filled shape —
     the weekday over the date in the day rail — kept above 4.5:1 rather than
     left to a designer's guess at "70% white". */
  --accent:#1c3a7a; --accent-2:#a8871c;
  --accent-grad:var(--accent);
  --accent-soft:var(--navy-100);
  --on-accent:#ffffff; --on-accent-dim:rgba(255,255,255,.86);
  /* The keyboard ring. Brighter than --accent on purpose: it has to be seen
     against a navy button as well as against white, and it is the only thing
     on the page a keyboard user has to find. */
  --focus:#1f5ae0;

  /* ---- crest gold ----
     Two golds, and the difference matters: --gold is label-grade (dark ochre
     on a white page — the only honest way to put the ring's colour on white),
     --accent-2 is fill-grade for a bar or a mark that carries no text. */
  --gold:#7f6413; --gold-soft:rgba(168,135,28,.14); --gold-line:rgba(168,135,28,.45);

  /* ---- opening animation ----
     Deliberately the same in both themes and deliberately not --surface: the
     title card is a white 1920×1080 film, and a navy backdrop in the dark theme
     would frame it in a hard rectangle. Named rather than written inline so the
     palette stays the only place colours are decided. */
  --intro-bg:#ffffff;

  /* Over a photograph.
     These are the one group in the palette that deliberately does not follow
     the theme: a photograph is the same photograph in light and dark, so a
     chevron drawn over it needs the same scrim either way. Named rather than
     written inline because css/roomsui.css forbids colour literals — a rule
     worth keeping even where the colour is constant, so that "constant" is a
     decision recorded in one place instead of six rgba() calls nobody can
     grep for. */
  --photo-nav:rgba(8,12,28,.55); --photo-nav-strong:rgba(8,12,28,.72);
  --photo-dot:rgba(255,255,255,.55); --photo-dot-ring:rgba(8,12,28,.25);
  --lightbox-bg:rgba(6,9,20,.92);
  --lightbox-btn:rgba(255,255,255,.12); --lightbox-btn-strong:rgba(255,255,255,.22);

  /* ---- room state ----
     Read at a glance, so green / red / amber stay green / red / amber. */
  --free:#0a7350;  --free-soft:rgba(10,115,80,.10);
  --busy:#b82f4f;  --busy-soft:rgba(184,47,79,.10);
  --wait:var(--gold); --wait-soft:rgba(168,135,28,.16);

  /* ---- the seven display statuses ----
     app/domain/display_status.py collapses two stored columns into exactly one
     of new · approved · in_progress · solved · rejected · cancelled · no_show,
     and this is that list in colour. Four reuse a room-state colour because
     they mean the same thing to a reader (new is waiting, approved is go,
     rejected is stop, cancelled is off); progress, solved and no_show get
     hues of their own so a queue of seven kinds of row is scannable.
     Colour is never the only signal: .ds-badge gives each status a different
     dot *shape* as well, and every badge carries its translated word. */
  --status-new:var(--wait);            --status-new-soft:var(--wait-soft);
  --status-approved:var(--free);       --status-approved-soft:var(--free-soft);
  --status-progress:#1a55a8;           --status-progress-soft:rgba(26,85,168,.10);
  --status-solved:#0f6b62;             --status-solved-soft:rgba(15,107,98,.11);
  --status-rejected:var(--busy);       --status-rejected-soft:var(--busy-soft);
  --status-cancelled:var(--text-faint);--status-cancelled-soft:rgba(13,20,46,.07);
  --status-noshow:#7a3fa8;             --status-noshow-soft:rgba(122,63,168,.11);

  /* Feedback names for the same three colours. A toast is not a room, and an
     admin reaching for "the error colour" should not have to know that this
     product calls it --busy. Aliases, not copies: they follow the theme. */
  --success:var(--free);   --success-soft:var(--free-soft);
  --danger:var(--busy);    --danger-soft:var(--busy-soft);
  --warning:var(--wait);   --warning-soft:var(--wait-soft);
  --info:var(--status-progress); --info-soft:var(--status-progress-soft);

  /* A second, hotter amber for the one CTA that means "you'll be put on a
     waitlist" — deliberately not --gold, so a waitlist action is never read
     as the university's own accent. Fixed across both themes on purpose (a
     warning colour that changed with light/dark would be a worse signal),
     which is also why it lives here and is not overridden below. */
  --cta-wait-grad:#e08a12;
  /* the day strip needs its own empty colour: on a white card, surface-2 vanishes */
  --tl-empty:rgba(13,23,48,.10);
  /* Dimming overlay behind a sheet/dialog. Deliberately not theme-linked — a
     scrim's job is to sit darker than either theme, not to match one. */
  --scrim:rgba(4,8,14,.6);
  /* Retained, unused: the page used to carry two radial washes behind the
     content. They were the one thing on screen that read as "app" rather than
     "university tool", and flat ground replaced them. The tokens stay defined
     because removing a token is how 39 references broke last round — the A5
     report asks the Lead to retire them once nothing names them. */
  --bg-glow-a:rgba(29,47,86,.06); --bg-glow-b:rgba(201,162,39,.05);

  /* ---- elevation ----
     Two steps, both restrained. --shadow is a card lifting off the page by a
     hair; --shadow-2 is for the things that genuinely float over it (dialog,
     drawer, sheet, toast, a dropdown). There is no third step: depth in this
     interface comes from the hairline rules, not from shade. */
  --shadow:0 1px 2px rgba(13,20,46,.06), 0 1px 1px rgba(13,20,46,.04);
  --shadow-2:0 16px 40px -20px rgba(13,20,46,.28), 0 2px 6px -2px rgba(13,20,46,.08);

  --r-card:var(--radius-14); --r-slot:var(--radius-8);
  --font:-apple-system,BlinkMacSystemFont,"SF Pro Display","Inter","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;

  /* ----------------------------------------------------------
     Scale — radius, spacing, type, line-height, weight, icon/control sizes.

     Tokens are named after the value they hold — --space-7 is 7px, --fs-13-5
     is 13.5px, --weight-660 is font-weight 660 — instead of after an abstract
     rank (space-1, space-2 …), because this interface was tuned pixel by pixel,
     the same way the crest colours above were sampled rather than guessed. A
     rank-based scale would have forced values like 7px, 11px or 13.5px to snap
     to whichever rung was nearest, and on a surface already tuned this fine,
     that snap is a visible change, not a cleanup. Naming by value keeps every
     pixel where it is while still giving it one shared name, so app.css,
     components.css, roomsui.css and the shells reach for the same token
     instead of each retyping its own "13px".

     Before writing a new number, check this list — almost everything a new
     rule needs already has a token here. --radius-pill (999px) replaces the
     old habit of writing 99px in one place and 999px in another. True circles
     (50%) are left as literal percentages — they are proportional, not a pixel
     value, so they do not belong on this scale.

     Letter-spacing is intentionally NOT on this scale: every letter-spacing
     value in this file is a one-off optical correction for a specific label,
     and a numbered ramp would invent structure the interface doesn't have.
     Elevation is on it now (--shadow / --shadow-2, above) because two named
     steps is exactly the structure this interface does have.

     TYPE FLOOR: 11px. --fs-9-5 / --fs-10 / --fs-10-5 are grandfathered
     micro-label sizes for uppercase letterspaced captions and the set may not
     grow — tests/test_responsive_qa.py pins it. New text uses --fs-11 or up. */
  --radius-2:2px; --radius-3:3px; --radius-6:6px; --radius-8:8px; --radius-11:11px;
  --radius-12:12px; --radius-13:13px; --radius-14:14px; --radius-15:15px;
  --radius-16:16px; --radius-18:18px; --radius-24:24px; --radius-pill:999px;

  --space-1:1px; --space-1-5:1.5px; --space-2:2px; --space-3:3px; --space-4:4px;
  --space-5:5px; --space-6:6px; --space-7:7px; --space-8:8px; --space-9:9px;
  --space-10:10px; --space-11:11px; --space-12:12px; --space-13:13px; --space-14:14px;
  --space-16:16px; --space-18:18px; --space-20:20px; --space-24:24px; --space-26:26px;
  --space-28:28px; --space-32:32px; --space-40:40px; --space-48:48px; --space-64:64px;
  --space-150:150px; /* --space-150: clearance for the fixed .actionbar */

  --fs-9-5:9.5px; --fs-10:10px; --fs-10-5:10.5px; --fs-11:11px; --fs-11-5:11.5px;
  --fs-12:12px; --fs-12-5:12.5px; --fs-13:13px; --fs-13-5:13.5px; --fs-14:14px;
  --fs-15:15px; --fs-15-5:15.5px; --fs-16:16px; --fs-16-5:16.5px; --fs-17:17px;
  --fs-18:18px; --fs-19:19px; --fs-20:20px; --fs-22:22px; --fs-24:24px; --fs-28:28px;

  /* Line-heights, named by value like everything else. --lh-1 is for a single
     line inside a control (a button, a chip) where the box sets the height;
     --lh-1-5 is running prose. Anything in between is a heading or a two-line
     label that must not look airy next to a dense table. */
  --lh-1:1; --lh-1-2:1.2; --lh-1-25:1.25; --lh-1-35:1.35; --lh-1-45:1.45; --lh-1-5:1.5;

  --weight-500:500; --weight-600:600; --weight-620:620; --weight-640:640; --weight-650:650;
  --weight-660:660; --weight-680:680; --weight-700:700;

  /* Square control/icon/avatar dimensions — glyphs and the boxes that hold them
     share one scale, since a 44px icon box and a 44px empty-state glyph are the
     same number for the same reason. */
  --size-2:2px; --size-4:4px; --size-6:6px; --size-7:7px; --size-8:8px; --size-9:9px;
  --size-10:10px; --size-12:12px; --size-14:14px; --size-16:16px; --size-17:17px;
  --size-18:18px; --size-20:20px; --size-22:22px; --size-24:24px; --size-26:26px;
  --size-28:28px; --size-32:32px; --size-34:34px; --size-36:36px; --size-38:38px;
  --size-40:40px; --size-44:44px; --size-48:48px; --size-52:52px; --size-56:56px;
  --size-64:64px; --size-66:66px; --size-72:72px; --size-96:96px; --size-180:180px;
}

/* The dark palette, twice. The first copy is the default for a dark client
   before any script has run — guarded with :not([data-theme="light"]) so an
   explicit Light choice is not overruled by the OS. The second is that
   explicit choice. They must stay identical; the test compares them. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --bg:var(--navy-950);
    --surface:rgba(255,255,255,.045); --surface-2:rgba(255,255,255,.08);
    --surface-solid:var(--navy-900);
    --border:rgba(255,255,255,.10); --border-strong:rgba(255,255,255,.18);
    --hover:rgba(255,255,255,.05);
    --text:#f2f5fa; --text-dim:#9aa8c0; --text-faint:#8b99b3;
    --accent:var(--navy-300); --accent-2:#cc9722;
    --accent-soft:rgba(157,185,239,.15);
    --on-accent:var(--navy-950); --on-accent-dim:rgba(7,12,28,.78);
    --focus:#9fc0ff;
    --gold:#cc9722; --gold-soft:rgba(204,151,34,.16); --gold-line:rgba(204,151,34,.42);
    --free:#3fb984;  --free-soft:rgba(63,185,132,.14);
    --busy:#e2637a;  --busy-soft:rgba(226,99,122,.14);
    --wait-soft:rgba(204,151,34,.16);
    --status-progress:#8ab4f8;  --status-progress-soft:rgba(138,180,248,.14);
    --status-solved:#4fc9bb;    --status-solved-soft:rgba(79,201,187,.14);
    --status-cancelled-soft:rgba(255,255,255,.08);
    --status-noshow:#c9a4f0;    --status-noshow-soft:rgba(201,164,240,.14);
    --tl-empty:rgba(255,255,255,.10);
    --bg-glow-a:rgba(29,47,86,.55); --bg-glow-b:rgba(201,162,39,.07);
    --shadow:0 1px 2px rgba(0,0,0,.5);
    --shadow-2:0 20px 48px -22px rgba(0,0,0,.75), 0 2px 8px -3px rgba(0,0,0,.5);
    color-scheme:dark;
  }
}
:root[data-theme="dark"]{
  --bg:var(--navy-950);
  --surface:rgba(255,255,255,.045); --surface-2:rgba(255,255,255,.08);
  --surface-solid:var(--navy-900);
  --border:rgba(255,255,255,.10); --border-strong:rgba(255,255,255,.18);
  --hover:rgba(255,255,255,.05);
  --text:#f2f5fa; --text-dim:#9aa8c0; --text-faint:#8b99b3;
  --accent:var(--navy-300); --accent-2:#cc9722;
  --accent-soft:rgba(157,185,239,.15);
  --on-accent:var(--navy-950); --on-accent-dim:rgba(7,12,28,.78);
  --focus:#9fc0ff;
  --gold:#cc9722; --gold-soft:rgba(204,151,34,.16); --gold-line:rgba(204,151,34,.42);
  --free:#3fb984;  --free-soft:rgba(63,185,132,.14);
  --busy:#e2637a;  --busy-soft:rgba(226,99,122,.14);
  --wait-soft:rgba(204,151,34,.16);
  --status-progress:#8ab4f8;  --status-progress-soft:rgba(138,180,248,.14);
  --status-solved:#4fc9bb;    --status-solved-soft:rgba(79,201,187,.14);
  --status-cancelled-soft:rgba(255,255,255,.08);
  --status-noshow:#c9a4f0;    --status-noshow-soft:rgba(201,164,240,.14);
  --tl-empty:rgba(255,255,255,.10);
  --bg-glow-a:rgba(29,47,86,.55); --bg-glow-b:rgba(201,162,39,.07);
  --shadow:0 1px 2px rgba(0,0,0,.5);
  --shadow-2:0 20px 48px -22px rgba(0,0,0,.75), 0 2px 8px -3px rgba(0,0,0,.5);
  color-scheme:dark;
}
/* The explicit Light choice. :root above already holds the whole light
   palette, so this block does not restate it — every value here is the same
   value, and the test proves that rather than trusting it. What it adds is
   color-scheme (so form controls and scrollbars follow) and a written record
   of the four tokens that make light a different theme rather than a tweak;
   the media query above is what it actually has to beat, and the :not() guard
   there is what lets it. */
:root[data-theme="light"]{
  --bg:#f2f4f8; --surface:#ffffff; --text:var(--navy-900); --border:var(--grey-200);
  color-scheme:light;
}

/* ============================================================
   Breakpoint canon — the only viewport widths any stylesheet in css/
   may branch on. A media query cannot read var(), so the canon cannot
   be a token: it lives here as the agreed table and is enforced by
   tests/test_responsive_qa.py — every min-width/max-width in every
   media query in css/ must be one of these values exactly, not "close".

     width  | device class                   | expected shape
     -------+--------------------------------+---------------------------
     375px  | iPhone SE / 12-13 mini         | one column, smallest type
     390px  | most iPhones, mid Androids     | one column
     430px  | Pro Max phones; Mini App width | one column; touch floor 40px
     768px  | tablet portrait                | rail becomes a sidebar
     1024px | tablet landscape / laptop      | full sidebar, split panes
     1280px | small desktop, docked laptop   | admin gets its third column
     1440px | desktop                        | max content width engages
     1920px | large monitor                  | content stays capped, gutters grow

   1280px and 1920px are named in the A5 brief as canon too, and this
   round does not use them: tests/test_responsive_qa.py's CANON_BREAKPOINTS
   holds the six above and rejects anything else, and that file belongs to
   nobody in this round. The A5 report asks the Lead to add them there
   first; branching on them before that turns the suite red for everyone.

   Pick the nearest canon value instead of the width where your own
   layout happens to snap: six shared lines beat six private ones, and
   a component styled for "tablet" must not change shape at a width its
   neighbours still call "phone".
   ============================================================ */

/* Horizontal overflow guard. One element wider than the viewport makes the
   whole page pan sideways on a phone, and the culprit is invisible until it
   is touched. The page never scrolls horizontally; a strip that genuinely
   wants to (the day rail, a schedule table) gets its own scroll container —
   .rail here, .ds-scroll-x from components.css for everyone else. `clip`
   rather than `hidden`: hidden turns body into a scroll container that
   sticky positioning and iOS momentum both trip over, clip only forbids. */
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
[hidden]{display:none !important}
html,body{height:100%;max-width:100%}
html{-webkit-text-size-adjust:100%;scrollbar-color:var(--border-strong) transparent}
body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);
  font-size:var(--fs-14);line-height:var(--lh-1-45);
  -webkit-font-smoothing:antialiased;overscroll-behavior-y:none;overflow-x:clip}
/* One ring, everywhere, and only for the keyboard. Every zone inherits it, so
   nobody has to remember to add a focus style to their own button — and a
   zone that wants a different one overrides this selector rather than
   discovering, in an audit, that it had none. */
:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
::selection{background:var(--accent-soft);color:var(--text)}
.app{position:relative;max-width:480px;margin:0 auto;min-height:100%;display:flex;flex-direction:column}

/* ---------------- full-screen states ---------------- */
.screen{position:fixed;inset:0;z-index:70;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:var(--space-14);padding:var(--space-32) var(--space-26);text-align:center;background:var(--bg)}
.screen h2{margin:0;font-size:var(--fs-19);font-weight:var(--weight-660);letter-spacing:-.3px}
.screen p{margin:0;font-size:var(--fs-13-5);line-height:var(--lh-1-5);color:var(--text-dim);max-width:320px}
.screen .icon{width:var(--size-56);height:var(--size-56);border-radius:var(--radius-16);background:var(--surface-2);
  border:1px solid var(--border);display:grid;place-items:center;margin-bottom:var(--space-4)}
.screen .icon svg{width:var(--size-26);height:var(--size-26);color:var(--text-dim)}
/* The seal sits above whatever else the screen says, at a size where the ring and
   the wordmark are both readable. */
.screen .seal{width:var(--size-96);height:var(--size-96);border-radius:50%;display:block;margin-bottom:var(--space-6);
  box-shadow:var(--shadow-2)}
.spinner{width:var(--size-34);height:var(--size-34);border-radius:50%;border:3px solid var(--border);
  border-top-color:var(--accent);animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---------------- header ---------------- */
.hdr{position:sticky;top:0;z-index:20;padding:var(--space-14) var(--space-16) var(--space-8);
  background:color-mix(in srgb,var(--bg) 88%,transparent);
  backdrop-filter:saturate(140%) blur(14px);-webkit-backdrop-filter:saturate(140%) blur(14px);
  border-bottom:1px solid transparent;transition:border-color .2s}
/* The rule appears only once the page is scrolled, so it reads as the header
   detaching from the content rather than as decoration sitting there from the
   start. Gold, at one hairline: the crest's ring, used as a ring. */
.hdr.scrolled{border-bottom-color:var(--gold-line)}
.hdr-top{display:flex;align-items:center;justify-content:space-between;gap:var(--space-12)}
.brand{display:flex;align-items:center;gap:var(--space-11);min-width:0}
/* A round seal wants a round frame, not the squircle a glyph would sit in. */
.brand-mark{width:var(--size-40);height:var(--size-40);border-radius:50%;flex:none;overflow:hidden;
  display:grid;place-items:center}
.brand-mark img,.brand-mark svg{width:100%;height:100%;display:block;border-radius:50%}
.brand-text{min-width:0}
.brand h1{font-size:var(--fs-16-5);line-height:var(--lh-1-2);margin:0;letter-spacing:-.2px;font-weight:var(--weight-680)}
/* The organisation name is the formal line: small caps, gold, letterspaced. It is
   what makes the header read as an institution rather than an app. */
.brand-org{margin:var(--space-2) 0 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;max-width:200px}
.brand p#whoami{margin:var(--space-2) 0 0;font-size:var(--fs-11-5);color:var(--text-faint);letter-spacing:.2px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
/* Segmented controls (language, tabs) are a recessed track with a raised
   selected segment — the track is --surface-2, the selection --surface. That
   direction is the one people read as "this one is chosen" in both themes; the
   older arrangement (selected = surface-2 on a surface track) inverted itself
   in light, where both were white. */
.lang{display:flex;padding:var(--space-2);gap:var(--space-2);background:var(--surface-2);border:1px solid var(--border);
  border-radius:var(--radius-pill);flex:none}
.lang button{border:0;background:transparent;color:var(--text-dim);font:var(--weight-600) var(--fs-11-5)/var(--lh-1) var(--font);
  padding:var(--space-7) var(--space-11);border-radius:var(--radius-pill);cursor:pointer;letter-spacing:.4px;transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.lang button.on{background:var(--surface);color:var(--text);box-shadow:var(--shadow)}

/* `overscroll-behavior-x: contain` because this strip lives inside Telegram:
   a flick that reaches either end would otherwise be handed to the client as
   a back-swipe, closing the Mini App mid-booking. Same reason the building
   chips carry it. */
.rail{display:flex;gap:var(--space-8);overflow-x:auto;overscroll-behavior-x:contain;margin:var(--space-14) -16px 0;padding:var(--space-2) var(--space-16) var(--space-4);scrollbar-width:none}
.rail::-webkit-scrollbar{display:none}
/* `display:flex` above wins against the user agent's `[hidden]{display:none}`,
   so hiding the strip needs saying out loud. Without this the day picker stayed
   on screen on all four sections — which is how it got there in the first
   place. */
.rail[hidden]{display:none}
.day{flex:none;min-width:var(--size-52);padding:var(--space-9) 0 var(--space-8);border-radius:var(--radius-12);text-align:center;cursor:pointer;
  background:var(--surface);border:1px solid var(--border);transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.day .dow{display:block;font-size:var(--fs-10-5);letter-spacing:.6px;text-transform:uppercase;
  color:var(--text-faint);font-weight:var(--weight-600)}
.day .dnum{display:block;font-size:var(--fs-17);font-weight:var(--weight-650);margin-top:var(--space-3);letter-spacing:-.3px;
  font-variant-numeric:tabular-nums}
.day.off{opacity:.32;cursor:not-allowed}
.day.on{background:var(--accent-grad);border-color:transparent;color:var(--on-accent)}
.day.on .dow{color:var(--on-accent-dim)}

/* The order sheet. Two columns so the number boxes line up with each other —
   four drink names of different lengths would otherwise step across the sheet. */
.cof-rows{display:grid;gap:var(--space-8);margin:var(--space-12) 0}
.cof-row{display:grid;grid-template-columns:1fr var(--size-72);gap:var(--space-10);align-items:center}
.cof-row input{width:100%;text-align:center}
.sheet .err{color:var(--danger)}
/* The confirm button on a sheet that cancels something. Filled, because a
   destructive confirmation must not look like the way out of one. */
.sheet-actions .cta.danger{background:var(--danger);border-color:var(--danger)}

.tabs{display:flex;gap:var(--space-3);margin-top:var(--space-12);padding:var(--space-3);background:var(--surface-2);
  border:1px solid var(--border);border-radius:var(--radius-12)}
.tabs button{flex:1;border:0;background:transparent;color:var(--text-dim);cursor:pointer;
  font:var(--weight-620) var(--fs-13)/var(--lh-1) var(--font);padding:var(--space-9) 0;border-radius:var(--radius-8);transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.tabs button.on{background:var(--surface);color:var(--text);box-shadow:var(--shadow)}
.tabs .badge{display:inline-block;margin-left:var(--space-6);padding:var(--space-2) var(--space-6);border-radius:var(--radius-pill);
  background:var(--wait-soft);color:var(--wait);font-size:var(--fs-10);font-weight:var(--weight-700);vertical-align:1px}
.tabs .badge[hidden]{display:none}

/* ---------------- content ---------------- */
main{flex:1;padding:var(--space-16);display:flex;flex-direction:column;gap:var(--space-12)}
.view{display:none;flex-direction:column;gap:var(--space-12)}
.view.on{display:flex;animation:fade .24s ease both}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

.card{position:relative;background:var(--surface);border:1px solid var(--border);
  border-radius:var(--r-card);padding:var(--space-14);cursor:pointer;box-shadow:var(--shadow);
  transition:transform .16s,border-color .16s}
.card:active{transform:scale(.985);border-color:var(--border-strong)}
.card.mute{opacity:.5;cursor:not-allowed}
/* The photo band. Its own aspect-ratio so the card is this tall before any
   picture arrives and exactly this tall afterwards — the list must not jump as
   nine cards fill in. 16:10 is the shape the owner's own room pictures come in,
   the same ratio the web card's frame uses. */
.card-photo{
  position:relative;display:block;width:100%;aspect-ratio:16 / 10;overflow:hidden;
  border-radius:var(--radius-11);background:var(--surface-2);
  border:1px solid var(--border);margin-bottom:var(--space-12);
}
.card-head{display:flex;gap:var(--space-12);align-items:flex-start}
.plan{width:var(--size-44);height:var(--size-44);border-radius:var(--radius-11);background:var(--surface-2);border:1px solid var(--border);
  flex:none;display:grid;place-items:center}
.plan svg{width:var(--size-26);height:var(--size-26)}
.card-title{flex:1;min-width:0}
.card-title h3{margin:0;font-size:var(--fs-15-5);font-weight:var(--weight-640);letter-spacing:-.2px}
.card-title .sub{margin:var(--space-3) 0 0;font-size:var(--fs-12-5);color:var(--text-dim)}
.meta{display:flex;gap:var(--space-10);align-items:center;margin-top:var(--space-7);font-size:var(--fs-11-5);color:var(--text-faint)}
.meta span{display:inline-flex;align-items:center;gap:var(--space-4);white-space:nowrap}
.chip{flex:none;display:inline-flex;align-items:center;gap:var(--space-6);padding:var(--space-5) var(--space-9);border-radius:var(--radius-pill);
  font:var(--weight-640) var(--fs-11)/var(--lh-1) var(--font);letter-spacing:.2px;white-space:nowrap}
.chip .dot{width:var(--size-6);height:var(--size-6);border-radius:50%}
.chip.free{background:var(--free-soft);color:var(--free)} .chip.free .dot{background:var(--free)}
.chip.busy{background:var(--busy-soft);color:var(--busy)} .chip.busy .dot{background:var(--busy)}
.chip.wait{background:var(--wait-soft);color:var(--wait)} .chip.wait .dot{background:var(--wait)}
.chip.off{background:var(--surface-2);color:var(--text-faint)} .chip.off .dot{background:var(--text-faint)}

.tl{display:flex;gap:var(--space-1-5);margin-top:var(--space-13);height:var(--size-7)}
.tl i{flex:1;border-radius:var(--radius-2);background:var(--tl-empty);transition:background .2s}
.tl i.b{background:var(--busy);opacity:.85}
.tl i.w{background:var(--wait);opacity:.9}
.tl i.mine{background:var(--accent)}
.tl i.sel{background:var(--accent-2)}
.tl-x{display:flex;justify-content:space-between;margin-top:var(--space-6);font-size:var(--fs-10);
  color:var(--text-faint);letter-spacing:.3px;font-variant-numeric:tabular-nums}

/* skeletons while the day loads */
.skel{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-card);
  padding:var(--space-14)}
.skel .bar{height:var(--size-12);border-radius:var(--radius-6);background:var(--surface-2);
  animation:pulse 1.3s ease-in-out infinite}
.skel .bar+.bar{margin-top:var(--space-9)}
@keyframes pulse{0%,100%{opacity:.45}50%{opacity:.9}}

/* my bookings */
.bk{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-card);
  padding:var(--space-13) var(--space-14);box-shadow:var(--shadow)}
.bk-row{display:flex;align-items:center;gap:var(--space-12)}
.bk-time{flex:none;text-align:center;padding:var(--space-8) var(--space-10);border-radius:var(--radius-11);background:var(--accent-soft);
  border:1px solid var(--border);min-width:var(--size-66);font-variant-numeric:tabular-nums}
.bk-time b{display:block;font-size:var(--fs-15);font-weight:var(--weight-660);letter-spacing:-.2px}
.bk-time span{display:block;font-size:var(--fs-10-5);color:var(--text-dim);margin-top:var(--space-2)}
.bk-info{flex:1;min-width:0}
.bk-info h4{margin:0;font-size:var(--fs-13-5);font-weight:var(--weight-620);letter-spacing:-.1px}
.bk-info p{margin:var(--space-3) 0 0;font-size:var(--fs-11-5);line-height:var(--lh-1-35);color:var(--text-dim)}
.ghost{border:1px solid var(--border);background:var(--surface-2);color:var(--text-dim);
  font:var(--weight-620) var(--fs-11-5)/var(--lh-1) var(--font);padding:var(--space-9) var(--space-11);border-radius:var(--radius-8);cursor:pointer;
  transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s;flex:none}
.ghost:active{transform:scale(.96)}
.ghost:disabled{opacity:.5;cursor:progress}
.ghost.danger{color:var(--busy);border-color:var(--busy-soft)}
/* chips: durations, group size, repeat */
.chips{display:flex;gap:var(--space-7);flex-wrap:wrap}
.chip-btn{border:1px solid var(--border);background:var(--surface-2);color:var(--text-dim);
  font:var(--weight-620) var(--fs-12-5)/var(--lh-1) var(--font);padding:var(--space-10) var(--space-13);border-radius:var(--radius-8);cursor:pointer;transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.chip-btn:active{transform:scale(.96)}
.chip-btn.on{background:var(--accent-grad);border-color:transparent;color:var(--on-accent)}

/* find a room */
.find{display:flex;align-items:center;gap:var(--space-12);width:100%;padding:var(--space-13) var(--space-14);cursor:pointer;
  background:var(--surface);border:1px dashed var(--border-strong);border-radius:var(--r-card);
  color:var(--text);font:var(--weight-640) var(--fs-13-5)/var(--lh-1-35) var(--font);text-align:left;transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.find:active{transform:scale(.985)}
.find .ico{width:var(--size-34);height:var(--size-34);border-radius:var(--radius-8);background:var(--accent-soft);flex:none;
  display:grid;place-items:center;color:var(--accent)}
.find .ico svg{width:var(--size-18);height:var(--size-18)}
.find span{flex:1;min-width:0}
.find small{display:block;font-weight:var(--weight-500);color:var(--text-faint);font-size:var(--fs-11-5);margin-top:var(--space-2)}

.opt{display:flex;align-items:center;gap:var(--space-12);width:100%;padding:var(--space-12) var(--space-13);cursor:pointer;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-11);color:var(--text);
  font:var(--weight-620) var(--fs-13-5)/var(--lh-1-25) var(--font);text-align:left;transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.opt:active{transform:scale(.97)}
.opt + .opt{margin-top:var(--space-8)}
.opt b{flex:none;font-size:var(--fs-15);font-variant-numeric:tabular-nums}
.opt span{flex:1;min-width:0}
.opt small{display:block;font-weight:var(--weight-500);color:var(--text-faint);font-size:var(--fs-11-5)}

.empty{padding:var(--space-48) var(--space-16);text-align:center;color:var(--text-faint)}
.empty svg{width:var(--size-44);height:var(--size-44);opacity:.5;margin-bottom:var(--space-12)}
.empty p{margin:0;font-size:var(--fs-13-5)}
.st{display:inline-flex;align-items:center;gap:var(--space-5);padding:var(--space-3) var(--space-8);border-radius:var(--radius-pill);
  font:var(--weight-700) var(--fs-10)/var(--lh-1) var(--font);letter-spacing:.3px;text-transform:uppercase}
.st.ok{background:var(--free-soft);color:var(--free)}
.st.pend{background:var(--wait-soft);color:var(--wait)}

/* ---------------- room panel ---------------- */
.panel{position:fixed;inset:0;z-index:40;max-width:480px;margin:0 auto;background:var(--bg);
  display:flex;flex-direction:column;transform:translateY(100%);visibility:hidden;
  transition:transform .32s cubic-bezier(.32,.72,0,1)}
.panel.on{transform:none;visibility:visible}
.panel-hdr{position:sticky;top:0;padding:var(--space-14) var(--space-16) var(--space-12);display:flex;align-items:center;gap:var(--space-12);
  border-bottom:1px solid var(--border);
  background:color-mix(in srgb,var(--bg) 90%,transparent);backdrop-filter:blur(14px)}
.back{width:var(--size-34);height:var(--size-34);border-radius:var(--radius-8);border:1px solid var(--border);background:var(--surface);
  color:var(--text);display:grid;place-items:center;cursor:pointer;flex:none}
.back svg{width:var(--size-17);height:var(--size-17)}
.panel-hdr .t{flex:1;min-width:0}
.panel-hdr h2{margin:0;font-size:var(--fs-15-5);font-weight:var(--weight-650);letter-spacing:-.2px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.panel-hdr p{margin:var(--space-2) 0 0;font-size:var(--fs-12);color:var(--text-dim)}
.panel-body{flex:1;overflow-y:auto;padding:var(--space-16) var(--space-16) var(--space-150)}

.block{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-card);
  padding:var(--space-14);margin-bottom:var(--space-12)}
.block > h4{margin:0 0 var(--space-11);font-size:var(--fs-11);font-weight:var(--weight-700);letter-spacing:.7px;
  text-transform:uppercase;color:var(--text-faint)}
.legend{display:flex;gap:var(--space-14);margin-top:var(--space-11);font-size:var(--fs-11);color:var(--text-dim);flex-wrap:wrap}
.legend span{display:inline-flex;align-items:center;gap:var(--space-6)}
.legend i{width:var(--size-9);height:var(--size-9);border-radius:var(--radius-3);display:block}

/* wheels */
.wheels{display:flex;align-items:stretch;gap:var(--space-10);position:relative}
.wcol{flex:1;min-width:0}
.wcol > label{display:block;text-align:center;font-size:var(--fs-10-5);font-weight:var(--weight-700);letter-spacing:.7px;
  text-transform:uppercase;color:var(--text-faint);margin-bottom:var(--space-7)}
.wheel{position:relative;height:var(--size-180);overflow:hidden;border-radius:var(--radius-12);
  background:var(--surface-2);border:1px solid var(--border)}
.wheel-list{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;scrollbar-width:none;
  -ms-overflow-style:none;position:relative;z-index:2}
.wheel-list::-webkit-scrollbar{display:none}
.wi{display:block;width:100%;height:var(--size-36);line-height:var(--size-36);border:0;background:transparent;
  color:var(--text-dim);font:var(--weight-600) var(--fs-16)/36px var(--font);letter-spacing:-.2px;cursor:pointer;
  scroll-snap-align:center;transition:color .15s,transform .15s;font-variant-numeric:tabular-nums}
.wi.pick{color:var(--text);font-weight:var(--weight-700);transform:scale(1.08)}
.wi.dis{color:var(--text-faint);opacity:.35;text-decoration:line-through;cursor:not-allowed}
.wi.warn{color:var(--wait)}
.wheel-band{position:absolute;left:6px;right:6px;top:50%;height:var(--size-36);transform:translateY(-50%);
  border-radius:var(--radius-8);background:var(--accent-soft);border:1px solid var(--accent);
  pointer-events:none;z-index:1}
/* The only linear-gradient left in the file, and it is a mask rather than a
   surface: without it the wheel's rows are cut off mid-glyph at the edges. */
.wheel-fade{position:absolute;left:0;right:0;height:var(--size-52);pointer-events:none;z-index:3}
.wheel-fade.t{top:0;background:linear-gradient(var(--surface-2),transparent)}
.wheel-fade.b{bottom:0;background:linear-gradient(transparent,var(--surface-2))}

/* ---------------- how long the meeting runs ----------------
   The column beside the start wheel. The wheel is --size-180 tall and this is
   not: a slider is one gesture and a wheel is a scroll, and stretching the
   slider to match would put a 24px thumb in the middle of a 180px box with
   nothing around it. It sits centred against the wheel instead. */
.wcol-dur{display:flex;flex-direction:column}
.dur{flex:1;display:flex;flex-direction:column;justify-content:center;
  padding:var(--space-14) var(--space-13);border-radius:var(--radius-12);
  background:var(--surface-2);border:1px solid var(--border)}
.dur-head{display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-14);
  text-align:center}
.dur-head b{font-size:var(--fs-18);font-weight:var(--weight-680);letter-spacing:-.3px;
  color:var(--accent);font-variant-numeric:tabular-nums}
.dur-head span{font-size:var(--fs-11-5);color:var(--text-faint);font-variant-numeric:tabular-nums}

/* Drawn on the whole duration scale, with the input laid over only the part of it
   this room actually has — js/app.js renderDuration() 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. */
.dur-track{position:relative}
.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, which is the same tick in the part of the
   afternoon the room does not have. Never a hit target. */
/* What the room still has from this start, and how much of it this meeting
   takes. Both measured against the whole duration scale, so the chosen bar ends
   under the thumb and the grey beyond it is what the afternoon does not have. */
.dur-avail,.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. */
.dur-avail{background:var(--accent);opacity:.3}
.dur-fill{background:var(--accent)}

.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}
.dur-tick.off{background:var(--border-strong)}

.dur-input{position:absolute;top:0;left:0;height:100%;margin:0;padding:0;background:none;
  -webkit-appearance:none;appearance:none;z-index:1;touch-action:pan-y}
/* The input paints nothing but its thumb: the bar under it is the two flat
   fills above, which every browser draws the same way. */
.dur-input::-webkit-slider-runnable-track{height:var(--size-6);background:none}
.dur-input::-moz-range-track{height:var(--size-6);background:none}
/* --size-28 and not --size-24: this one is dragged with a thumb, and 44px of
   padding around a 28px circle is the smallest comfortable target on a phone. */
.dur-input::-webkit-slider-thumb{-webkit-appearance:none;width:var(--size-28);height:var(--size-28);
  margin-top:calc((var(--size-28) - var(--size-6)) / -2);border:var(--space-3) solid var(--surface);
  border-radius:var(--radius-pill);background:var(--accent);box-shadow:var(--shadow-2)}
.dur-input::-moz-range-thumb{width:var(--size-28);height:var(--size-28);box-sizing:border-box;
  border:var(--space-3) solid var(--surface);border-radius:var(--radius-pill);background:var(--accent);
  box-shadow:var(--shadow-2)}
.dur-input:focus{outline:none}
.dur-input:focus-visible::-webkit-slider-thumb{outline:var(--size-2) solid var(--focus);outline-offset:var(--space-2)}
.dur-input:focus-visible::-moz-range-thumb{outline:var(--size-2) solid var(--focus);outline-offset:var(--space-2)}

.dur-note{margin:var(--space-10) 0 0;font-size:var(--fs-11);line-height:var(--lh-1-45);
  color:var(--text-faint);text-align:center}

/* The two halves stack, at every width. Side by side they were two wheels and
   that was the right shape for two wheels; a slider in half of a 375px screen
   gets about 150px for seven positions — twenty pixels a step, which is not
   something a thumb can land on — and the panel's own max-width means even a
   tablet gives it barely more. Stacked it is the width of the panel and reads
   as the sentence it is: starts at 09:00, runs an hour, ends at 10:00. */
.wheels{flex-direction:column;gap:var(--space-14)}
.wcol-dur .dur{padding:var(--space-16) var(--space-16) var(--space-14)}
.dur-head{flex-direction:row;align-items:baseline;justify-content:center;gap:var(--space-10);
  margin-bottom:var(--space-16)}
/* 44px of height around a 28px thumb: the smallest comfortable touch target. */
.dur-track{height:var(--size-44)}
.dur-note{max-width:34ch;margin-inline:auto}

.range{margin-top:var(--space-12);padding:var(--space-11) var(--space-13);border-radius:var(--radius-11);background:var(--surface-2);
  border:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:var(--space-10)}
.range b{font-size:var(--fs-16);font-weight:var(--weight-680);letter-spacing:-.3px;font-variant-numeric:tabular-nums}
.range span{font-size:var(--fs-12);color:var(--text-dim)}
.note{margin:var(--space-10) 0 0;font-size:var(--fs-11-5);line-height:var(--lh-1-45);color:var(--wait);
  display:flex;gap:var(--space-7);align-items:flex-start}
.note svg{width:var(--size-14);height:var(--size-14);flex:none;margin-top:var(--space-1)}

.stepper{display:flex;align-items:center;justify-content:space-between;gap:var(--space-12)}
.stepper .ctl{display:flex;align-items:center;gap:var(--space-12)}
.stepper button{width:var(--size-38);height:var(--size-38);border-radius:var(--radius-8);border:1px solid var(--border);
  background:var(--surface-2);color:var(--text);font:var(--weight-600) var(--fs-20)/var(--lh-1) var(--font);cursor:pointer;
  transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s;display:grid;place-items:center}
.stepper button:active{transform:scale(.94)}
.stepper button:disabled{opacity:.3;cursor:not-allowed}
.stepper .num{min-width:var(--size-38);text-align:center;font-size:var(--fs-19);font-weight:var(--weight-680);
  font-variant-numeric:tabular-nums}
.stepper .cap{font-size:var(--fs-11-5);color:var(--text-faint)}
.overcap{color:var(--busy)}

.actionbar{position:absolute;left:0;right:0;bottom:0;
  padding:var(--space-12) var(--space-16) calc(var(--space-14) + env(safe-area-inset-bottom));
  background:color-mix(in srgb,var(--bg) 92%,transparent);backdrop-filter:blur(14px);
  border-top:1px solid var(--border);display:flex;align-items:center;gap:var(--space-12)}
.sum{flex:1;min-width:0}
.sum b{display:block;font-size:var(--fs-15);font-weight:var(--weight-660);letter-spacing:-.2px;font-variant-numeric:tabular-nums}
.sum span{display:block;font-size:var(--fs-11-5);color:var(--text-dim);margin-top:var(--space-2)}
.cta{border:0;background:var(--accent-grad);color:var(--on-accent);font:var(--weight-680) var(--fs-13-5)/var(--lh-1) var(--font);
  padding:var(--space-14) var(--space-18);border-radius:var(--radius-8);cursor:pointer;
  transition:transform .14s, opacity .14s;flex:none;letter-spacing:.1px}
.cta:active{transform:scale(.97)}
/* Text is pinned to navy-950 rather than var(--on-accent): the amber above is
   fixed across themes, but on-accent flips to white in the light theme, which
   used to leave this button at ~2:1 contrast there — unreadable, and never
   caught because the dark theme's on-accent happened to be dark navy already. */
.cta.wait{background:var(--cta-wait-grad);color:var(--navy-950)}
.cta:disabled{opacity:.4;cursor:not-allowed}

/* sheets */
.scrim{position:fixed;inset:0;z-index:50;background:var(--scrim);backdrop-filter:blur(3px);
  opacity:0;visibility:hidden;transition:background-color .18s, border-color .18s, box-shadow .18s, opacity .18s, transform .18s}
.scrim.on{opacity:1;visibility:visible}
.sheet{position:fixed;left:0;right:0;bottom:0;z-index:51;max-width:480px;margin:0 auto;
  background:var(--surface-solid);border:1px solid var(--border);border-bottom:0;
  border-radius:var(--radius-18) var(--radius-18) 0 0;padding:var(--space-8) var(--space-18) calc(var(--space-20) + env(safe-area-inset-bottom));
  transform:translateY(101%);transition:transform .32s cubic-bezier(.32,.72,0,1);
  box-shadow:var(--shadow-2)}
.sheet.on{transform:none}
.handle{width:var(--size-38);height:var(--size-4);border-radius:var(--radius-pill);background:var(--border-strong);margin:0 auto var(--space-16)}
.sheet h3{margin:0 0 var(--space-4);font-size:var(--fs-18);font-weight:var(--weight-660);letter-spacing:-.3px}
.sheet .lead{margin:0 0 var(--space-16);font-size:var(--fs-13);color:var(--text-dim)}
.kv{display:flex;flex-direction:column;gap:var(--space-1);background:var(--border);border-radius:var(--radius-11);
  overflow:hidden;margin-bottom:var(--space-14)}
.kv div{display:flex;justify-content:space-between;gap:var(--space-12);padding:var(--space-12) var(--space-14);
  background:var(--surface-solid);font-size:var(--fs-13)}
.kv div span{color:var(--text-dim)}
.kv div b{font-weight:var(--weight-620);text-align:right}
.field{margin-bottom:var(--space-14)}
.field label{display:block;font-size:var(--fs-11);color:var(--text-faint);margin-bottom:var(--space-7);
  letter-spacing:.5px;font-weight:var(--weight-700);text-transform:uppercase}
.field input{width:100%;padding:var(--space-13) var(--space-14);border-radius:var(--radius-8);border:1px solid var(--border-strong);
  background:var(--surface);color:var(--text);font:var(--weight-500) var(--fs-14)/var(--lh-1-2) var(--font);outline:none;transition:background-color .18s, border-color .18s, box-shadow .18s, color .18s, opacity .18s, transform .18s}
.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
/* The acceptance on the condition step: a real checkbox beside its words,
   because «я понимаю» has to be an act rather than a button press. */
.agree{display:flex;align-items:flex-start;gap:var(--space-9);margin:0 0 var(--space-14);
  font:var(--weight-500) var(--fs-13)/var(--lh-1-35) var(--font);color:var(--text)}
.agree input{margin-top:2px;flex:none}
.sheet-actions{display:flex;gap:var(--space-10)}
.sheet-actions .ghost{flex:1;padding:var(--space-14);font-size:var(--fs-13-5);text-align:center}
.sheet-actions .cta{flex:1.4}
.cta:disabled,.ghost:disabled{pointer-events:none}

.toast{position:fixed;left:50%;bottom:calc(26px + env(safe-area-inset-bottom));
  transform:translate(-50%,20px);z-index:60;padding:var(--space-12) var(--space-18);border-radius:var(--radius-11);
  background:var(--surface-solid);border:1px solid var(--border);box-shadow:var(--shadow-2);
  font:var(--weight-620) var(--fs-13)/var(--lh-1-35) var(--font);display:flex;align-items:center;gap:var(--space-9);
  opacity:0;visibility:hidden;transition:background-color .18s, border-color .18s, box-shadow .18s, opacity .18s, transform .18s;max-width:88vw;text-align:left}
.toast.on{opacity:1;visibility:visible;transform:translate(-50%,0)}
.toast .dot{width:var(--size-7);height:var(--size-7);border-radius:50%;background:var(--free);flex:none}
.toast.warn .dot{background:var(--busy)}
.toast.wait .dot{background:var(--wait)}

.offline{position:fixed;top:0;left:0;right:0;z-index:65;padding:var(--space-8) var(--space-14);text-align:center;
  background:var(--busy);color:var(--on-accent);font:var(--weight-640) var(--fs-12)/var(--lh-1-35) var(--font);
  transform:translateY(-100%);transition:transform .24s}
.offline.on{transform:none}

/* Motion is decoration here — every animation in this file is a fade, a pulse
   or a scale, and none of them carries information. Somebody who has asked
   their system not to animate gets the same interface without them. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;scroll-behavior:auto !important;
  }
}

/* ============================================================
   Opening animation.

   A white 1920×1080 title card that covers the page for three seconds while
   the app boots behind it. Painted white in both themes on purpose: the film
   itself is white, and a dark surround would frame it in a hard rectangle —
   the one thing worse than no animation is an animation with a border.

   `position: fixed` with a very high z-index so it sits over every shell; the
   app underneath is live the whole time, which is why the skip button can hand
   control over instantly rather than starting a load.
   ============================================================ */
.intro {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: grid;
  place-items: center;
  background: var(--intro-bg);
  /* Only the fade out is animated. The element is removed from the DOM when it
     ends, so there is nothing to fade back in and no state to get stuck in. */
  transition: opacity .45s ease;
}
.intro[hidden] { display: none; }

/* The reduced-motion panel. One mark, centred, at the size the film's own
   lockup occupies, so the two openings are the same shape and only one of them
   moves. Hidden by default: the overlay normally holds the film, and two things
   stacked in one grid cell would show through each other. */
.intro-mark { display: none; }
.intro.intro-still .intro-mark {
  display: block;
  width: clamp(96px, 34vw, 176px);
  height: auto;
  border-radius: 50%;
}
/* Nothing here hides the film for reduced motion: js/core.js removes that
   element outright, so a second selector for it would be dead CSS — and a
   stray one-line block naming it is exactly what a later reader mistakes for
   the video's own sizing rule further down. */
.intro.intro-out { opacity: 0; pointer-events: none; }

/* Half the size it first shipped at, and cropped by a hair rather than fitted.

   Two faults, one shape.

   Size: the film is 1920×1080 and fitting it to the viewport blew it up to the
   full width of the screen, so every edge showed its pixels. Held to 620px the
   source is being scaled *down*, which is the only way a raster asset stays
   sharp.

   The line: a thin artefact is baked into the last column of the film, and no
   amount of scaling hides it — it has to fall outside the frame. A square crop
   was the first attempt and was wrong: the animation resolves into a *wide*
   lockup, the seal on the left and the wordmark on the right, so cropping to a
   square cut the mark in half and threw the word away.

   So the box is only slightly narrower than the film — 1.7 against 16:9 — and
   `cover` trims about 2% from each side. That is enough to lose the artefact
   and far less than the empty margin the lockup already carries. */
.intro-video {
  /* One clamp instead of breakpoints, because this is the rare case where the
     size should follow the glass continuously rather than jump at a canon
     width.

     88vw, not the 72 it started at. The lockup is wide — a seal beside a
     wordmark — so on a 375px phone 72vw left it 270px across, and at that width
     the word is a grey smudge on a large empty white field. 88vw gives it 330px
     and the same presence a phone gives anything else worth looking at.

     The bounds are the two failure modes: below 280px the wordmark stops being
     readable at all, and above 620px a 1080-tall source is being enlarged and
     shows its pixels — which is the fault this whole rule exists to fix. */
  width: clamp(280px, 88vw, 620px);
  aspect-ratio: 1.7;
  object-fit: cover;
  background: var(--intro-bg);
}


/* Somebody who has asked their system for less motion has asked for exactly
   this. js/core.js checks the same query and never starts the film; the rule
   is here as well so that a cached page mid-play stops immediately. */
@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
}

/* The two controls under a name in the account panel. A row that wraps rather
   than a grid: there are two of them, sometimes one, and a grid for that is a
   grid that has to be told how many columns it has. */
.acc-avatar-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-top: var(--space-8);
}
