/* Manager dashboard — recovery inbox (C3.1) + insights (C3.2). Tokens and .pill/.card come from
   tokens.css + cards.css; this file adds only the layout, the dialog chrome, the button variants
   those two don't define, the "Daylight" palette override below, and the chart palette. */

/* ===========================================================================================
 * Palette — "Botanical dusk". A luxury-Himalayan-hospitality green, ivory-first.
 *
 * A token override rather than an edit to tokens.css, because tokens.css is also the guest chat
 * app's palette and is contrast-measured for it. chat.css and cards.css read the same custom
 * properties, so they pick this up for free.
 *
 * Two green roles carry the whole interactive language — see the note beside --brand/--accent
 * above for what each one is for. The ivory surface ramp below is Daylight's original, unchanged:
 * it was already a warm, contrast-measured cream rather than a grey, which is exactly the
 * "content stays ivory/cream" brief.
 *
 * Its status accents — green #2f7d5b, amber #c2571f, red #b23b3b — land on the `-line` tokens (the
 * chip border and status dot, where the accent is *seen as* the colour): as 13px text on their own
 * `-bg` they measure 4.30 / 4.00 / 5.00:1 and two miss 4.5. The `-ink` tokens are darker same-hue
 * steps so the chip label passes everywhere: 5.94 / 6.58 / 6.48:1. Left alone here — an amber
 * caution chip and a red danger chip stay legible statuses precisely by NOT being the brand hue.
 *
 * --on-brand is ivory rather than dark ink, the reverse of the gold build this replaces: the fill
 * is now dark enough (a forest emerald, not a bright gold) that light text reads better on it —
 * 5.5:1 vs. a dark-ink pairing that would sit under 3:1. .btn-brand still hovers *lighter*, since
 * lightening a dark fill can only raise the ivory ink's contrast further.
 *
 * Derived tokens below carry their own measurement inline. --info-* is left alone: nothing in this
 * brief covers it, and it is the one semantic that should stay cool and distinct from both greens.
 *
 * Measured, not eyeballed. Light theme, worst surface first (--ink/--ink-soft/--ink-faint are
 * Daylight's unchanged figures — 13.0 / 5.8 / 4.8 on --paper and better on every lighter surface):
 *   --brand-ink (dark emerald text) / --paper 8.3:1, better on every lighter surface — clears AA
 *   --accent as text (olive)        / --paper 5.4:1, likewise
 *   --on-brand (ivory) / --brand fill 5.6:1 · --header-ink / rail 12.7:1 · --header-ink-soft / rail 4.8:1
 * ======================================================================================== */

:root {
  color-scheme: light;

  /* ---------------------------------------------------------------------------------------
   * Palette — "Blue hour". A muted dusty-blue hospitality system, replacing the emerald/olive
   * "Botanical dusk" pair this file used to carry.
   *
   *   --brand   deep dusty blue — the interactive colour: buttons, links, focus rings, the
   *             selected time-range chip, badge washes. Desaturated on purpose (S≈38%): a
   *             saturated blue at this size is the single strongest "generic SaaS" signal there
   *             is, and the whole register here is a hotel operations desk.
   *   --accent  slate blue-green — the second voice: the sentiment trend line and the chart
   *             ramps. A distinct hue from --brand rather than a lighter step of it, so the two
   *             read as a considered pair; it is also the one place the palette keeps a trace of
   *             the green it came from, which stops the page going uniformly blue.
   *
   * The temperature contrast is the point of the whole scheme and is deliberate: a COOL navy
   * rail and cool blue inks, against WARM ivory surfaces and a warm golden-hour photograph. Blue
   * everywhere would flatten exactly the thing that makes the reference read as expensive.
   * --------------------------------------------------------------------------------------- */
  --brand: #3d6288;
  --brand-dark: #2c4a68;
  --brand-light: #7699b8;
  /* Brand-coloured *text*: a shade darker than the fill so it clears AA at 13px on ivory (the
     fill itself is a fill, not a text colour — see --on-brand below for text ON the fill). */
  --brand-ink: #2a4e70;
  /* Text/icons sitting ON the --brand fill. The fill is dark enough that ivory reads better than
     a dark ink would. Warm ivory rather than white — it is the surface colour of the whole page,
     and a pure white label on a blue chip is the cheapest-looking pairing available. */
  --on-brand: #f7f5ef;
  /* The fill's own wash, for badges and hover tints — tokens.css's --brand-wash/-strong are
     hardcoded to its own hue and don't move with this override, so they're redefined here from
     --brand directly. */
  --brand-wash: color-mix(in srgb, var(--brand) 10%, transparent);
  --brand-wash-strong: color-mix(in srgb, var(--brand) 18%, transparent);

  /* Accent — slate blue-green, a different family from the dusty blue brand so the two read as a
     pair rather than one hue twice. Deep enough that ivory text on it clears AA. */
  --accent: #3a6472;
  --accent-ink: #f2f6f6;

  /* -----------------------------------------------------------------------------------------
   * Champagne — the ONE non-green in the system, and deliberately a narrow one.
   *
   * This is not the previous build's saturated marigold (#dda31f), which was loud enough to be a
   * second brand colour and competed with every green on the page. This is a soft, chalky
   * champagne: low chroma, high lightness, the colour of brushed brass in a hotel lobby rather
   * than of a highlighter.
   *
   * It is allowed only on *interaction state* inside the rail, plus the property selector's
   * hover rim: the selected nav item, a nav hover, the workspace eyebrow, the "Acting as" line.
   * Nothing that carries a number, a status or a category may use it — those are all blue or the
   * semantic ok/warn/danger. That restriction is what keeps the champagne reading as a material
   * finish (brass on navy, the way a hotel lobby uses it) rather than as another semantic
   * competing with the data.
   *
   * It earns its keep harder in this palette than it did in the green one: against a cool navy
   * rail a warm brass is a genuine complementary accent, where against forest green it was
   * merely a second warm hue. It is also the one thing carrying warmth into the rail, which is
   * what stops the navy reading as corporate.
   *
   * ## Retuned to the cream-gold of the design reference (measured, not eyeballed)
   *
   * The previous values were a true brass — #d9c18d, i.e. a mid-lightness saturated gold. Sampled
   * off the reference's selected pill, the intended finish is a much LIGHTER and much less
   * saturated warm cream: #fddfc3 (L≈89%, S≈24%) against the old #d9c18d (L≈70%, S≈47%). That gap
   * is the whole complaint — a 70%-lightness gold on navy reads as a brass button, where an
   * 89%-lightness cream reads as lit ivory, which is what the reference's "warm champagne" is.
   *
   * So the ramp moves up in lightness and down in chroma, and its RANGE collapses: lit→deep spans
   * 5% lightness here against 14% before. That is deliberate — "avoid excessive gradients" — and
   * it leaves just enough of a lit top edge to keep the pill from looking like flat paint.
   *
   * Deep navy ink (--rail-active-ink) measures 13.2:1 on --champagne and 12.4:1 at --champagne-deep,
   * so the label holds right across the pill rather than only at its middle. The champagne itself
   * reads 12.4:1 against the rail, so it still works as rail text (the eyebrow, "Acting as").
   * Nothing here goes near yellow or orange: hue stays ~30° with chroma low enough that it reads as
   * cream first and gold second.
   * --------------------------------------------------------------------------------------- */
  --champagne: #f7e0c6;
  --champagne-lit: #fdecd8;
  --champagne-deep: #eed3b4;
  --champagne-wash: rgba(247, 224, 198, 0.13);
  --champagne-wash-strong: rgba(247, 224, 198, 0.22);

  /* ------------------------------------------------------------------------------------------
   * Star ink — every rating glyph on the staff surface, and the ratings-distribution bars.
   *
   * The champagne above CANNOT do this job and that is the whole reason this token exists. Those
   * four steps are FILLS at 89-93% lightness, tuned to sit under dark ink as a brass pill; drawn
   * as a 15px glyph on the cream canvas (--bg, 94% lightness) #f7e0c6 measures 1.1:1 and is
   * invisible. So the stars were --brand-ink, i.e. navy — legible, and the one figure on the page
   * a reader most expects to be gold.
   *
   * This is that hue at a lightness that survives the cream: brushed brass, the same family as the
   * champagne and not the marigold the old build used. Measured 4.80:1 on --bg, 5.28:1 on
   * --surface and 4.80:1 on --surface-2 — clears the 3:1 graphical-object floor with room, and
   * clears AA for text as well, which matters because the numeral beside the glyphs is inked from
   * the surrounding type and the glyphs must not read as the weaker element.
   *
   * Deliberately NOT --warn-ink (#8f3d10). That is the amber the status system pairs with
   * --warn-bg, it reads burnt-orange rather than gold at glyph size, and a rating is not a warning
   * — reusing it would put a 5-star review in the same colour as a caution.
   * --------------------------------------------------------------------------------------- */
  --star-ink: #8a6410;

  /* Ink for text on the champagne fill — the deepest navy in the palette, so the selected rail
     item reads as dark-navy-on-brass rather than as black-on-yellow. */
  --rail-active-ink: #0f1e30;

  /* -----------------------------------------------------------------------------------------
   * The surface ramp. Five steps, each ~2.5-3% off its neighbour, all in the ivory hue family —
   * and none of them pure white, because pure-white cards on a warm canvas is *the* thing that
   * makes a warm palette read as clinical: the warmth then survives only in the ~8% of pixels
   * that aren't a card.
   *
   * Re-measured after moving --surface off #ffffff, and nothing regressed past its floor: the
   * worst ink/surface pair is --ink-faint on --surface-3 at 4.51:1, and every chart mark lost
   * 0.1-0.2 (--viz-pos sits exactly on the 3.0 graphical-object floor). --viz-neutral (2.41) and
   * --funnel-1 (2.60) are under 3.0 but already were on white (2.49 / 2.69), and are exempt for
   * the reason the heat ramp documents: every one of those segments prints its own count.
   *
   *   --surface       the card face
   *   --surface-2     nested: table headers, zebra rows, chart plot backgrounds
   *   --surface-3     a recessed well: empty states, insets
   *   --surface-lift  the ONE brightest surface — a hovered card, an open sheet
   * --------------------------------------------------------------------------------------- */
  /* Brightness comes from RANGE, not lightness. "Too pale" measured as a card sitting at 1.27:1
     against the canvas it floats on — nothing read as an object, so the screen collapsed into one
     cream field. Cards sit on --bg (the tab panels carry no background of their own), so that is
     the ratio that matters: pushing --bg down and out (0.749 -> 0.660 luminance, 9.5% -> 17%
     saturation) and --surface up to near-white takes card pop to 1.46. Deepening the canvas costs
     --ink-faint contrast, so it darkens a step to pay for it — which also repairs a pre-existing
     failure, since faint ink on the old --bg measured 4.30:1 and never cleared AA. Now 4.75:1. */
  /* The canvas goes LIGHT — warm ivory, near-white — and that is a reversal of the note that used
     to live here, so it is worth saying why rather than quietly deleting it.
   *
     The old reasoning was that card "pop" must come from a measurable luminance step between
     --surface and --bg, and #dfdbd1 bought 1.36:1 of it. That holds only if the step is the ONLY
     thing separating a card from the canvas. It is not: every card here also carries --hairline and
     --shadow-2. Measured off the design reference, its cards sit at 1.02:1 against their page — i.e.
     effectively zero luminance step — and they still read unmistakably as objects, because the
     border and the layered shadow do that work. So the step is spendable, and spending it is what
     buys the light, airy, premium register the reference has and a 72%-luminance canvas cannot.
   *
     #f5f2eb is warm ivory: R>G>B, ~4% saturation, so it is a *warm* near-white and not the flat
     grey the brief rules out. Card pop against it is 1.10:1 — still slightly more separation than
     the reference's own 1.02:1, so cards keep a hair of luminance help on top of border+shadow.
     Every ink gets *more* contrast on it than before, not less (--ink-faint goes 4.75:1 -> 5.9:1),
     so nothing regressed: lightening a canvas can only help dark ink.
   *
     --paper, --surface-2 and --surface-3 are all deliberately UNCHANGED, even though two of them are
     now darker than the canvas. That sounds like an inverted ramp and is not: none of them is ever
     painted against --bg. Every use is inside a card — --paper is the fill behind `.tag` and
     `.field-input`, --surface-2 is table headers and zebra rows, --surface-3 is an inset well — so
     the surface each is measured against is --surface, and each still sits correctly below it. */
  --bg: #f5f2eb;
  --paper: #eeebe2;
  --surface: #fffdf8;
  --surface-2: #f4f2ec;
  --surface-3: #e7e5dc;
  /* Still not #ffffff — the ivory system has no pure white in it anywhere, even for one hover. */
  --surface-lift: #fffefc;
  --bubble-out: #e6ebee;
  --bubble-in: #fffdf8;
  /* Ink is deep navy-slate, matching the rail rather than the old forest. Every heading, number
     and label on the page inherits from these three, so this is most of what makes the palette
     read as one system rather than as blue chrome around unrelated text.
     Measured on the worst surface each is used on (--paper / --bg respectively):
     --ink 12.7:1 · --ink-soft 5.9:1 · --ink-faint 5.4:1 — all clear AA. */
  --ink: #16283c;
  --ink-soft: #46617c;
  --ink-faint: #465f78;
  --hairline: rgba(22, 40, 60, 0.11);
  --hairline-strong: rgba(22, 40, 60, 0.2);

  /* -----------------------------------------------------------------------------------------
   * The rail — deep navy, and it is the anchor of the whole scheme. "Permanent Manager
   * Workspace navigation rail": a dark panel beside an ivory canvas is what gives the app a
   * structural light/dark division to build everything else against.
   *
   * Navy rather than the forest green it replaces, and *lighter in visual weight* than that
   * green was: the gradient's range is narrower (a 9% lightness spread against the old 14%) and
   * its foot no longer goes almost to black, so the panel reads as one calm surface instead of a
   * heavy slab that got heavier towards the bottom. It is a blue-leaning navy with a trace of
   * green in it (H≈214, not a pure 220 blue), which keeps it from the flat corporate navy that
   * "premium dark sidebar" usually collapses into.
   *
   * Ivory ink on it is 11.9-14.6:1 across the gradient. The rail is dark in *both* themes, so the
   * header tokens need no per-theme flip — and the ghost tokens go to white translucency, because
   * a faint blue wash would be invisible against a rail this close to it in hue.
   *
   * --header-ink-soft moves from a sage to a cool blue-grey for the same reason as the inks.
   * --------------------------------------------------------------------------------------- */
  --header-bg: linear-gradient(168deg, #1a2d45 0%, #142438 52%, #101c2d 100%);
  --header-ink: #f2f2ee;
  --header-ink-soft: #9fb0c4;
  --header-line: rgba(255, 255, 255, 0.11);
  --header-rule: linear-gradient(90deg, var(--brand), var(--accent) 60%, var(--brand-light));
  --ghost-line: rgba(22, 40, 60, 0.2);

  --scroll-thumb: rgba(22, 40, 60, 0.28);
  --scroll-thumb-hover: rgba(22, 40, 60, 0.45);
  --scroll-track: rgba(22, 40, 60, 0.06);

  /* Status. `-line` is the spec accent, `-ink` the readable same-hue step — see the header. */
  --ok-bg: #e5f1ea;
  --ok-ink: #1f6647;
  --ok-line: #2f7d5b;
  --warn-bg: #fbf1da;
  --warn-ink: #8f3d10;
  --warn-line: #c2571f;
  --danger-bg: #fbe9e6;
  --danger-ink: #96302f;
  --danger-line: #b23b3b;

  /* Shadows are re-tinted to the new ink hue AND rebuilt as multi-layer — see the Elevation block
     below, which is where all three are actually defined. */

  --focus-ring: 0 0 0 3px rgba(61, 98, 136, 0.5);
  /* The colour the real (outline-based) focus ring is drawn in — see the :focus-visible rule for
     why this exists alongside --focus-ring rather than replacing it. --brand measures 4.4:1 on
     --surface and 3.2:1 on --bg, clearing 1.4.11's 3:1 floor for a focus indicator on every
     light surface on the page. The rail sets its own lighter value below, since --brand against
     a navy rail is nowhere near 3:1. */
  --focus-ring-color: var(--brand);

  /* -----------------------------------------------------------------------------------------
   * Elevation. Every shadow is still at least two layers — a tight contact shadow that seats the
   * edge plus a wide diffuse one that lifts it — but all three are roughly *halved* from the
   * previous build.
   *
   * The reason: with a dozen cards on one screen, a shadow that reads correctly on a single
   * isolated card becomes a grey haze once it is tiled. What looked like depth on one panel
   * looked like smudge across a grid. Editorial layouts separate panels with space and a
   * hairline and spend almost nothing on shadow; the lift here only needs to be enough that a
   * card is not *flush* with the canvas.
   *
   * Tinted, not grey: the shadow colour is the ink's own navy hue, so it sits *in* the ivory
   * rather than dirtying it. A neutral grey shadow on a warm ground is what makes warm palettes
   * look muddy — and a cool-tinted one under a warm card is also just what daylight does.
   * --------------------------------------------------------------------------------------- */
  --shadow-1: 0 1px 2px rgba(22, 40, 60, 0.05);
  --shadow-2:
    0 1px 2px rgba(22, 40, 60, 0.04),
    0 4px 12px -6px rgba(22, 40, 60, 0.07),
    0 16px 32px -20px rgba(22, 40, 60, 0.09);
  --shadow-3:
    0 2px 4px rgba(22, 40, 60, 0.06),
    0 12px 28px -10px rgba(22, 40, 60, 0.11),
    0 40px 72px -28px rgba(22, 40, 60, 0.2);

  /* A blue rim-light for the hovered/active card. On an ivory ground a lit *edge* sells depth
     far better than a darker shadow does — and unlike a shadow it costs nothing when tiled. */
  --glow-brand: 0 0 0 1px rgba(61, 98, 136, 0.3), 0 4px 16px -8px rgba(61, 98, 136, 0.22);

  /* Shape. One radius per role and no ad-hoc values anywhere downstream, which is what
     "consistent corner radii" actually requires.
   *
     14px, deliberately NOT the 18-22px that reads as current-generation dashboard styling: past
     about 16px a card stops looking like a sheet of paper and starts looking like a widget, and
     the whole register here is editorial. Scoped to this page the same way the palette is, since
     tokens.css's radii are also the guest chat's. */
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 20px;

  /* Shell metrics, as tokens because more than one element has to agree on them.
   *
   * --rail-w is the navigation rail's width; the sample-data footer bar starts at exactly this
   * offset so it spans the content column and never covers the navigation.
   *
   * --disclosure-h is that bar's height, and it is 0 by default: the bar only exists when the
   * server says the rows are seeded (page.js's paintSampleBanner), so on a real property nothing
   * reserves space for it. `body:has()` below flips it on, which is what lets .mgr-main reserve
   * exactly the right padding without any JS coordination. */
  --rail-w: 16.5rem;
  --disclosure-h: 0rem;

  /* -----------------------------------------------------------------------------------------
   * Type. --font-display is the editorial half of the pairing: a serif for titles and for the big
   * metric numbers, the existing sans for everything that is read rather than looked at.
   *
   * All-sans dashboards read as tooling; a warm ivory ground with a serif headline reads as a
   * printed report, which is the register this is demoed in. No new asset is fetched to get it —
   * the stack is entirely faces already on the target machines: Sitka Banner is Matthew Carter's
   * display cut and ships with Windows 8.1+ (verified present on the demo machine), Iowan Old
   * Style and Palatino cover macOS, Georgia is the near-universal floor, and Liberation/DejaVu
   * Serif substitute on Linux.
   * --------------------------------------------------------------------------------------- */
  --font-display:
    "Sitka Banner", "Sitka Text", "Iowan Old Style", Georgia, "Palatino Linotype", Palatino,
    Constantia, Cambria, serif;

  /* Scale contrast. The old ramp put --fs-h1 at 20-28px against 15-17.5px body — barely a
     doubling across the entire page, which is why nothing looked like a headline. The display
     sizes below are for the serif only and are deliberately much larger. */
  --fs-display: calc(clamp(30px, 2.6vw, 46px) * var(--type-scale));
  --fs-metric: calc(clamp(26px, 2.1vw, 38px) * var(--type-scale));

  /* Micro-label tracking, used by every uppercase key/eyebrow on the page. */
  --track-micro: 0.08em;

  /* -----------------------------------------------------------------------------------------
   * Frosted glass. The spec's card system, expressed as tokens so a card never hand-rolls a blur.
   * --glass-bg carries real alpha — the whole point is the hero showing through — and every glass
   * surface pairs it with a 1px top highlight (--glass-edge) so the pane has a lit upper edge
   * instead of looking like a grey wash.
   *
   * Cheap-looking glass is nearly always one of: too little blur (reads as flat translucency),
   * too much alpha (reads as opaque, so why blur), or no edge treatment. 18px/78% with a lit edge
   * is the combination that held up over the hero at projector distance.
   * --------------------------------------------------------------------------------------- */
  --glass-bg: rgba(255, 253, 248, 0.78);
  --glass-bg-strong: rgba(255, 253, 248, 0.9);
  --glass-blur: saturate(1.35) blur(18px);
  --glass-edge: rgba(255, 255, 255, 0.75);
  --glass-line: rgba(22, 40, 60, 0.1);

  /* Card face. Not flat #ffffff: a 1.5% warm vertical gradient gives the surface a direction of
     light, which is most of why a white card can look either printed or like an empty div. */
  --card-face: linear-gradient(178deg, #fffefb 0%, #fdf9f1 100%);

  /* -----------------------------------------------------------------------------------------
   * The Overview hero — img/hero-lodge.jpg, the supplied dusk photograph: a snow range under
   * alpenglow, a lit hill lodge on the right shoulder, dark conifer ridge across the foreground.
   * scripts/make-hero-lodge.ps1 prepares it; the file is used as given, no filter or recolour.
   *
   * ## Why this is a NEW pair and not an overwrite
   *
   * hero-mountain.jpg belongs to the guest welcome screen (static/index.html + css/glass.css) and
   * the manager login. hero-summit.jpg is this hero's PREVIOUS asset, whose crops were measured for
   * an ivory veil under dark type. Both are left alone: the Overview points at its own pair, so
   * reskinning this dashboard cannot silently change a surface nobody was looking at.
   *
   * ## The crop does the work — read scripts/make-hero-lodge.ps1 before touching anything here
   *
   * The source frame is 1742x903 (1.93:1) and its subject is spread down two thirds of the height:
   * sky to row ~215, summit tip ~240, range bottoming ~520, the lodge at rows ~375-700 on the right,
   * conifer ridge from ~560 down. A dashboard hero is a 3:1-ish band, so `cover` on the full frame
   * scales by WIDTH and shows only the top half — sky and peaks, with the lodge cut off below the
   * fold. background-position cannot recover it; the frame is simply the wrong shape.
   *
   * So the asset is a BAKED 1600x551 band (rows 120..720) — thin sky strip, whole range, the lodge
   * complete, top of the ridge — and the phone gets a second, squarer crop reframed on peak-plus-
   * lodge. Everything below is measured against those two framings.
   *
   * ## The composition — a DARK band, which is the inversion from the previous hero
   *
   *   top left     the greeting and subtitle, in ivory over a soft pool of shade (--hero-scrim).
   *   top right    the chrome pills, which stay ivory surfaces and need no help from the scrim.
   *   centre/right the range and the lodge, essentially untouched: the pool has fallen to zero by
   *                the time it reaches them, so the alpenglow and the lit windows stay the
   *                brightest things on the page.
   *   foot         the operational summary strip, on its own translucent dark surface over the
   *                conifer ridge. The ridge is the darkest region in the frame (mean luminance
   *                0.02-0.04), which is exactly why the strip was put there.
   *
   * ## Why a pool and not a veil, and why these numbers
   *
   * Measured on the band, over the left 32% where the type is capped (52ch), the brightest pixel per
   * tenth of the height runs 0.48 -> 0.70 down to the halfway line. Ivory type needs the composite
   * at or under 0.178 for 4.5:1, which takes 0.39-0.50 of scrim there. A full-width veil at that
   * strength would also cross the lit range, which peaks at 0.98 and is the reason the photograph is
   * in the page at all. An ellipse anchored at the top-left corner buys the same alpha where the
   * words are and has fallen under 0.1 before it reaches the summit.
   *
   * Verified by compositing this exact ellipse over the exact band at four real hero geometries
   * (1176x368, 1656x480, 1016x368, 736x368, each with `cover` and 50% 35%): worst case in the type
   * zone is 5.50:1, against the 4.5:1 floor. The old hero's ivory veil and foot dissolve are gone —
   * both existed to hand dark type a pale ground, and the type is no longer dark.
   * --------------------------------------------------------------------------------------- */
  --hero-photo: url("/img/hero-lodge.jpg");

  /* The scrim colour, as a channel triple so both layers below and both themes share one value.
     A desaturated slate rather than black: neutral black over apricot sky greys it toward brown,
     and this photograph's shadows are already blue. */
  --hero-scrim-rgb: 10, 18, 28;

  /* Two layers, composited in this order (topmost first in the `background` shorthand):
   *
     `--hero-scrim` — the type pool. An ellipse 100% of the hero wide and 130% tall, anchored at the
     top-left corner, so its major axis runs down-page and it covers the whole text stack (which is
     ~178px tall at the top of the box) without spreading sideways into the range. Clear by 84% of
     its radius; the summit's centre sits at ~0.59 of it, where the ramp is under 0.4, and the lodge
     is past 1.0 and untouched.
   *
     `--hero-foot` — what the summary strip sits on. Only 0.50 at the very bottom, because it is not
     the strip's own contrast (the strip carries its own surface, see .hero-strip): this exists so
     the strip's edge does not land on a hard tonal step, and so the hero's bottom edge reads as
     evening rather than as a cropped photograph. Clear by 46%, well below the range. */
  --hero-scrim: radial-gradient(
    100% 130% at 0% 0%,
    rgba(var(--hero-scrim-rgb), 0.76) 0%,
    rgba(var(--hero-scrim-rgb), 0.66) 30%,
    rgba(var(--hero-scrim-rgb), 0.5) 52%,
    rgba(var(--hero-scrim-rgb), 0.24) 68%,
    rgba(var(--hero-scrim-rgb), 0) 84%
  );
  --hero-foot: linear-gradient(
    to top,
    rgba(var(--hero-scrim-rgb), 0.5) 0%,
    rgba(var(--hero-scrim-rgb), 0.22) 26%,
    rgba(var(--hero-scrim-rgb), 0) 46%
  );

  /* Hero type. Ivory rather than white: #ffffff over a photograph reads as a UI label pasted on
     top, and this page's own light ink is already warm (--header-ink #f2f2ee, the rail's).
   *
     --hero-ink-soft is the subtitle and every micro-label in the strip. Measured against the type
     zone's worst composited pixel it lands at 4.72:1, clearing AA with the smallest margin of
     anything in the hero — which is why it is a named token and not an opacity on --hero-ink. An
     `opacity: 0.7` here would silently track the photograph instead of a measured floor. */
  --hero-ink: #f4f2ec;
  --hero-ink-soft: #cfd8e2;

  /* The summary strip's own surface. 0.62 alpha over the band's foot, which after --hero-foot runs
     no brighter than 0.68 luminance anywhere under the strip: composited that is 0.096, so ivory on
     it clears 7.2:1 and the micro-labels above clear 4.9:1. Blur, because unlike the old chrome this
     genuinely does sit on a photograph and there is something behind it to refract. */
  --hero-strip-bg: rgba(12, 20, 32, 0.62);
  --hero-strip-line: rgba(255, 255, 255, 0.16);
  --hero-strip-edge: rgba(255, 255, 255, 0.22);

  /* The hero's bottom edge. In light theme the ivory canvas below is 0.88 luminance against a foot
     of ~0.05, so the step alone draws the horizon and this only needs to keep it from looking
     ragged; in dark the canvas is near-black and the seam is doing all the work (see there). */
  --hero-seam: rgba(22, 40, 60, 0.16);

  /* Film grain, as a data URI so it costs no request. feTurbulence at a high baseFrequency is
     indistinguishable from photographic grain at 3-5% opacity, and it is what stops a large
     gradient area (the hero) from banding on a projector. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");

  /* -----------------------------------------------------------------------------------------
   * Chart palette (C3.2). Generated in OKLCH at measured WCAG targets against the real chart
   * surface — --surface, #ffffff here and #1e2723 in dark — then checked with the palette
   * validator, not eyeballed. Every ramp holds the brand hue (OKLCH H 173.5) so the figures read
   * as part of this app rather than as a charting library's defaults.
   *
   *   --heat-1..4    sequential wash behind a cell's own number. Monotone lightness with adjacent
   *                  ΔL 0.069-0.085 (floor 0.06) and a 1° hue spread, so magnitude reads from the
   *                  fill alone. Pale on purpose: because every cell prints its count, the fill
   *                  carries the gestalt and the *text* carries the value, so the governing check
   *                  is ink-on-fill (6.2-13.4:1 light, 5.6-15.0:1 dark) rather than the ordinal
   *                  ramp's 2:1 mark-vs-surface floor, which the palest step does not meet and
   *                  does not need to — nothing here is read as an unlabelled mark. A saturated
   *                  ramp was tried first and abandoned: no single ink clears 4.5:1 across all
   *                  four steps (bin 2 peaks at 3.92:1 either way), and 28 dark blocks dominated
   *                  a page whose whole palette is warm and light.
   *                  Bin 0 is NOT a step: an empty cell gets no fill, because "nothing tagged" is
   *                  not the palest amount of something.
   *   --funnel-1..3  ordinal, same hue, wider steps (2.69 / 4.31 / 7.02:1) so stage order reads.
   *   --viz-pos/neg  the diverging pair. Deliberately split in *lightness* as well as hue: the
   *                  app's ok/warn/danger tokens as adjacent fills collapse to ΔE 1.7 under
   *                  deuteranopia, and the obvious equal-lightness teal/red pair still only
   *                  reaches 5.4. Splitting lightness lifts the worst CVD pair to ΔE 14.6 light /
   *                  19.8 dark, with the normal-vision floor at 23.9 / 27.7.
   * --------------------------------------------------------------------------------------- */
  /* Rotated from the green family to the blue one, holding every structural property the notes
     above describe: monotone lightness across the heat ramp, widening ordinal steps in the
     funnel, and a diverging pair split in lightness as well as hue. Only the hue moved — the
     lightness ladder is step-for-step what it was, so every reading above still holds. */
  --heat-1: #dae7f2;
  --heat-2: #b6cee3;
  --heat-3: #8badc9;
  --heat-4: #6389a8;

  --funnel-1: #86a8c4;
  --funnel-2: #4d7ba1;
  --funnel-3: #2b5578;

  --viz-pos: #3f7fa8;
  --viz-neg: #c2452f;
  /* The donut's middle band. Hueless on purpose: it sits between the blue and the rust, and a grey
     cannot collapse into either under any colour-vision deficiency — which is exactly what the
     rejected ok/warn/danger triple did (ΔE 1.7 under deuteranopia). Warmed a touch from the old
     value so it does not read as a fourth, paler step of the now-blue ramp beside it. */
  --viz-neutral: #a8a49c;
  --viz-track: rgba(22, 40, 60, 0.08);

  /* --accent, not --brand. Two reasons, and the ramps above are the first: they all sit in the
     blue family, and --accent's slate blue-green is the one hue in the system that is adjacent to
     them without being one of their steps — a --brand line would read as another funnel stage.
     The second is 1.4.11: --accent measures 5.6:1 on --surface, well clear of the 3:1 floor for a
     graphical object. */
  --viz-line: var(--accent);
  --viz-grid-line: var(--hairline);
  --viz-zero-line: var(--hairline-strong);
}

:root[data-theme="dark"] {
  color-scheme: dark;

  /* Dark keeps the --brand fill unchanged — holding it steady across themes is what tokens.css
     does too, and the emerald is already dark enough to need no per-theme adjustment. Only the
     ramp and the text-weight tokens re-derive: a deep-ink brand-ink would vanish on a charcoal
     surface, so it lightens. */
  --brand-dark: #4d3148;
  --brand-light: #ef7f5f;
  --brand-ink: #fba787;

  --accent: #cd6243;
  --accent-ink: #1a1218;

  --focus-ring: 0 0 0 3px rgba(239, 127, 95, 0.5);
  /* Lighter in dark, where --brand is close to the charcoal surfaces it would ring against. */
  --focus-ring-color: var(--brand-light);

  /* The spec's dark --paper/--ink are already tokens.css's dark values, so the surface stack is
     inherited as-is. Only the status *accents* move, to hold the light theme's hues; the -bg/-ink
     pairs stay at their measured 7-8.4:1. */
  --ok-line: #3d8f68;
  --warn-line: #b4551f;
  --danger-line: #a44a46;

  /* The brass inverts to a lit champagne — the same hue family, at the lightness the charcoal
     surfaces need. 12.75:1 on --bg, 9.63:1 on --surface, 10.61:1 on --surface-2. */
  --star-ink: #f0cd7f;

  /* Elevation in dark can't rely on a darker shadow — there is nothing darker to go to. The
     contact layers stay, and the lift comes from the lit top edge on the card face instead. */
  --shadow-1:
    0 1px 2px rgba(0, 0, 0, 0.4),
    0 2px 6px -1px rgba(0, 0, 0, 0.35);
  --shadow-2:
    0 1px 2px rgba(0, 0, 0, 0.45),
    0 4px 10px -2px rgba(0, 0, 0, 0.4),
    0 12px 28px -8px rgba(0, 0, 0, 0.45);
  --shadow-3:
    0 2px 4px rgba(0, 0, 0, 0.5),
    0 12px 24px -6px rgba(0, 0, 0, 0.5),
    0 32px 64px -16px rgba(0, 0, 0, 0.6);

  --glow-brand: 0 0 0 1px rgba(239, 127, 95, 0.4), 0 6px 20px -6px rgba(239, 127, 95, 0.22);

  --glass-bg: rgba(48, 31, 43, 0.72);
  --glass-bg-strong: rgba(48, 31, 43, 0.88);
  --glass-edge: rgba(255, 255, 255, 0.1);
  --glass-line: rgba(255, 255, 255, 0.12);

  /* The two new rungs. tokens.css's dark block already sets --surface/--surface-2; the well goes
     *below* the paper here rather than above it, because in dark a recess is darker, not lighter. */
  --surface-3: #1d141a;
  --surface-lift: #432b3e;

  /* Dark mode had the same collapse the light theme did, for the same reason: the rail measured
     1.12:1 against the canvas and a card 1.19:1, so the whole screen was one near-black green field
     with no structure. Same fix — drop the canvas, lift the panels — which takes both to 1.36:1.
     --surface also rises so ink-faint keeps 4.95:1 on it. */
  --bg: #100b0f;
  --paper: #20161d;
  --surface: #352330;
  --surface-2: #2a1c26;

  /* The rail gains chroma as well as lightness in dark, same shape as the light gradient's own
     dark end so the two themes read as one rail lit differently rather than two panels. */
  --header-bg: linear-gradient(168deg, #40293b 0%, #32212e 52%, #261a23 100%);

  --card-face: linear-gradient(178deg, #3c2737 0%, #33222f 100%);

  /* The hero does NOT invert with the theme, and that is deliberate rather than an omission.
   *
     It was theme-sensitive when it was a light band: an ivory veil under navy type had to resolve to
     this theme's --bg or the hero stopped meeting the canvas. The band is now a dusk photograph under
     a slate scrim with ivory type — which is already the dark treatment. Restating the scrim darker
     here would only crush the alpenglow the picture exists for, and restating --hero-ink lighter
     would push it off the warm ivory the rail uses.
   *
     What does change is the seam at the hero's foot: --bg goes near-black, so the crisp bottom edge
     that reads as a horizon against ivory would read as no edge at all. --hero-seam draws it. */
  --hero-seam: rgba(255, 255, 255, 0.08);

  /* The anchor flips in dark: the faintest step sits just above the surface and the loudest is the
     lightest, so magnitude still reads as "more contrast" rather than inverting. */
  --heat-1: #231820;
  --heat-2: #4a2f45;
  --heat-3: #713e5b;
  --heat-4: #ab4651;

  --funnel-1: #77405d;
  --funnel-2: #c35a3a;
  --funnel-3: #f48e6d;

  --viz-pos: #f69575;
  --viz-neg: #b0594f;
  --viz-neutral: #83837c;
  --viz-track: rgba(255, 255, 255, 0.08);

  --viz-line: #f48d6d;
}

@media (prefers-color-scheme: dark) {
  /* JS-disabled fallback only, scoped so it can never fight an explicit choice — same pattern and
     same reasoning as tokens.css's duplicated dark block.
   *
   * This block is load-bearing for the palette above, not just for the charts: tokens.css's own
   * fallback selector is `:root:not([data-theme])`, which outweighs a plain `:root`, so without
   * these lines a JS-disabled page on a dark-OS machine would take tokens.css's pine/marigold
   * values back over Daylight's light block. */
  :root:not([data-theme]) {
    color-scheme: dark;

    --brand-dark: #4d3148;
    --brand-light: #ef7f5f;
    --brand-ink: #fba787;

    --accent: #cd6243;
    --accent-ink: #1a1218;

    --focus-ring: 0 0 0 3px rgba(239, 127, 95, 0.5);
  /* Lighter in dark, where --brand is close to the charcoal surfaces it would ring against. */
  --focus-ring-color: var(--brand-light);

    --ok-line: #3d8f68;
    --warn-line: #b4551f;
    --danger-line: #a44a46;

    /* Mirrors the [data-theme="dark"] block above — see the --star-ink note in :root. */
    --star-ink: #f0cd7f;

    --shadow-1:
      0 1px 2px rgba(0, 0, 0, 0.4),
      0 2px 6px -1px rgba(0, 0, 0, 0.35);
    --shadow-2:
      0 1px 2px rgba(0, 0, 0, 0.45),
      0 4px 10px -2px rgba(0, 0, 0, 0.4),
      0 12px 28px -8px rgba(0, 0, 0, 0.45);
    --shadow-3:
      0 2px 4px rgba(0, 0, 0, 0.5),
      0 12px 24px -6px rgba(0, 0, 0, 0.5),
      0 32px 64px -16px rgba(0, 0, 0, 0.6);

    --glow-brand: 0 0 0 1px rgba(239, 127, 95, 0.4), 0 6px 20px -6px rgba(239, 127, 95, 0.22);

    --glass-bg: rgba(48, 31, 43, 0.72);
    --glass-bg-strong: rgba(48, 31, 43, 0.88);
    --glass-edge: rgba(255, 255, 255, 0.1);
    --glass-line: rgba(255, 255, 255, 0.12);

    --surface-3: #1d141a;
    --surface-lift: #432b3e;

    --bg: #100b0f;
    --paper: #20161d;
    --surface: #352330;
    --surface-2: #2a1c26;

    --header-bg: linear-gradient(168deg, #40293b 0%, #32212e 52%, #261a23 100%);

    --card-face: linear-gradient(178deg, #3c2737 0%, #33222f 100%);

    /* The hero itself is theme-independent now; only the seam at its foot is not — see the note in
       the [data-theme="dark"] block above, which this mirrors. */
    --hero-seam: rgba(255, 255, 255, 0.08);

    --heat-1: #231820;
    --heat-2: #4a2f45;
    --heat-3: #713e5b;
    --heat-4: #ab4651;

    --funnel-1: #77405d;
    --funnel-2: #c35a3a;
    --funnel-3: #f48e6d;

    --viz-pos: #f69575;
    --viz-neg: #b0594f;
    --viz-neutral: #83837c;
    --viz-track: rgba(255, 255, 255, 0.08);

    --viz-line: #f48d6d;
  }
}

/* The `hidden` attribute must win over every layout rule in this file.
 *
 * This is load-bearing, not defensive: the UA stylesheet's `[hidden] { display: none }` is a plain
 * author-level-beatable rule, so `.mgr-body { display: grid }` and `.mgr-insights { display: flex }`
 * silently overrode it and **all three tab panels rendered at once** — every chart, table and
 * sparkline built on load, stacked down one scrolling page. It read as a slow, broken layout rather
 * than as a CSS specificity problem, which is exactly why it survived a headless render check that
 * only ever inspected one renderer's output at a time.
 */
[hidden] {
  display: none !important;
}

/* Borrowed from layout.css, which this page deliberately does not load (it owns the guest chat's
   100dvh shell and would fight this layout).
 *
 * The reset is not cosmetic here: without it every `width: 100%` box that also has padding and a
 * border overflows its container by the sum of both — the rail's nav items, `.case-btn`,
 * `.field-input`, and `.dlg-panel`'s `min(34rem, 100%)`. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* cards.css marks the routing trail with a --brand-light dot by default; retargeted to --accent
   here, which clears 1.4.11's 3:1 floor for a graphical object on --surface with room to spare
   (~6.5:1) and matches the trend line's own hue in the charts. */
.trail li::before {
  background: var(--accent);
}

/* Two free polish lines. `balance` stops a two-word orphan on the second line of a card heading —
   across a dozen cards that is a surprisingly large share of "looks messy". `pretty` does the same
   for the last line of body copy. Both degrade to normal wrapping where unsupported. */
h1,
h2,
h3,
.viz-h,
.view-title {
  text-wrap: balance;
}

p,
.viz-sub,
.view-sub,
.muted {
  text-wrap: pretty;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--sp-3);
  top: -3rem;
  z-index: 20;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  background: var(--brand);
  color: var(--on-brand);
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus {
  top: var(--sp-3);
}

/* The keyboard focus ring — and this rule was previously producing NOTHING.
 *
 * `--focus-ring` is a *box-shadow* value ("0 0 0 3px rgba(...)"), which is how tokens.css defines
 * it and how cards.css, layout.css and glass.css all consume it. This file fed it to `outline:`
 * instead. The outline shorthand takes width/style/colour, so that declaration was invalid and
 * dropped: measured, `outline-style` computed to `none` on every focusable element on the page.
 * There was no visible keyboard focus indicator anywhere in the manager dashboard — a 2.4.7
 * failure that is invisible in a mouse-driven review, which is exactly how it survived.
 *
 * Fixed as a real outline rather than by switching to box-shadow, because several focusable
 * elements here (the chrome buttons, the nav pill, the account panel) already carry meaningful
 * box-shadows that a ring would have to clobber. An outline also tracks `border-radius`
 * automatically and never affects layout.
 *
 * The shared --focus-ring token is deliberately left alone: the other three stylesheets use it
 * correctly, and re-typing it would break them. */
:where(button, a, textarea, [tabindex], summary):focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: 2px;
}

body {
  margin: 0;
  /* Ambient light rather than a flat fill — the "subtle premium layered surface", not a gradient for
     its own sake:
   *
       1. a cool blue settling down from under the hero, so the photograph's atmosphere carries a
          little way into the page instead of stopping at a line;
       2. a second, cooler blue answering from the bottom-right corner, which is what keeps the
          canvas from going flat across a 1920px screen.
   *
     Halved from 0.11/0.08 to 0.055/0.04, and the third layer (a warm ivory bloom that used to sit
     over them) is gone entirely. Both changes follow from --bg going near-white. Those alphas were
     set against a 72%-luminance canvas where they read as a whisper; over a 89%-luminance ivory the
     same blues have far more room to show and started reading as an actual blue-violet cast across
     the page — the wash the brief explicitly rules out. And the ivory bloom existed only to stop
     layers 1 and 2 greying out a mid-tone ground; the ground is now ivory itself, so the bloom was
     correcting a problem that no longer exists.
   *
     `fixed` so the light belongs to the room rather than to the scroll position, and on `body`
     rather than `.mgr` so it still covers the viewport when the grid is shorter than the page. */
  background:
    radial-gradient(100% 62% at 14% -8%, rgba(61, 98, 136, 0.055), transparent 58%),
    radial-gradient(90% 70% at 102% 108%, rgba(58, 100, 114, 0.04), transparent 60%),
    var(--bg);
  background-attachment: fixed;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

/* ===========================================================================================
 * Shell — a sticky rail beside a scrolling column. One grid: the rail spans all three rows of
 * the second column (top bar, warning strip, views). Sticky rather than fixed, so it stays in
 * the grid and cannot overlap the column at any width.
 * ======================================================================================== */

.mgr {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  /* TWO rows, not three. Only .mgr-top and .mgr-main auto-place here — .mgr-side spans `1 / -1` and
     claims no row of its own — so a third track was an empty row, and being `1fr` it swallowed every
     spare pixel: measured 843px of blank canvas below the content at 1600x900. That is where the
     dead space under the page came from. `1fr` still has an `auto` minimum, so a long issue list
     grows the row and scrolls the page exactly as before. */
  grid-template-rows: auto 1fr;
  min-height: 100dvh;
}

/* Paper tooth over the whole canvas. Two jobs beyond taste: it stops the wide gradients above
   banding into visible steps (a projector's dithering makes 8-bit banding worse than a monitor's),
   and it gives the ivory a printed texture. Kept at 4% — past ~5% it reads as dirt, not paper.
   `multiply` on light so the grain darkens; `screen` in dark, where it has to lighten to show. */
.mgr::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: var(--grain);
  background-size: 140px 140px;
  opacity: 0.04;
  mix-blend-mode: multiply;
}

:root[data-theme="dark"] .mgr::before {
  opacity: 0.07;
  mix-blend-mode: screen;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .mgr::before {
    opacity: 0.07;
    mix-blend-mode: screen;
  }
}

/* One botanical mark on the content canvas — the same brand/botanical-sprig.svg the rail uses,
   masked so the colour is set here and only the drawing comes from the file.
 *
 * Bottom-right, mirrored (`scaleX(-1)`) so it grows inward from the corner rather than mirroring
 * the rail's sprig, and very large — 30rem — because a big shape at 3.5% opacity reads as an
 * atmospheric variation in the paper, while a small one at the same opacity reads as a smudge
 * someone forgot to delete. That is the whole trick to "almost subconscious": scale it up and
 * fade it down, never the reverse.
 *
 * `fixed`, so it belongs to the room rather than to the scroll position — the same reasoning as
 * body's ambient gradients. z-index 0 puts it under .mgr-main (which is z-index 1). */
.mgr::after {
  content: "";
  position: fixed;
  right: -4rem;
  bottom: -6rem;
  z-index: 0;
  width: 30rem;
  height: 40rem;
  pointer-events: none;
  background: var(--brand);
  opacity: 0.035;
  transform: scaleX(-1);
  mask: url("/brand/botanical-sprig.svg") no-repeat center / contain;
  -webkit-mask: url("/brand/botanical-sprig.svg") no-repeat center / contain;
}

:root[data-theme="dark"] .mgr::after {
  background: var(--brand-light);
  opacity: 0.05;
}

/* Under 60rem the content column is the full width of the screen, so a 30rem sprig sits directly
   behind the cards instead of in the margin beside them. */
@media (max-width: 60rem) {
  .mgr::after {
    display: none;
  }
}

/* --- the rail ----------------------------------------------------------------------------- */

.mgr-side {
  grid-row: 1 / -1;
  position: sticky;
  top: 0;
  align-self: start;
  /* Full viewport height, so the rule on its right reaches the bottom on a short page. */
  height: 100dvh;
  /* 16.5rem, up from 15.5. The extra rem is spent entirely on the nav's own breathing room —
     at the old width the longest labels ("Conversations", "Integrations") sat about 8px from
     the badge column, which is what made the rail feel like a dense admin list. Tokenised
     because the sample-data footer bar has to start at exactly this offset. */
  width: var(--rail-w);
  display: flex;
  flex-direction: column;
  /* A small base gap, with the larger separations added as margins on the elements that want
     them (.side-eyebrow's padded rule, .side-group-h's top margin). A single large flex gap
     cannot do this: it would space a section heading from its own list exactly as far as it
     spaces two unrelated blocks, which is the thing that makes a rail read as an undifferentiated
     column of rows. */
  gap: var(--sp-2);
  padding: var(--sp-5) var(--sp-3) var(--sp-4);
  background: var(--header-bg);
  color: var(--header-ink);
  border-right: 1px solid rgba(0, 0, 0, 0.2);
  /* The rail is a raised edge, not a painted stripe: an inset highlight down its inner right side
     plus a soft cast onto the content behind it. Without this the rail and the hero met as two flat
     fills with a 1px line between them. */
  overflow-y: auto;
  /* `stable` so the nav does not shift sideways by the scrollbar's width when a viewport is short
     enough to overflow — the rail is the one element on the page present in every state, and a
     2px horizontal jump in it is more noticeable than anywhere else. */
  scrollbar-gutter: stable;
  box-shadow:
    inset -1px 0 0 rgba(255, 255, 255, 0.06),
    4px 0 24px -14px rgba(8, 16, 26, 0.5);
  /* Above .mgr-top's 3, so the cast shadow falls on the hero rather than disappearing under it. The
     rule already establishes a stacking context via `sticky`. */
  z-index: 4;
}

/* Rail-scoped ghost tokens. The global ones are a 7%/15% green wash — right on ivory, invisible
   on a rail this close to it in hue, which would have left every nav hover and the theme toggle
   with no visible state at all. White translucency is the dark-surface equivalent. */
.mgr-side {
  --ghost-bg: rgba(255, 255, 255, 0.07);
  --ghost-bg-hover: rgba(255, 255, 255, 0.14);
  --ghost-line: rgba(255, 255, 255, 0.22);
  /* --brand is a mid-dark blue and measures under 1.5:1 against this navy — a focus ring in it
     would be invisible on precisely the surface a keyboard user traverses first. The champagne is
     the rail's own accent and reads ~8:1 here. */
  --focus-ring-color: var(--champagne);
}

/* Same grain as the hero, at half strength. It is what makes the rail and the hero read as one
   surface lit from one direction instead of two unrelated panels. */
.mgr-side::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--grain);
  background-size: 140px 140px;
  mix-blend-mode: multiply;
  opacity: 0.05;
  pointer-events: none;
}

/* A single fine-line botanical sprig, low in the rail behind the account panel — reusing the
   welcome card's own asset (brand/botanical-sprig.svg) as a CSS mask, exactly the technique its
   own file comment describes, rather than drawing a second one. `z-index: -1` on a stacking
   context .mgr-side already establishes (via `position: sticky` + its own z-index) is what keeps
   this behind the nav and the account card instead of painted over them. Restrained on purpose:
   one mark, low opacity, in the one corner nothing else is reading. */
.mgr-side::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: -1.75rem;
  bottom: -0.5rem;
  width: 9rem;
  height: 12rem;
  background: var(--brand-light);
  opacity: 0.1;
  mask: url("/brand/botanical-sprig.svg") no-repeat center / contain;
  -webkit-mask: url("/brand/botanical-sprig.svg") no-repeat center / contain;
  pointer-events: none;
}

/* The lockup. `center` rather than `flex-start`: the mark is a 34px square badge and the words
   beside it are two short lines, so top-aligning them left the mark visually high. */
.side-brand {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  padding: 0 var(--sp-2);
}

/* The mark gets the same soft rim the ivory cards downstream get, so it sits *in* the rail rather
   than on it — a bare SVG badge on a dark panel reads as a pasted asset. */
.side-mark {
  flex: none;
  width: calc(36px * var(--type-scale));
  height: calc(36px * var(--type-scale));
  border-radius: 10px;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.1),
    0 2px 8px -2px rgba(8, 16, 26, 0.5);
}

.side-words {
  min-width: 0;
}

/* The one serif moment in the rail — the editorial half of the pairing, same as .view-title, so
   the wordmark reads as the same hand as the hero's h1 rather than a sans logotype bolted onto a
   serif page. */
.side-name {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(var(--fs-h2) * 0.95);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.005em;
}

.side-script {
  margin: 0.05rem 0 0;
  font-family: var(--font-indic);
  font-size: var(--fs-micro);
  line-height: 1.4;
  color: var(--header-ink-soft);
}

/* The tagline is dropped from the lockup at desktop. Three lines of text beside a 36px mark is a
   paragraph, not a wordmark, and "The guest is divine." is already the guest app's line — in a
   staff workspace it was the one piece of copy on the rail that wasn't wayfinding. It stays in
   the DOM (and in the drawer, below) rather than being deleted: it is real brand copy, and the
   narrow layout has the vertical room the desktop lockup doesn't. */
.side-tagline {
  margin: 0.1rem 0 0;
  font-size: var(--fs-micro);
  font-style: italic;
  color: var(--header-ink-soft);
}

@media (min-width: 60rem) {
  .side-tagline {
    display: none;
  }
}

/* Names the rail as a whole, the way .side-group-h names a section of it — "a permanent Manager
   Workspace navigation rail" as a felt identity, not a fabricated account field. Champagne, not
   green: it is the rail's own title, and the one place the accent may state identity rather than
   interaction. Sits under the lockup, separated by a hairline so it reads as a caption to the
   whole rail rather than as the first section header. */
.side-eyebrow {
  margin: 0;
  padding: var(--sp-3) var(--sp-2) 0;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--champagne);
}

/* --- rail navigation ---------------------------------------------------------------------- */

/* Thirteen sections do not fit a laptop viewport, so the nav is the part that scrolls and the
   lockup above and the switch below stay pinned. `min-height: 0` is what lets a flex child shrink
   below its content and actually scroll. */
/* One scroller for the whole rail, not one per nav group.
 *
 * Each group used to be its own `overflow-y: auto` box, which on a short viewport produced *two*
 * scrollbars stacked inside a 15.5rem rail — and because this page deliberately does not load
 * layout.css, they were unstyled OS scrollbars: chunky grey Windows widgets on a deep teal panel.
 * The rail scrolls as a single column instead (see .mgr-side), and the bar is styled below. */
/* `flex: none` on BOTH groups, and it is load-bearing.
 *
 * With the inner `overflow-y: auto` gone, these boxes clip nothing — so leaving them shrinkable
 * (the flex default is `0 1 auto`) let the rail squash the secondary group below its content height
 * while the content itself spilled out visibly and landed on top of .side-foot: "Dark mode" printed
 * over "Team". Fixed size, and the rail's own scroll handles the overflow. */
.side-nav {
  display: flex;
  flex-direction: column;
  flex: none;
  gap: 1px;
}

/* The primary group takes the slack; the two-item secondary group keeps its natural height.
   Matched on the heading it is labelled by — `:first-of-type` counted every div in the rail, so
   the lockup was the "first" one and this never applied. */
/* Keeps the theme switch and Log out at the foot of a rail whose content is shorter than the
   viewport, now that no nav group absorbs the slack. Resolves to 0 when the rail overflows, so it
   cannot push the footer out of a scrolled rail. */
.side-foot {
  margin-top: auto;
}

/* The rail's scrollbar, made a designed element rather than an OS one.
 *
 * `scrollbar-color` (Firefox) and `::-webkit-scrollbar` (Chromium/WebKit) — deliberately NOT
 * `scrollbar-width`, because per layout.css's note setting that to anything but `auto` makes the
 * engine discard every `::-webkit-scrollbar` rule on the element, silently losing the custom bar. */
/* Quieter than before (0.28 -> 0.16, and 10px -> 8px). The rail overflows only on short
   viewports; when it does, a bright bar down the edge of the one permanently-visible surface is
   the loudest thing on the screen. It brightens on hover, so it is still findable when wanted. */
.mgr-side {
  scrollbar-color: rgba(244, 241, 232, 0.16) transparent;
}
.mgr-side::-webkit-scrollbar {
  width: 8px;
}
.mgr-side::-webkit-scrollbar-track {
  background: transparent;
}
.mgr-side::-webkit-scrollbar-thumb {
  border-radius: var(--radius-pill);
  background: rgba(244, 241, 232, 0.14);
  /* A transparent border is how you inset a webkit thumb — there is no padding on the pseudo. */
  border: 2px solid transparent;
  background-clip: content-box;
}
.mgr-side:hover::-webkit-scrollbar-thumb {
  background: rgba(244, 241, 232, 0.3);
  background-clip: content-box;
}

/* Section headings sit one weight and one step of tracking away from the nav labels under them —
   quieter than the rows they head, which is correct: they are wayfinding for the eye scanning
   past, not content. The margin is what separates "a heading and its list" from "two blocks". */
/* 0.62 alpha, not the 0.42 that looked right by eye: at 11px uppercase this is small text and
   0.42 measures 3.45:1 on the rail — under AA. 0.62 takes it to 5.9:1 and is still visibly a step
   quieter than the nav labels it heads. */
.side-group-h {
  margin-top: var(--sp-2);
  padding: 0 var(--sp-3);
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: rgba(255, 255, 255, 0.62);
}

.side-rule {
  height: 1px;
  margin: var(--sp-2) var(--sp-3) 0;
  background: rgba(255, 255, 255, 0.08);
}

/* --- nav item: default, hover, active ------------------------------------------------------
 *
 * Three states that have to be *obviously* three states without any of them shouting. The whole
 * budget is: a wash, a left marker, and one champagne pill.
 *
 * `position: relative` anchors the hover marker below. The transition list is explicit rather
 * than `all` so a repaint of the champagne gradient never animates. */
.nav-item {
  position: relative;
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  width: 100%;
  padding: 0.55rem var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--header-ink);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0.005em;
  text-align: left;
  cursor: pointer;
  transition:
    background var(--dur) var(--ease),
    color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

/* Hover: a champagne wash at 12% and the label brightening to full ivory — plus a 2px champagne
   marker growing in at the left edge, which is what makes the row read as *reachable* rather than
   merely tinted. Deliberately not a border or a glow: at thirteen rows, anything with an outline
   turns the rail into a list of buttons. */
.nav-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 2px;
  height: 0;
  border-radius: 0 2px 2px 0;
  background: var(--champagne);
  transform: translateY(-50%);
  transition: height var(--dur) var(--ease);
}

.nav-item:hover:not([aria-selected="true"]) {
  background: var(--champagne-wash);
  color: #fffdf6;
}

.nav-item:hover:not([aria-selected="true"])::before {
  height: 55%;
}

/* Active: the champagne pill. Deep forest ink on soft brass — 8.8:1 at the light stop, 7.2:1 at
 * the dark one, so the label holds across the whole gradient rather than only at its middle.
 *
 * "Slight elevation, very subtle border/highlight": a lit top edge at 40% white, a 1px contact
 * shadow, and a *small* warm bloom — 12px of spread, where the previous build used 18px at 0.6
 * alpha and read as a glowing button. The pill should look like a brass tab set into the rail,
 * catching the same light the ivory cards downstream do. */
.nav-item[aria-selected="true"] {
  background: linear-gradient(
    176deg,
    var(--champagne-lit) 0%,
    var(--champagne) 55%,
    var(--champagne-deep) 100%
  );
  color: var(--rail-active-ink);
  border-color: rgba(255, 255, 255, 0.24);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    0 1px 2px rgba(8, 16, 26, 0.28),
    0 4px 12px -6px rgba(217, 193, 141, 0.45);
}

/* The icon carries the same three states as the label, one step quieter each time. */
.nav-item .icon {
  opacity: 0.7;
  transition: opacity var(--dur) var(--ease);
}
.nav-item:hover:not([aria-selected="true"]) .icon {
  opacity: 0.95;
}
.nav-item[aria-selected="true"] .icon {
  opacity: 1;
}

.nav-label {
  flex: 1;
  min-width: 0;
}

/* A queue length, not a feature — so it is a count, and it should read as one from across the
 * room without becoming the loudest thing on the rail.
 *
 * A muted brick fill with light ink, rather than the pale-amber chip it was. On a navy rail that
 * amber chip was the single brightest object on the whole panel: a 90%-lightness block beside a
 * 20%-lightness surface, which put the visual emphasis on "there are three of something" rather
 * than on where you are. Brick is the same semantic (a queue you are behind on) at a lightness
 * that belongs to the rail — and it is the only warm-red in the navigation, so it still catches
 * the eye immediately.
 *
 * Sized to a circle at one digit (min-width == the line box) and stretching to a pill at two,
 * which is what "small, elegant notification badges" needs geometrically.
 *
 * The fill and the ring are a measured pair, and they are pulling in opposite directions: a
 * brighter brick separates better from the navy but leaves less room for the numeral on top of
 * it. #b05a4e is the brightest fill that still clears AA for 10px text against white (4.75:1).
 * That leaves the fill itself at 2.9-3.6:1 against the rail's gradient, so the 25%-white ring
 * carries the object's *boundary* instead — an edge at a contrast the fill alone cannot reach,
 * which is the standard way out of exactly this squeeze. */
.nav-badge {
  flex: none;
  display: grid;
  place-items: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.3rem;
  border-radius: var(--radius-pill);
  background: #b05a4e;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  text-align: center;
}
/* On the champagne pill the badge inverts — a deep navy chip with a champagne numeral, the same
   measured pair as the parent read the other way round. The brick it uses elsewhere would be a
   second warm hue sitting inside the first, which is exactly the mud this palette avoids. */
.nav-item[aria-selected="true"] .nav-badge {
  background: var(--rail-active-ink);
  box-shadow: none;
  color: var(--champagne-lit);
}

/* --- rail footer: the theme switch, then the account panel -------------------------------- */

.side-foot {
  flex: none;
  margin-top: var(--sp-5);
  padding-top: var(--sp-3);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* --- the account panel ---------------------------------------------------------------------
 *
 * "A premium account panel, integrated into the sidebar" — one bordered card, not a chip pasted
 * onto the footer, holding the avatar/name/role and the one action tied to them (Sign out) as a
 * single unit. Rail tokens throughout (--ghost-bg/--header-ink), not the ivory --surface-2/--ink
 * the identity chip used when it lived in the hero chrome — that pairing would vanish against
 * the dark rail this now lives on.
 *
 * "Acting as" doubles as the signed-in indicator: with no staff directory behind this build (see
 * the avatar note below), it is the one true, already-real statement this page can make about
 * who is at the keyboard, rather than an invented "online" dot. */
.side-account {
  display: flex;
  flex-direction: column;
  padding: var(--sp-3);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--radius);
  /* A lifted panel, not a flat tint: a slightly lighter forest fill than the rail behind it plus
     a lit top edge, which is the same recipe the ivory cards use — so "elevated surface" means
     one thing across the whole product rather than two. */
  background: linear-gradient(178deg, rgba(255, 255, 255, 0.09) 0%, rgba(255, 255, 255, 0.05) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    0 2px 10px -6px rgba(8, 16, 26, 0.7);
}

.side-account-id {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  min-width: 0;
}

/* An initial, not a photograph: there is no staff directory behind this page until C3.4's SSO.
   Solid --brand, not a gradient toward --brand-light: --on-brand only clears AA at the fill's
   darker end (5.6:1 solid vs. ~3.2:1 at --brand-light), so a gradient here would fade the initial
   out at its own lighter edge. */
.side-account-avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: calc(32px * var(--type-scale));
  height: calc(32px * var(--type-scale));
  border-radius: 50%;
  background: var(--brand);
  color: var(--on-brand);
  font-size: var(--fs-small);
  font-weight: 700;
  text-transform: uppercase;
}

.side-account-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

/* The signed-in line. Champagne rather than the muted ivory the role below it uses, so the panel
   has one lit detail — the same accent the selected nav item carries, which is what ties the
   account card to the rail above it instead of leaving it as a grey box at the bottom. */
.side-account-k {
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--champagne);
}

.side-account-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 0.1rem;
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: -0.005em;
}

/* 0.68, for the same reason .side-group-h is 0.62: 0.5 measured 3.75:1 against the account
   panel's own (already lightened) fill, which fails AA at this size. */
.side-account-role {
  font-size: var(--fs-micro);
  color: rgba(255, 255, 255, 0.68);
}

/* Grouped under the identity row by a hairline rather than its own card, so Sign out reads as
   this account's action rather than a second, unrelated control. Inset to the panel's own edges
   (the negative margins) so its hover fill spans the full card width instead of floating as a
   narrower rectangle inside it. */
.side-account .side-logout {
  width: auto;
  margin: var(--sp-3) calc(var(--sp-3) * -1) calc(var(--sp-3) * -1);
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
}

.side-logout {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: rgba(255, 255, 255, 0.62);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  transition:
    background var(--dur) var(--ease),
    color var(--dur) var(--ease);
}
/* A wash and a warmer ink, not the full --danger-bg slab: that token is an ivory-surface fill and
   painting it inside a dark rail put a bright pink rectangle at the bottom of the workspace. Sign
   out is a destructive-ish action, so it earns the danger *hue* — at rail strength. */
.side-logout:hover {
  background: rgba(196, 92, 82, 0.18);
  color: #f4b7ae;
}

.side-theme {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--header-ink-soft);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 500;
  cursor: pointer;
  transition:
    background var(--dur) var(--ease),
    color var(--dur) var(--ease);
}
.side-theme:hover {
  background: var(--champagne-wash);
  color: var(--header-ink);
}

.side-theme-label {
  flex: 1;
  text-align: left;
}

/* Drawn rather than iconified: the knob's position states the theme, where a sun/moon icon leaves
   the reader guessing whether it shows the current state or the one it switches to. */
.side-theme-track {
  flex: none;
  position: relative;
  width: 2.2rem;
  height: 1.2rem;
  border-radius: var(--radius-pill);
  background: var(--ghost-bg-hover);
  border: 1px solid var(--ghost-line);
}

.side-theme-knob {
  position: absolute;
  top: 1px;
  left: 1px;
  width: calc(1.2rem - 4px);
  height: calc(1.2rem - 4px);
  border-radius: 50%;
  background: var(--header-ink);
}

/* Champagne when on, matching the selected nav pill: within the rail, "lit" is one colour. */
.side-theme[aria-pressed="true"] .side-theme-track {
  background: var(--champagne);
  border-color: var(--champagne);
}
.side-theme[aria-pressed="true"] .side-theme-knob {
  left: calc(2.2rem - 1.2rem + 1px);
  background: var(--rail-active-ink);
}

/* --- top bar ------------------------------------------------------------------------------ */

/* The top bar. This is shared markup — one header, repainted per view by page.js — so its base
 * state here is the plain, compact bar every non-Overview section uses: no photo, no extra
 * height, just the title/subtitle and chrome sitting on the page's own ambient body background
 * (see `body`'s radial gradients). That ambient wash IS this page's "normal premium background";
 * the bar itself stays transparent so it shows through rather than competing with it.
 *
 * The mountain hero below (`.mgr-top--overview`) is additive on top of this, gated by page.js
 * adding that class only while Overview is the active view — see `paintViewSelection`. Nothing
 * here should assume the photo is present, because for every other section it is not.
 *
 * `isolation: isolate` keeps the hero's grain blend mode (added by the modifier) off the rest of
 * the page even when it is active. NOT `overflow: hidden` — the property popup absolutely
 * positioned inside this header needs to hang below it uncropped (see .prop-switch/.prop-list). */
.mgr-top {
  position: relative;
  isolation: isolate;
  /* z-index 3 rather than auto, for that same property popup: `.mgr-main` sets `z-index: 1` (to
     sit above the fixed grain layer), so with this header at `auto` the stat tiles painted
     straight over the popup. 3 clears main; the rail goes to 4 so its cast shadow still falls on
     the hero rather than under it. */
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  align-items: flex-end;
  justify-content: space-between;
  padding: var(--sp-6) var(--sp-6) var(--sp-5);
  border-bottom: 1px solid var(--hairline);
  color: var(--ink);
}

/* The Overview-only lodge hero. See --hero-photo's note for the composition and for where every
 * number below was measured — the short version is that the type sits top-left in a pool of shade,
 * the range and the lodge are left alone, and the operational summary strip sits on the dark
 * conifer ridge across the foot.
 *
 * Layer order (topmost first): grain, the type pool, the foot ramp, the photograph.
 *
 * ## Why the strip is INSIDE this header and not in the panel below it
 *
 * The requirement is that the summary strip overlap the photograph with no seam between them. The
 * panel below is `.mgr-main`, a separate grid row at `z-index: 1` while this header is at 3 (see
 * .mgr-top: the property popup has to hang over the content). Pulling the strip up out of the panel
 * with a negative margin would put it UNDER the photograph, and raising it above 3 to fix that would
 * put it over the property popup instead — trading a layout bug for an interaction bug. Rendering it
 * as the hero's own last row needs neither: it is in the picture because it is in the box.
 *
 * ## Geometry
 *
 * A three-row grid: greeting and chrome share the top row, row two is elastic space that gives the
 * range its own air, and the strip spans the foot. `1fr` on the middle row rather than `space-between`
 * on a flex column, because the strip has to stay full-bleed across both columns.
 *
 * `min-height` is what buys the photograph its room; without it the box is only as tall as its
 * contents, which is the thin-banner shape this exists to avoid. 42vh lands ~368px at a laptop
 * height and ~450px at 1080p. That is taller than the ivory hero it replaces (38vh) and the strip is
 * the whole reason: ~122px of the box is now strip plus its inset, so at the old height the
 * photograph would have had less room than before rather than more. The ceiling still stops a tall
 * monitor from turning it into a splash screen — the first card row must break the fold.
 *
 * `background-position: 50% 35%`.
 *
 * The band is 2.90:1 and the hero is wider than that at every desktop width, so `cover` scales by
 * WIDTH, offX is zero, and an x% of the hero is always the same x% of the photograph — which is what
 * lets the pool's percentages hold at every width. Vertically `cover` overflows by 10-15%; 35% keeps
 * the sky strip above the summit and trims a little more ridge than sky, which is the cheapest part
 * of this frame to lose because the strip is sitting on it anyway.
 *
 * X matters on a TABLET, where the hero is narrower than 2.90:1 and `cover` crops horizontally
 * instead. 50% holds the main range; the lodge on the right shoulder is what starts to go, and the
 * ≤62rem block below reframes rather than letting it be sliced. */
.mgr-top.mgr-top--overview {
  min-height: clamp(23rem, 42vh, 30rem);
  padding: var(--sp-7) var(--sp-6) var(--sp-6);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto 1fr auto;
  column-gap: var(--sp-5);
  row-gap: var(--sp-5);
  /* `start`, against the plain bar's `flex-end`. The chrome has to sit level with the greeting at the
     top of the row, not fall to the bottom of a row the two-line title made 94px tall — and the
     strip in row three is pinned by the elastic row above it rather than by this. */
  align-items: start;
  /* A hairline, not the ivory dissolve this replaces. The dissolve existed to hand dark type a pale
     ground and to hide the join; a dusk photograph meeting the ivory canvas is a horizon, and a
     horizon wants an edge. See --hero-seam for why it is a token. */
  border-bottom: 1px solid var(--hero-seam);
  background:
    var(--hero-scrim),
    var(--hero-foot),
    var(--hero-photo) 50% 35% / cover no-repeat;
  color: var(--hero-ink);
}

.mgr-top--overview .view-head {
  grid-column: 1;
  grid-row: 1;
}

.mgr-top--overview .mgr-chrome {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
}

/* The title goes ivory with the box. Not `color: inherit` on .view-title: that class is also the
   plain bar's heading on twelve other sections, where --ink is correct. */
.mgr-top--overview .view-title {
  color: var(--hero-ink);
}

/* The subtitle is over a photograph rather than flat ivory, so it takes --hero-ink-soft instead of
   --ink-soft (which is a mid navy and would be ink-on-ink here).
 *
   The width cap is a legibility constraint, not typographic preference: every alpha in --hero-scrim
   is measured over the left 32% of the band, which is as far as 52ch reaches at the narrowest
   desktop width. `.view-sub`'s own 58ch runs to ~36% at that width, where the pool has fallen off
   and the distant range's lit snow (luminance 0.95) begins. Capping the text is the cheap fix;
   widening the pool to cover it would mean washing the range to protect one line. */
.mgr-top--overview .view-sub {
  max-width: 52ch;
  color: var(--hero-ink-soft);
}

/* The wave. `aria-hidden` in the markup, so this is purely visual: a touch of optical spacing and a
   size step down, because at the title's own 400-weight display size the emoji renders visually
   heavier than the letterforms beside it and pulls the eye off the name. */
.view-title-wave {
  display: inline-block;
  margin-left: 0.3em;
  font-size: 0.78em;
  /* Emoji fonts sit high on the baseline; this drops it back onto the x-height of the text. */
  vertical-align: 0.06em;
}

/* The hamburger. `display: none` at desktop, so it leaves the accessibility tree too and needs no
   aria-hidden bookkeeping; the narrow block below flips it to inline-flex.
 *
 * The bars are one element: a 2px background plus two box-shadow copies offset up and down. Three
 * nested spans would be the usual way and this needs no extra markup. On open they become an X —
 * the shadows collapse to the centre line and the two visible bars counter-rotate. */
.nav-toggle {
  display: none;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius-sm);
  background: var(--glass-bg-strong);
  box-shadow: inset 0 1px 0 var(--glass-edge);
  color: var(--ink);
  cursor: pointer;
}

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentcolor;
  transition:
    transform var(--dur-fast) var(--ease),
    opacity var(--dur-fast) var(--ease);
}
.nav-toggle-bars {
  position: relative;
}
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
}
.nav-toggle-bars::before {
  transform: translateY(-6px);
}
.nav-toggle-bars::after {
  transform: translateY(6px);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars {
  /* The middle bar goes, the outer two cross. */
  background: transparent;
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before {
  transform: rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after {
  transform: rotate(-45deg);
}

/* A light grain pass over the hero. A photograph carries its own texture, so this is much fainter
   than the drawn scene it once replaced needed (0.09, to stop a CSS gradient banding on a projector).
   It still earns its place: the type pool is a ~450px radial ramp, the largest continuous gradient
   on the page and the first place an 8-bit display shows steps — the ramp moved from an ivory
   dissolve to a slate ellipse but the banding risk did not go away with it.
   Scoped to the Overview hero — the plain bar every other section uses has no gradient to band. */
.mgr-top--overview::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--grain);
  background-size: 140px 140px;
  mix-blend-mode: overlay;
  opacity: 0.05;
  pointer-events: none;
}

/* ------------------------------------------------------------------------------------------------
 * The operational summary strip — the hero's foot.
 *
 * ONE surface with five cells and dividers between them, not five cards in a row. That is the
 * requirement and it is also the honest shape: these are five readings of the same shift, and five
 * separate panes would say they were five unrelated things. The dividers are `border-inline-start`
 * on every cell but the first, so the count of rules always follows the count of cells.
 *
 * Its contrast does NOT come from --hero-foot. That ramp only softens the tonal step at the hero's
 * edge; the strip carries its own translucent surface, because the ridge under it still runs to 0.68
 * luminance in places (the lit terrace below the lodge). See --hero-strip-bg for the measurement.
 * ---------------------------------------------------------------------------------------------- */
.hero-strip {
  grid-column: 1 / -1;
  grid-row: 3;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  align-items: stretch;
  min-width: 0;
  border: 1px solid var(--hero-strip-line);
  border-radius: var(--radius);
  background: var(--hero-strip-bg);
  /* A real blur: unlike the chrome pills, this genuinely sits on a photograph and there is something
     behind it to refract. Prefixed for Safari, which still needs it. */
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  /* The lit upper edge every glass surface on this page pairs with its alpha — without it the pane
     reads as a grey wash rather than as glass. */
  box-shadow:
    inset 0 1px 0 var(--hero-strip-edge),
    var(--shadow-2);
  color: var(--hero-ink);
}

/* Never on the Overview — the strip only exists there — but the class is on a shared header, so a
   future section that borrows it does not inherit a five-column grid by accident. */
.mgr-top:not(.mgr-top--overview) .hero-strip {
  display: none;
}

.hero-cell {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  min-width: 0;
  padding: var(--sp-4) var(--sp-5);
}

.hero-cell + .hero-cell {
  border-inline-start: 1px solid var(--hero-strip-line);
}

/* The badge: a circle of white at low alpha with a lit rim, the same device every card heading's
   mark now uses (see `.viz-mark`) — but tuned for a dark translucent surface rather than an ivory
   one, which is why it is its own rule and not that class. A pastel wash would sit on this pane
   like a sticker. */
.hero-cell-badge {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(38px * var(--type-scale));
  height: calc(38px * var(--type-scale));
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.1);
  color: var(--hero-ink);
}

.hero-cell-badge .icon {
  width: 18px;
  height: 18px;
}

.hero-cell-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

/* Tabular figures so a 2 and a 6 in adjacent cells sit on the same stem, and the serif because
   every other headline number on this page is display-cut too. */
.hero-cell-v {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(24px * var(--type-scale));
  font-weight: 500;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.hero-cell-k {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.3;
}

/* The one line of context under each metric — who is next, which room, what is waiting. Clipped to
   a single line rather than wrapped: five cells that each choose their own height would break the
   strip's baseline, and the same fact is spelled out in full in the section the cell points at. */
.hero-cell-hint {
  margin: 0;
  overflow: hidden;
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--hero-ink-soft);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- the header chrome --------------------------------------------------------------------- */

/* Solid ivory pills, and they stay solid now that the hero is dark.
 *
 * Frosted glass was tried here twice. It was right when the hero was a photograph under light type;
 * it was pointless when the hero became an ivory dissolve (a blur of cream is a blur of nothing);
 * and it is wrong again now, for a new reason: on a dusk photograph a translucent pale pane picks up
 * whatever is behind it, so the property name would be legible over the sky and muddy over the
 * range. An opaque surface is the only one whose contrast does not depend on where in the frame the
 * control happens to land. It also keeps the reference's own treatment — white pills on the picture.
 *
 * The label ink inside them is therefore unchanged: these are ivory surfaces, so --ink and
 * --ink-faint are measured correctly against them and no hero-specific override is needed. */
.mgr-ident,
.mgr-day,
.prop-switch {
  background: var(--surface);
  border-color: var(--hairline-strong);
  box-shadow: var(--shadow-1);
}

/* The greeting, as an eyebrow above the title rather than as the title. Small caps-height sans over a
   display serif is the standard editorial lockup, and it puts the page's name in the largest type
   instead of the one line carrying no information. Ivory, because it is on the photograph — and
   --hero-ink rather than --hero-ink-soft: at this size the soft tone is the one that would need the
   scrim's deepest zone, and it sits at the very top where the pool is strongest anyway. */
.view-greet {
  display: flex;
  gap: 0.35rem;
  align-items: center;
  margin: 0 0 0.35rem;
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  color: var(--hero-ink);
  text-transform: uppercase;
}

/* Emoji render heavier than the letterforms beside them and sit high on the baseline; both are
   corrected here rather than by choosing a lighter glyph, because the glyph is picked by the clock. */
.view-greet-glyph {
  font-size: 0.95em;
  letter-spacing: 0;
  vertical-align: 0.05em;
}

/* The day readout. Same pill chrome as the property selector because it is the same kind of thing —
   which slice of the world the boards below are answering for — and deliberately NOT a button:
   there is nothing to press. See manager.html for why this is a readout and not a date picker.
 *
   Built to be READ, not to be small. The date is the one piece of chrome that qualifies every number
   on the page, so it gets a calendar mark, a stacked label-over-value lockup and the value at body
   size in --ink. The label goes above rather than beside it: inline, "TODAY Mon, 18 Aug 2026" is a
   700px-wide pill at this type size, which took the whole chrome row on a laptop. */
.mgr-day {
  display: flex;
  flex-direction: row;
  gap: var(--sp-3);
  align-items: center;
  margin: 0;
  padding: 0.3rem var(--sp-4) 0.3rem var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

/* The mark, in the brand wash — the same circular treatment the card headings use, one size down. */
.mgr-day-icon {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(28px * var(--type-scale));
  height: calc(28px * var(--type-scale));
  border-radius: 50%;
  background: var(--brand-wash-strong);
  color: var(--brand-ink);
}

.mgr-day-icon .icon {
  width: 15px;
  height: 15px;
}

.mgr-day-text {
  display: flex;
  flex-direction: column;
  gap: 0;
  line-height: 1.2;
}

.mgr-day-k {
  font-size: calc(var(--fs-micro) * 0.92);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}

/* --fs-small and --ink, not --fs-micro and --ink-faint: this is the value, and at micro size in a
   faint tone it was the quietest thing in a row of ivory pills. */
.mgr-day-v {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------------------------------------
 * The notification bell and its panel.
 *
 * A round ivory button matching the chrome pills beside it, because it is the same layer of the
 * interface. `position: relative` on the wrapper is the panel's anchor and the reason `.mgr-top`
 * cannot clip — the same constraint `.prop-switch`'s listbox already imposes (see .mgr-top).
 * --------------------------------------------------------------------------------------------- */

.notif {
  position: relative;
  flex: none;
}

.notif-btn {
  display: grid;
  place-items: center;
  width: var(--chrome-h);
  height: var(--chrome-h);
  padding: 0;
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--shadow-1);
  color: var(--ink);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease);
}
.notif-btn:hover {
  border-color: rgba(61, 98, 136, 0.38);
  background: var(--brand-wash);
}

.notif-icon {
  display: grid;
  place-items: center;
}
.notif-icon .icon {
  width: 18px;
  height: 18px;
}

/* A dot, not a number. The count is in the button's `aria-label` (page.js), so this is decorative —
   and a two-digit badge on a 38px circle either overflows it or shrinks to unreadable. The ring is
   the pill's own surface, so the dot reads as sitting on top of the button rather than clipped by
   it, at any of the three chrome backgrounds this button takes. */
.notif-dot {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 9px;
  height: 9px;
  border: 2px solid var(--surface);
  border-radius: 50%;
  background: var(--danger-line);
}

/* The panel. Right-aligned to the bell and clamped to the viewport, because the bell is the
   rightmost control on the page and a left-aligned popover would open off-screen. */
.notif-panel {
  position: absolute;
  top: calc(100% + var(--sp-2));
  right: 0;
  z-index: 20;
  width: min(24rem, calc(100vw - 2 * var(--sp-4)));
  max-height: min(28rem, 70vh);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  overflow-y: auto;
  padding: var(--sp-4);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-3);
  color: var(--ink);
  text-align: left;
}

.notif-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: baseline;
  justify-content: space-between;
}

.notif-h {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -0.01em;
}

.notif-sub {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.notif-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.notif-row {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  width: 100%;
  padding: var(--sp-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.notif-row:hover {
  background: var(--surface-2);
}

/* Severity in the dot, and never only in the dot — every row prints its kind ("Action required",
   "Alert") as its first line, so the colour is emphasis rather than the message. */
.notif-dotmark {
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 0.4rem;
  border-radius: 50%;
  background: var(--ink-faint);
}
.notif-row[data-tone="blocked"] .notif-dotmark {
  background: var(--danger-line);
}
.notif-row[data-tone="caution"] .notif-dotmark {
  background: var(--warn-line);
}
.notif-row[data-tone="info"] .notif-dotmark {
  background: var(--brand);
}

/* An unseen row is hollow-marked rather than tinted: a background wash on "new" rows would fight the
   hover state, which is the one tint in this list that has to be unambiguous. */
.notif-row[data-unseen="0"] .notif-dotmark {
  background: none;
  box-shadow: inset 0 0 0 1.5px var(--hairline-strong);
}

.notif-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.notif-kind {
  font-size: var(--fs-micro);
  font-weight: 600;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}

.notif-title {
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* Clipped to two lines. A request summary is guest-written and can be a paragraph; the section this
   row opens shows all of it. */
.notif-detail {
  display: -webkit-box;
  overflow: hidden;
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--ink-faint);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.notif-when {
  margin-top: 0.1rem;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.notif-go {
  flex: none;
  margin-inline-start: auto;
  color: var(--ink-faint);
}

/* ================================================================================================
 * Toasts — the visual half of a live event. See js/manager/toast.js for what earns one.
 *
 * Bottom-right, and that corner is chosen rather than defaulted to. Top-right is where the bell and
 * its panel are, and a toast landing there would cover the badge it just incremented — the two would
 * be fighting over the same 24rem. Bottom-right is empty on every section, is out of the way of the
 * rail, and is where a stack can grow downward without pushing anything.
 *
 * `position: fixed` on a container OUTSIDE #app: `.mgr-top` sets `isolation: isolate` and the panels
 * are `hidden`-toggled tabpanels, so a toast rendered inside either could be clipped or hidden with
 * the section it happened to be in.
 * ============================================================================================= */

.toasts {
  position: fixed;
  right: var(--sp-5);
  bottom: var(--sp-5);
  /* Above the rail (4) and the header (3), below the modal scrim (55) and the drawer (60): a toast
     must not cover a case sheet a manager is reading a decision in. */
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(23rem, calc(100vw - 2 * var(--sp-5)));
  /* The container spans the corner permanently, so it must not eat clicks where it is empty. Each
     toast turns pointer events back on for itself. */
  pointer-events: none;
}

.toast {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-3);
  color: var(--ink);
  pointer-events: auto;
  animation: toast-in 240ms var(--ease) both;
}

/* A 3px left edge in the event's own tone. The whole card is NOT tinted: a stack of two washed cards
   over the dashboard's ivory reads as an error state even when one of them is a towel request. */
.toast[data-tone="info"] {
  border-inline-start: 3px solid var(--brand);
}
.toast[data-tone="caution"] {
  border-inline-start: 3px solid var(--warn-line);
}
.toast[data-tone="blocked"] {
  border-inline-start: 3px solid var(--danger-line);
}

.toast[data-leaving] {
  animation: toast-out 220ms var(--ease) both;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes toast-out {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateY(4px) scale(0.98);
  }
}

/* The same circular mark the card headings and the status rows use, so a toast reads as belonging to
   this dashboard rather than to the browser. */
.toast-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(32px * var(--type-scale));
  height: calc(32px * var(--type-scale));
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--ink-soft);
}
.toast-mark[data-tone="info"] {
  background: var(--brand-wash-strong);
  color: var(--brand-ink);
}
.toast-mark[data-tone="caution"] {
  background: var(--warn-bg);
  color: var(--warn-ink);
}
.toast-mark[data-tone="blocked"] {
  background: var(--danger-bg);
  color: var(--danger-ink);
}
.toast-mark .icon {
  width: 17px;
  height: 17px;
}

.toast-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

/* "New request" / "Alert" — the one thing that must be legible before the copy is read. */
.toast-kind {
  margin: 0;
  font-size: var(--fs-micro);
  font-weight: 600;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}

.toast-title {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* Two lines, clipped. The detail is guest-written or a road note and can be a paragraph; the section
   the toast opens shows all of it. */
.toast-detail {
  display: -webkit-box;
  overflow: hidden;
  margin: 0;
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--ink-faint);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.toast-x {
  display: grid;
  flex: none;
  place-items: center;
  width: 24px;
  height: 24px;
  margin-inline-start: auto;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
}
.toast-x:hover {
  background: var(--surface-2);
  color: var(--ink);
}
.toast-x .icon {
  width: 14px;
  height: 14px;
}

/* A toast that navigates says so on hover, the same way the queue rows do. */
.toast--link {
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}
.toast--link:hover {
  border-color: rgba(61, 98, 136, 0.38);
}

/* On a phone the stack spans the width, minus the page's own inset. Bottom-right stops meaning
   anything at 375px. */
@media (max-width: 40rem) {
  .toasts {
    right: var(--sp-3);
    left: var(--sp-3);
    bottom: var(--sp-3);
    width: auto;
  }
}

/* chat.css gives .btn-ghost `color: var(--header-ink)`, which is this page's rail *ivory* — so on
   every light surface here it rendered ivory-on-ivory and vanished (the case sheet's "Edit &
   approve", every Retry button). Page-wide, not scoped to .mgr-chrome, because .btn-ghost is not. */
.btn-ghost {
  color: var(--ink);
}
/* In the chrome it takes the same solid ivory treatment, so the control strip stays one system. */
.mgr-chrome .btn-ghost {
  background: var(--surface);
  border-color: var(--hairline-strong);
  box-shadow: var(--shadow-1);
  font-weight: 600;
}
.mgr-chrome .btn-ghost:not(:disabled):hover {
  background: var(--surface-lift);
  border-color: var(--hairline-strong);
}

.view-head {
  min-width: 0;
}

/* The page's one display-serif moment, and the anchor of the whole hierarchy. --fs-display, not
   --fs-h1: at 20-28px against 15-17.5px body the title was the same weight as everything else on
   the screen, which is most of why the page had no focal point. Negative tracking because serifs
   at display size set loose by default. */
/* Weight 400, not 600. Emphasis comes from size, never from weight: a 700-weight serif at 46px is
   a supermarket sign, a 400-weight serif at 46px is authority. Every premium reference sets display
   type light-to-regular — 400 at 68-72px is the norm — and this was the mistake in the first pass.
   Tracking goes to -0.03em because serifs set loose at display size. */
.view-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

/* Line-height 1.55 and a little more air above it: at 1.25 the subtitle crowded the serif title's
   descenders, which is the kind of thing that reads as "cramped" without being nameable. */
.view-sub {
  margin: 0.5rem 0 0;
  max-width: 58ch;
  font-size: var(--fs-small);
  line-height: 1.55;
  color: var(--ink-faint);
}

/* One control strip: which property, and a refresh. Who is acting now lives in the rail's own
 * account panel (see .side-account below) rather than sharing this strip.
 *
 * Both controls are the same height and sit on one line. The property picker used to stack its
 * label above the select, so it stood taller than the button beside it and the row had two
 * different vertical centres — the label now runs inline, in the same pill chrome. */
.mgr-chrome {
  --chrome-h: calc(38px * var(--type-scale));
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
}

.mgr-chrome > * {
  min-height: var(--chrome-h);
}

.mgr-ident {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem var(--sp-2);
  align-items: baseline;
  margin: 0;
  font-size: var(--fs-micro);
}

.mgr-ident-k,
.prop-switch-k {
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  font-size: var(--fs-micro);
}

.mgr-ident-v {
  font-weight: 600;
}

/* the property switcher */

.prop-switch,
.mgr-ident {
  display: flex;
  flex-direction: row;
  gap: var(--sp-2);
  align-items: center;
  padding: 0.25rem 0.35rem 0.25rem var(--sp-4);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--surface);
}

/* --- the property combobox ---------------------------------------------------------------- */

/* `position: relative` is the popup's anchor, and the reason .mgr-chrome cannot clip: the list is
   absolutely positioned out of this box. */
.prop-switch {
  position: relative;
}

.prop-select {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: calc(30px * var(--type-scale));
  padding: 0.3rem 0.55rem 0.3rem var(--sp-3);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  max-width: 15rem;
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

/* Champagne on hover, and this is the third and last place the accent is allowed (see its token
   note): the property switcher is the single most-used control in the header, and a brass rim
   warming under the pointer is exactly the "premium interaction detail" the accent is reserved
   for. The fill stays ivory — only the edge lights. */
.prop-select:hover {
  border-color: var(--champagne-deep);
  background: var(--surface-lift);
  box-shadow: 0 0 0 3px var(--champagne-wash);
}

/* Open reads as pressed-and-lit, and goes green rather than champagne: open is a *state* the
   listbox below shares, and the selected option in that list is a --brand fill. One hue for the
   open pair. */
.prop-select[aria-expanded="true"] {
  border-color: var(--brand);
  box-shadow: var(--glow-brand);
}

.prop-select-v {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A drawn chevron, so it inherits the ink and rotates on open — the native one could do neither. */
.prop-select-chev {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  margin-inline-start: auto;
  border-right: 2px solid currentcolor;
  border-bottom: 2px solid currentcolor;
  border-radius: 1px;
  transform: translateY(-2px) rotate(45deg);
  opacity: 0.6;
  transition: transform var(--dur-fast) var(--ease);
}
.prop-select[aria-expanded="true"] .prop-select-chev {
  transform: translateY(1px) rotate(-135deg);
}

.prop-list {
  position: absolute;
  z-index: 40;
  top: calc(100% + var(--sp-1));
  right: 0;
  /* Never narrower than the trigger, never wider than the viewport edge it opens against — this is
     what the native popup got wrong on a phone, where it ran off-screen. */
  min-width: 100%;
  max-width: min(20rem, calc(100vw - var(--sp-4) * 2));
  max-height: min(18rem, 50dvh);
  overflow-y: auto;
  margin: 0;
  padding: var(--sp-1);
  list-style: none;
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-3);
  scrollbar-color: var(--scroll-thumb) transparent;
}

.prop-option {
  display: flex;
  align-items: center;
  /* 44px: this is a touch target on a phone, where the native picker used to be the platform's job. */
  min-height: 44px;
  padding: 0 var(--sp-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  cursor: pointer;
}

/* Active = where the keyboard or pointer is. A wash, so it never competes with the selected row. */
.prop-option.is-active {
  background: var(--brand-wash-strong);
}

/* Selected = the property actually being viewed. The brand fill and the check are the only place
   a committed choice is stated, so it has to win over the active wash — hence the order here. */
.prop-option[aria-selected="true"] {
  background: var(--brand);
  color: var(--on-brand);
}
.prop-option[aria-selected="true"]::after {
  content: "";
  width: 0.34rem;
  height: 0.62rem;
  margin-inline-start: auto;
  border-right: 2px solid currentcolor;
  border-bottom: 2px solid currentcolor;
  transform: rotate(45deg) translate(-1px, -1px);
}
.prop-option[aria-selected="true"].is-active {
  background: var(--brand-light);
}

@media (prefers-reduced-motion: reduce) {
  .prop-select,
  .prop-select-chev {
    transition: none;
  }
}

/* The actor chip that used to live here moved to the rail's own account panel — see
   .side-account near .side-foot. The build's lack of a staff sign-in is stated in Settings
   ("Staff sign-in — not configured"), not as a strip across the top of every view. */

/* `position: relative` + z-index because the canvas grain is a `position: fixed` layer at z-index 0
   on .mgr; the rail and the hero establish their own stacking contexts already, this covers the
   content column so panels sit above the texture rather than under it. */
.mgr-main {
  position: relative;
  z-index: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* Reserves exactly the height of the sample-data footer bar, so the last card in any section
     clears it instead of ending underneath it. Resolves to 0 when there is no bar — see
     --disclosure-h. This is what lets that bar be `fixed` without ever covering content. */
  padding-bottom: var(--disclosure-h);
}

/* `:has()` on body, not a class from JS: paintSampleBanner prepends the element and nothing else
   needs to know. The height is the bar's real one — two lines of --fs-micro at 1.4 plus its
   padding — so the reservation and the bar cannot drift apart. */
body:has(.sample-banner) {
  --disclosure-h: 2.4rem;
}

@media (max-width: 40rem) {
  /* The sentence wraps to two lines in a phone's width, so the bar is taller and the reservation
     has to follow it. */
  body:has(.sample-banner) {
    --disclosure-h: 3.4rem;
  }
}

/* A tabpanel is focusable so the tab -> panel path works, but it is not an interactive control:
   no visible ring on a programmatic focus, only on real keyboard focus of the panel itself. */
.mgr-insights:focus {
  outline: none;
}

/* filters */

/* The Reports section's 3/6/12-month range (see `paintWindowFilters`, non-segmented mode).
 *
 * Unselected is deliberately quiet — transparent, a hairline, secondary ink. Only one of the set
 * is ever true, so every one that is *not* selected is permanent furniture, and giving it a fill
 * makes a row of controls compete with the figures they scope.
 *
 * Selected is the emerald --brand, which it already was; what changes here is that the pair now
 * has real separation (a transparent-bordered fill against no fill at all, rather than one wash
 * against another) and a transition between them. */
.filter {
  display: inline-flex;
  gap: 0.35rem;
  align-items: center;
  min-height: calc(30px * var(--type-scale));
  padding: 0.3rem var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  cursor: pointer;
  transition:
    background var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}
.filter:hover:not([aria-pressed="true"]) {
  border-color: var(--hairline-strong);
  background: var(--surface);
  color: var(--ink);
}
.filter[aria-pressed="true"] {
  background: var(--brand);
  border-color: transparent;
  color: var(--on-brand);
  box-shadow: 0 1px 2px rgba(22, 40, 60, 0.16);
}

.filter-count {
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
}

/* cards.css gives `.pill` a `margin-left: auto` so a lone pill can sit at the right end of a header
   row. In a *group* of pills that is a shove, not an alignment: the first pill absorbs all the free
   space and everything after it drifts to the right edge, which is why the detail sheet's pills
   read as "OPEN … … NEGATIVE Room Cleanliness". In a sequence, pills are a sequence. */
.detail-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  align-items: center;
}

.detail-pills > .pill,
.cell-stack > .pill,
.ops-row > .pill,
.ops-sum-v > .pill {
  margin-left: 0;
}

.detail-meta {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.tag {
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--paper);
  font-size: var(--fs-micro);
  color: var(--ink-soft);
}

/* detail */

.detail-head {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--hairline);
}

.panel {
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--hairline);
}
.panel:last-child {
  border-bottom: 0;
}

.panel-h {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.muted {
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--fs-small);
}

/* transcript */

.transcript {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.turn {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: var(--sp-2);
  align-items: baseline;
}

.turn-role {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.turn-text {
  margin: 0;
  padding: var(--sp-2);
  border-radius: var(--radius-sm);
  background: var(--bubble-in);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.turn-agent .turn-text {
  background: var(--bubble-out);
}

/* issues */

.issue-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.issue {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}

.issue-verbatim {
  flex: 1 1 100%;
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
  font-style: italic;
  overflow-wrap: anywhere;
}

/* remedy + notices */

.remedy {
  margin: 0;
  padding: var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: var(--fs-lead);
  overflow-wrap: anywhere;
}
.remedy.is-struck {
  text-decoration: line-through;
  color: var(--ink-faint);
  font-size: var(--fs-body);
}

.notice,
.note {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  margin: 0 0 var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  font-size: var(--fs-small);
}

.note {
  flex-wrap: wrap;
  margin: 0;
}

.note-text {
  margin: 0;
  flex: 1;
}

.notice[data-tone="caution"],
.note[data-tone="caution"] {
  background: var(--warn-bg);
  border-color: var(--warn-line);
  color: var(--warn-ink);
}
.notice[data-tone="ok"],
.note[data-tone="ok"] {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok-ink);
}
.notice[data-tone="blocked"],
.note[data-tone="blocked"] {
  background: var(--danger-bg);
  border-color: var(--danger-line);
  color: var(--danger-ink);
}
.notice[data-tone="info"],
.note[data-tone="info"] {
  background: var(--info-bg);
  border-color: var(--info-line);
  color: var(--info-ink);
}

/* actions + button variants cards.css/chat.css don't define */

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.btn-solid {
  border: 1px solid transparent;
}
.btn-brand {
  background: var(--brand);
  color: var(--on-brand);
}
/* Hovers *darker*, not lighter: --on-brand is ivory, so lightening the fill toward it would drop
   contrast (5.6:1 at rest to ~3.2:1 at --brand-light — under AA), where darkening it can only
   ever raise contrast further above the floor. The lighten-on-hover this replaces was tuned for
   the gold build's light-fill/dark-ink pairing, the opposite of this one. */
.btn-brand:not(:disabled):hover {
  background: var(--brand-dark);
}
.btn-danger {
  background: var(--danger-ink);
  color: var(--paper);
}
.btn-danger:not(:disabled):hover {
  filter: brightness(1.1);
}
.btn-quiet {
  background: var(--ghost-bg);
  border: 1px solid var(--hairline);
  color: var(--ink-soft);
}
.btn-quiet:not(:disabled):hover {
  background: var(--ghost-bg-hover);
}

/* dialog */

.dlg-scrim {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: var(--sp-3);
  background: rgb(0 0 0 / 45%);
}

.dlg-panel {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  width: min(34rem, 100%);
  max-height: calc(100dvh - 2 * var(--sp-3));
  overflow-y: auto;
  padding: var(--sp-4);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-3);
}

/* Stop the page behind a modal from scrolling under it. */
body.dlg-open {
  overflow: hidden;
}

/* ---------------------------------------------------------------------------------------------
 * The case sheet — content in front, not a side pane.
 *
 * Wider than a dialog because the thing being read is a transcript: at a third of the screen the
 * turns wrapped every few words. The header is sticky so the close button is always reachable in a
 * long case, and only the body scrolls so the title never leaves.
 * ------------------------------------------------------------------------------------------- */

.sheet-panel {
  display: flex;
  flex-direction: column;
  width: min(62rem, 100%);
  max-height: calc(100dvh - 2 * var(--sp-3));
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}

.sheet-head {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  justify-content: space-between;
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  border-bottom: 1px solid var(--hairline);
  background: var(--surface);
}

.sheet-title {
  margin: 0;
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
}

.sheet-x {
  flex: none;
}

.sheet-body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--sp-4) var(--sp-4);
}

.dlg-head {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-start;
  justify-content: space-between;
}

.dlg-title {
  margin: 0;
  font-size: var(--fs-h3);
  line-height: var(--lh-tight);
}

.dlg-desc {
  margin: 0;
  color: var(--ink-soft);
}

.dlg-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.dlg-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  justify-content: flex-end;
}

.dlg-error {
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--danger-line);
  border-radius: var(--radius-sm);
  background: var(--danger-bg);
  color: var(--danger-ink);
  font-size: var(--fs-small);
}

/* dialog fields */

.field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.field-label {
  font-size: var(--fs-small);
  font-weight: 600;
}

.field-input {
  width: 100%;
  padding: var(--sp-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  resize: vertical;
}
.field-input[readonly] {
  background: var(--surface-2);
  color: var(--ink-soft);
}

.field-hint {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* ===========================================================================================
 * Insights (C3.2)
 * ======================================================================================== */

/* Section rhythm. The old --sp-3/--sp-4 pair was the single biggest reason the page read as
   "cards placed beside each other": at 12px the gutter between two panels was the same measure as
   the gap between a card's own heading and its body, so nothing grouped. --sp-5 between blocks and
   --sp-6 of page inset gives the grid room to read as a composition, and it is the change that
   most makes the dashboard feel unhurried. */
.mgr-insights {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-6) var(--sp-7);
}

.insights-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: flex-end;
  /* The filter group sits right; anything to its left claims the space. */
  justify-content: flex-end;
}

/* One filter row above everything it scopes — never a control inside a chart card.
 *
 * A true segmented control, not three loose pills: 3/6/12 months are mutually exclusive views of
 * one axis, and a shared track says that structurally. Loose pills say "three independent
 * toggles", which is a different control and the wrong promise. The track also gives the
 * unselected segments somewhere to sit without needing borders of their own — which is what stops
 * two permanently-unselected options from carrying as much visual weight as the live one. */
.viz-filters {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
}

/* Inside the track the segments lose their own chrome — see .filter, which keeps a border for
   the Reports window filter where each pill IS independent. */
.viz-filters .filter {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}

.viz-filters .filter:hover:not([aria-pressed="true"]) {
  border-color: transparent;
  background: var(--surface-lift);
}

.viz-filters-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Visible label, and the group's accessible name via aria-labelledby. */
.viz-filters-k {
  margin: 0;
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.insights-status:empty {
  display: none;
}

/* A refetch holds the previous render at reduced opacity instead of flashing a skeleton. */
[data-loading] {
  opacity: 0.55;
  transition: opacity var(--dur) var(--ease);
}


/* chart cards */

/* One column until there is room for two. The time series span the full width because a trend
   needs horizontal room to be a trend; the heatmap and funnel then share the last row, which is
   what stops the funnel from sitting alone beside dead space. */
.viz-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}

@media (min-width: 68rem) {
  .viz-grid {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  }
  .viz-card-wide {
    grid-column: 1 / -1;
  }
}

/* ---------------------------------------------------------------------------------------------
 * The Overview's grid.
 *
 * Three columns once there is room, because the page has exactly two rhythms: one wide figure with
 * a companion beside it, then three equal panels. The trend spans two columns so a six- or
 * twelve-month series has the width to be a series; the split, queue, categories and operations
 * panels are all one-column reads.
 * ------------------------------------------------------------------------------------------- */

/* `stretch`, not `start`: the three panels on the bottom row are read across, and a row where one
   card ends level with its heading and the next runs three times deeper has no line to read along.
   Each card is already a column flex, so the extra height goes to the card, not to its contents. */
.ov-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: stretch;
}

@media (min-width: 60rem) {
  .ov-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .ov-wide {
    grid-column: 1 / -1;
  }
}

@media (min-width: 86rem) {
  .ov-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .ov-wide {
    grid-column: span 2;
  }
}

/* Every column, at every breakpoint — for a card that is a row of its own rather than a panel
   sharing one. `1 / -1` is breakpoint-independent, unlike `.ov-wide`'s span. */
.ov-full {
  grid-column: 1 / -1;
}

/* The Overview's second row: Today's stays and Property status, two-up across the whole width.
 *
 * A nested grid rather than two cards in the three-column parent, because two of three columns leaves
 * a third of the row empty and reads as an unfinished third card. Spanning the full width and halving
 * it gives both cards more room than a third each and closes the row honestly.
 *
 * `align-items: stretch` is inherited from `.ov-grid`, so the two stay the same height as each other
 * whatever their content — which is why both their CTAs sit on one baseline (see `.stays-cols`). */
.ov-row2 {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: stretch;
}

@media (min-width: 60rem) {
  .ov-row2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ------------------------------------------------------------------------------------------------
 * Alerts: Roads across the top, Weather and Permits filling the row under it.
 *
 * Its own grid rather than `.ov-grid` plus a modifier. `.ov-grid` goes to three columns at 86rem,
 * so a two-column override there is a rule fighting the rule above it — and on a wide screen the
 * pair below Roads was laid out on thirds with the last one empty. Two columns is not a tweak to
 * the Overview's layout, it *is* this section's layout: three long-form panels, not six charts.
 * ---------------------------------------------------------------------------------------------- */

.alerts-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  /* Weather and Permits read across, so they share a base line however unequal their contents. */
  align-items: stretch;
}

@media (min-width: 60rem) {
  .alerts-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* Roads takes both: the landslide notes wrap to three lines in half a row. */
  .alerts-grid > .alerts-wide {
    grid-column: 1 / -1;
  }
}

/* The one footnote under all three cards. It sits on the page rather than in a card, so it has to
   borrow the cards' inset to line up with their text instead of hanging off the left edge. */
#alerts-foot {
  padding-inline: var(--sp-5);
}

/* Before the payload lands there is nothing to qualify, and an empty `<p>` still takes a line. */
#alerts-foot:empty {
  display: none;
}

/* The card. Depth now comes from three quiet things rather than four loud ones:
 *   1. --card-face, a 1.5% warm gradient, so the surface has a direction of light.
 *   2. A hairline border — the primary separator, and on an ivory-on-ivory page it does more work
 *      than any shadow can.
 *   3. --shadow-1, a single 1px contact layer. Just enough that the card is not flush.
 *
 * The lit inset top edge is gone, and so is --shadow-2. Both were tuned on a card viewed alone;
 * tiled a dozen times across a grid the highlight read as a scuff along every top border and the
 * shadow as haze between panels. What replaces them is space (the grid gutters above) — which is
 * how editorial layouts have always separated panels.
 *
 * Padding stays --sp-5. Tight padding is what makes a card look like a table cell. */
.viz-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 0;
  padding: var(--sp-5);
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  transition:
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}

/* A card heading with a control or a status chip on its right. */
.viz-head-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  justify-content: space-between;
}

/* ================================================================================================
 * Overview card headings: a tinted circular mark, then the heading.
 *
 * Scoped to `#ov-grid`, and that scope is the point. `.viz-head` and `.viz-head-row` are shared with
 * Reports, Reviews and Alerts, which have their own established heading rhythm and no marks in their
 * markup — a global change here would have restyled four sections nobody asked about.
 *
 * ## Why a circular wash and not a coloured heading
 *
 * The colour has to differentiate cards at a glance without becoming the loudest thing on the page.
 * Tinting the heading itself makes eight headings compete with the figures under them and stops the
 * type reading as one system; putting the colour in a 32px circle beside it gives the same
 * at-a-glance sort with a twentieth of the ink. It is also the reference's own treatment.
 *
 * Every pair below is one of this page's existing measured status washes (--ok-bg/--ok-ink and its
 * siblings), so no new colour was introduced to get here — and the icon is never load-bearing: every
 * card's heading says in words what its mark says in colour.
 * ============================================================================================= */

#ov-grid .viz-title {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  min-width: 0;
}

#ov-grid .viz-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(32px * var(--type-scale));
  height: calc(32px * var(--type-scale));
  border-radius: 50%;
  /* The neutral fallback, so a mark with an unknown or missing tone is still a circle rather than a
     bare icon floating beside the heading. */
  background: var(--surface-2);
  color: var(--ink-soft);
}

#ov-grid .viz-mark .icon {
  width: 17px;
  height: 17px;
}

#ov-grid .viz-mark[data-tone="ok"] {
  background: var(--ok-bg);
  color: var(--ok-ink);
}
#ov-grid .viz-mark[data-tone="warn"] {
  background: var(--warn-bg);
  color: var(--warn-ink);
}
#ov-grid .viz-mark[data-tone="danger"] {
  background: var(--danger-bg);
  color: var(--danger-ink);
}
#ov-grid .viz-mark[data-tone="brand"] {
  background: var(--brand-wash-strong);
  color: var(--brand-ink);
}
/* Info: the palest step of the chart ramp, which is this page's existing pale blue, under the same
   navy ink `brand` uses. A separate tone from `brand` because two cards already carry that one
   (automation and the trend) and Property status has to be tellable from them at a glance — the
   whole point of the marks. Distinguished by lightness rather than hue, so it survives greyscale. */
#ov-grid .viz-mark[data-tone="info"] {
  background: var(--heat-1);
  color: var(--brand-ink);
}
/* Gold: the champagne wash the rail already uses, over the amber ink from --warn. A separate pair
   from `warn` on purpose — Guest experience is not a warning, and the two never sit in one row. */
#ov-grid .viz-mark[data-tone="gold"] {
  background: var(--champagne-wash-strong);
  color: var(--warn-ink);
}
#ov-grid .viz-mark[data-tone="accent"] {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
}

/* One step up in size and weight, and only inside the Overview. 500 to 600 is what makes a card
   heading read as a heading beside a 27px figure; `--fs-h2` alone was being out-shouted by the
   numbers under it. The serif and the negative tracking are unchanged — this is emphasis within the
   existing type system, not a second one. */
#ov-grid .viz-h {
  font-size: calc(var(--fs-h2) * 1.06);
  font-weight: 600;
  letter-spacing: -0.015em;
}

/* Premium spacing: the head gets a rule under it, so the heading band is visually its own thing
   rather than the first line of the content. A hairline, not a border — at --hairline-strong it
   started to read as a table header. */
#ov-grid .viz-head,
#ov-grid .viz-head-row {
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--hairline);
}

/* The subtitle hangs under the whole head rather than beside the mark, so it starts at the card's
   own text column and not indented behind a 32px circle. */
#ov-grid .viz-head .viz-sub {
  margin-top: var(--sp-2);
}

/* ================================================================================================
 * A segmented control — the Guest experience window.
 *
 * One pill split into cells rather than the page's separate `.filter` chips, because it has to sit
 * beside a heading in a 355px card: three chips with their own borders and gaps came to 235px and
 * wrapped to a line of their own. Shared track, no gaps, 42px cells — 118px total.
 *
 * The selected cell is a filled navy pill on an ivory track, which is the strongest available
 * "this one" that does not need colour: 8.2:1 of contrast between the two states, so the selection
 * survives greyscale, projector and forced colours (see the forced-colors block for that last one).
 * ============================================================================================= */

.viz-seg {
  display: inline-flex;
  flex: none;
  padding: 2px;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
}

.seg-btn {
  min-width: calc(38px * var(--type-scale));
  padding: 0.25rem 0.5rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-faint);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.seg-btn:hover:not([aria-pressed="true"]) {
  color: var(--ink);
  background: var(--surface-lift);
}

.seg-btn[aria-pressed="true"] {
  background: var(--brand);
  color: var(--on-brand);
  box-shadow: var(--shadow-1);
  cursor: default;
}

/* A closing line under a panel: what was left out, or what the data cannot tell you. */
.viz-foot {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* A control that reads as a link because it navigates rather than acts. --brand-ink, not --brand:
   the brand green only clears 3.4:1 on white, which fails for label-sized text. */
/* The 44px touch overlay further down covers coarse pointers. This is the *fine*-pointer floor:
   WCAG 2.5.8 asks 24x24 CSS px of every target regardless of input, and this measured 20px tall.
   The inline-target exception does not rescue it — "View all" sits alone in a card head, not inside
   a sentence. `inline-flex` + min-height grows the box without moving the text or the underline. */
.link-btn {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--brand-ink);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  text-align: left;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.link-btn:hover {
  text-decoration-thickness: 2px;
}

/* The time-series figures need width more than the funnel does. */
.viz-card-wide {
  grid-column: 1 / -1;
}

.viz-head {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

/* Card titles take the serif too, one step down from .view-title. Two type families and three
   sizes is the whole system — a card heading that is merely bold sans at body+1 is indistinguishable
   from the text under it. */
.viz-h {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.viz-sub {
  margin: 0;
  max-width: 62ch;
  font-size: var(--fs-small);
  color: var(--ink-faint);
}

.viz-figure {
  margin: 0;
  min-width: 0;
}

/* Wide content scrolls in its own box; the page body never scrolls sideways. */
.viz-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.viz {
  display: block;
  width: 100%;
  height: auto;
}

/* svg marks */

.viz-gridline {
  stroke: var(--viz-grid-line);
  stroke-width: 1;
}

/* The zero line is emphasised, not dashed: dashing reads as "threshold" when it is just an axis. */
.viz-zero {
  stroke: var(--viz-zero-line);
  stroke-width: 1;
}

.viz-line {
  stroke: var(--viz-line);
  stroke-width: 2;
}

/* Raised from 0.1 to 0.2. This is the largest single coloured region on the Overview, and at 0.1 it
   was a grey-sage haze that read as nothing — one of the bigger contributors to "pale". A gradient
   that fades toward the axis would be better still, but it needs a <linearGradient> in the SVG's
   defs, which is a change in insights.js rather than here. */
.viz-wash {
  fill: var(--viz-line);
  fill-opacity: 0.2;
  stroke: none;
}

/* The 2px ring in the surface colour keeps a dot legible where it crosses its own line. */
.viz-dot {
  fill: var(--viz-line);
  stroke: var(--surface);
  stroke-width: 2;
}

.viz-pos {
  fill: var(--viz-pos);
}

.viz-neg {
  fill: var(--viz-neg);
}

/* Text wears text tokens, never the series colour. */
.viz-tick {
  fill: var(--ink-faint);
  font-size: 13px;
  font-family: var(--font-sans);
  text-anchor: middle;
  font-variant-numeric: tabular-nums;
}

.viz-tick-y {
  text-anchor: end;
}

.viz-value {
  fill: var(--ink);
  font-size: 14px;
  font-family: var(--font-sans);
  font-weight: 650;
}

.viz-value-low {
  fill: var(--ink-soft);
  font-weight: 600;
}

/* legend */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-4);
  margin-top: var(--sp-1);
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.legend-item {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
}

.legend-swatch {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 3px;
  background: currentColor;
}
.legend-swatch.viz-pos {
  background: var(--viz-pos);
}
.legend-swatch.viz-neg {
  background: var(--viz-neg);
}

/* ===========================================================================================
 * Sentiment split — the donut
 * ======================================================================================== */

.donut-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-4);
  align-items: center;
}

.donut-figure {
  flex: none;
  margin: 0;
}

.donut {
  width: calc(132px * var(--type-scale));
  height: auto;
}

.donut-track {
  stroke: var(--viz-track);
}

.donut-arc {
  /* Butt caps, not round: a rounded cap overlaps its neighbour and reads as a wider band. */
  stroke-linecap: butt;
}

.arc-pos {
  stroke: var(--viz-pos);
}
.arc-neu {
  stroke: var(--viz-neutral);
}
.arc-neg {
  stroke: var(--viz-neg);
}

/* The donut's centre number is a headline, so it takes the display serif and grows. Serif numerals
   at size are the single most "editorial report" thing on the page. */
.donut-total {
  fill: var(--ink);
  font-family: var(--font-display);
  font-size: 34px;
  font-weight: 400;
  letter-spacing: -0.02em;
  text-anchor: middle;
}

.donut-total-label {
  fill: var(--ink-faint);
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 600;
  text-anchor: middle;
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}

/* The legend is the figure's table twin: every band's count and share are here as text. */
.donut-legend {
  flex: 1 1 9rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.split-row {
  display: grid;
  grid-template-columns: 0.85rem 1fr auto auto;
  gap: var(--sp-2);
  align-items: center;
  font-size: var(--fs-small);
}

.split-swatch {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 3px;
}
.split-swatch.arc-pos {
  background: var(--viz-pos);
}
.split-swatch.arc-neu {
  background: var(--viz-neutral);
}
.split-swatch.arc-neg {
  background: var(--viz-neg);
}

.split-label {
  color: var(--ink-soft);
  overflow-wrap: anywhere;
}

.split-value {
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.split-share {
  min-width: 2.6rem;
  text-align: right;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* ===========================================================================================
 * Most-tagged categories — ranked bars
 * ======================================================================================== */

.cat-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.cat-row {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.cat-label {
  display: flex;
  gap: var(--sp-2);
  align-items: baseline;
  justify-content: space-between;
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
}

.cat-share {
  color: var(--ink-faint);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.cat-track {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
}

/* Scaled to the leader, so the ranking is the read; the share beside it carries the proportion. */
.cat-bar {
  height: 0.7rem;
  border-radius: 0 4px 4px 0;
  background: var(--brand);
  min-width: 2px;
}

.cat-value {
  font-size: var(--fs-small);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

/* ===========================================================================================
 * Needs a decision — the open-issue preview
 * ======================================================================================== */

.queue-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* "Things needing recovery" — the one panel on the Overview where a guest is actually waiting on the
 * person reading it, and the only place on this page a row is a real queue rather than a figure.
 *
 * Urgency without loudness: the row does NOT get a red fill, a left stripe or a badge of its own.
 * It gets *presence* — a real surface instead of a wash, more vertical room than a table row, and
 * the priority pill the markup already carries doing the semantic work. A panel that shouts on
 * every render stops meaning anything by the third look; a panel whose rows feel like objects you
 * can pick up reads as a workspace.
 *
 * The green wash and the border darkening are reserved for hover, where they mean "this one" — an
 * at-rest tint would say the same thing about all three. */
.queue-btn {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  width: 100%;
  padding: var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.queue-btn:hover {
  border-color: rgba(61, 98, 136, 0.38);
  background: var(--brand-wash);
}

/* The kind's glyph in a tinted circle — the row's only colour, and the thing that tells a complaint
   from a room issue before the text is read. Two tones, not three: every row in this card is something
   awaiting a person, so `urgent` (high severity or high priority) is red and the rest are amber. Green
   would mean nothing here. */
.queue-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(34px * var(--type-scale));
  height: calc(34px * var(--type-scale));
  border-radius: 50%;
  background: var(--warn-bg);
  color: var(--warn-ink);
}
.queue-mark[data-tone="urgent"] {
  background: var(--danger-bg);
  color: var(--danger-ink);
}
.queue-mark .icon {
  width: 17px;
  height: 17px;
}

.queue-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* The name and the severity pill share a line, with the pill pushed to the right edge of the body
   rather than sitting in a gutter of its own. In a gutter it set the row's left inset for every row
   including the ones whose pill is two characters wide, which left the names off the card's own
   text column. */
.queue-top {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  justify-content: space-between;
}

.queue-name {
  font-weight: 600;
  letter-spacing: -0.005em;
  overflow-wrap: anywhere;
}

.queue-meta {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}

/* Where in the stay, and through which channel. A step quieter than the categories above it — it is
   the answer to a question the manager has not asked yet. */
.queue-ctx {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  opacity: 0.85;
}

.queue-go {
  display: flex;
  flex: none;
  gap: 0.15rem;
  align-items: center;
  color: var(--brand-ink);
  font-size: var(--fs-micro);
  font-weight: 600;
}

/* The word, not the chevron, is what goes at narrow widths: the chevron still says "this opens
   something" in a third of the space. */
@media (max-width: 34rem) {
  .queue-go-label {
    display: none;
  }
}

/* ================================================================================================
 * Guest experience — three figures over the sentiment ring.
 *
 * `auto-fit` with a 5.5rem floor rather than three fixed columns: at the Overview's narrowest
 * three-up width a card is ~19rem inside its padding, and "Reviews this period" over a 2-digit
 * number needs about 6rem before the label wraps mid-word. Below that the figures go two-up and then
 * one-up on their own, with no breakpoint of their own to keep in step with the grid's.
 * ============================================================================================= */

.gx-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
  gap: var(--sp-3) var(--sp-4);
}

.gx-figure {
  min-width: 0;
}

/* Display cut and tabular, like every other headline number on the page. Smaller than --fs-metric:
   three of these sit in a third of a row, where the tile scale would wrap every label under them. */
.gx-v {
  display: flex;
  gap: 0.2rem;
  align-items: baseline;
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(27px * var(--type-scale));
  font-weight: 500;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}

/* "/ 5" is a unit and not part of the figure, so it takes the body face at label size — set at the
   metric size it read as a second number. */
.gx-unit {
  font-family: var(--font-sans);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--ink-faint);
}

.gx-k {
  margin: 0.15rem 0 0;
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.25;
}

.gx-hint,
.gx-delta {
  margin: 0.1rem 0 0;
  font-size: var(--fs-micro);
  line-height: 1.3;
  color: var(--ink-faint);
}

/* The delta takes a tone; the hint never does. Only one of them is ever rendered per figure — a
   move and a footnote in the same slot would compete for the same glance. */
.gx-delta {
  display: flex;
  gap: 0.2rem;
  align-items: center;
}
.gx-delta[data-dir="up"] {
  color: var(--ok-ink);
}
.gx-delta[data-dir="down"] {
  color: var(--danger-ink);
}
.gx-delta .icon {
  width: 13px;
  height: 13px;
}

/* ================================================================================================
 * AtithiVani automation today.
 *
 * Two groups in one panel, and the visual weight is deliberately lopsided: the automated group is a
 * ticked list a manager skims, the "waiting on you" group is rows they act on. So the first gets a
 * check mark and body ink, the second gets a clock, a heavier label and — where the row is a request
 * — a real hit target.
 * ============================================================================================= */

/* The two figures. A pair rather than a sentence and a sub-line: "3 handled / 4 awaiting you" is the
   card, and giving each half its own number and label makes the comparison the point.
 *
   `1fr 1fr` and not `auto`: equal halves keep the two numbers on the same left edges as the three in
   Guest experience beside it, which is what makes the row of cards scan as one instrument. */
.auto-figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
}

.auto-figure {
  min-width: 0;
  padding-inline-start: var(--sp-3);
  /* A 2px tone rule rather than a tinted panel — this card already has coloured marks in its rows,
     and two washed blocks above them would be the loudest thing in a three-card row. */
  border-inline-start: 2px solid var(--hairline-strong);
}
.auto-figure[data-tone="ok"] {
  border-inline-start-color: var(--ok-line);
}
.auto-figure[data-tone="warn"] {
  border-inline-start-color: var(--warn-line);
}

.auto-figure-v {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(27px * var(--type-scale));
  font-weight: 500;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}
.auto-figure[data-tone="ok"] .auto-figure-v {
  color: var(--ok-ink);
}
.auto-figure[data-tone="warn"] .auto-figure-v {
  color: var(--warn-ink);
}

.auto-figure-k {
  margin: 0.15rem 0 0;
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink-soft);
}

.auto-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.auto-row {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-start;
  min-width: 0;
}

/* The mark carries the row's state, in a soft circle like every other mark on this page.
 *
 * Three states and three tones: green tick for done, amber clock for waiting, red alert for waiting
 * AND high priority. There is deliberately no "failed" state — see `pendingActions` in overview.js for
 * why a failure tone would be a colour that could never appear.
 *
 * The colour is never the only signal: the glyph differs per state too, and the row's own text says
 * whether something was handled or is awaiting a decision. */
.auto-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(24px * var(--type-scale));
  height: calc(24px * var(--type-scale));
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--ink-faint);
}
.auto-mark .icon {
  width: 13px;
  height: 13px;
}
.auto-mark[data-state="done"] {
  background: var(--ok-bg);
  color: var(--ok-ink);
}
.auto-mark[data-state="pending"] {
  background: var(--warn-bg);
  color: var(--warn-ink);
}
.auto-mark[data-state="urgent"] {
  background: var(--danger-bg);
  color: var(--danger-ink);
}

.auto-body,
.auto-btn {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  min-width: 0;
}

/* A pending row that maps to a request is a button; a pending itinerary row is not, because there is
   no sheet on this page for it to open. They share every other rule, so the underline on hover is
   the only thing distinguishing them — which is exactly the distinction being made. */
.auto-btn {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.auto-btn:hover .auto-text {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.auto-text {
  font-size: var(--fs-small);
  line-height: 1.35;
}
.auto-row[data-state="pending"] .auto-text {
  font-weight: 600;
}

.auto-meta {
  font-size: var(--fs-micro);
  line-height: 1.3;
  color: var(--ink-faint);
}

/* ================================================================================================
 * Today's stays — arrivals and departures, side by side.
 * ============================================================================================= */

/* Three figures across the top of the card — arrivals, in-stay, departures — then who is next.
 *
 * The two side-by-side name lists this replaced are gone: at a third of a row they were ~145px each,
 * which wrapped every name, and their two "view all" links were more chrome than the four rows above
 * them. Three counts and two names is the same information a desk actually reads, in half the height.
 *
 * Same `1fr` columns and same tone-rule treatment as the automation card's figures, so the two cards
 * in row two and the three in row one all put their numbers on a shared grid. */
/* The card's own body: figures, then the list, then the link pinned to the foot. `#stays` keeps its
   `.stays-cols` class from when it held two columns — the name is now a misnomer for a single column,
   but renaming it would touch the markup, the renderer and this file for no behavioural gain. */
.stays-cols {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 0;
}

.stays-figures {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
}

.stays-figure {
  min-width: 0;
  padding-inline-start: var(--sp-3);
  border-inline-start: 2px solid var(--hairline-strong);
}
.stays-figure[data-tone="ok"] {
  border-inline-start-color: var(--ok-line);
}
.stays-figure[data-tone="info"] {
  border-inline-start-color: var(--brand);
}

.stays-figure-v {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(27px * var(--type-scale));
  font-weight: 500;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}

.stays-figure-k {
  margin: 0.15rem 0 0;
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink-soft);
}

/* The direction mark on a guest row: an arrow in, or an arrow out. Small and tinted rather than a
   full circle — the row is one line and a 34px circle would set its height. */
.stays-dir {
  display: grid;
  flex: none;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--ok-bg);
  color: var(--ok-ink);
}
.stays-dir[data-kind="departing"] {
  background: var(--surface-3);
  color: var(--ink-soft);
}
.stays-dir .icon {
  width: 11px;
  height: 11px;
}

.stays-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

/* Direction mark, name, room. The room is pushed to the far edge so a stack of them reads down, and
   `baseline` rather than `center` because the two texts are different sizes. The row now has the whole
   card's width instead of a ~145px column, so nothing needs to wrap. */
.stays-row {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--hairline);
}
.stays-row:last-child {
  border-bottom: 0;
}

.stays-name {
  min-width: 0;
  overflow: hidden;
  font-size: var(--fs-small);
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stays-room {
  flex: none;
  margin-inline-start: auto;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.stays-empty {
  margin: 0;
  font-size: var(--fs-micro);
}

/* `margin-top: auto` pins the link to the card's foot, so this card's CTA sits on the same baseline as
   Property status's beside it however unequal their content. */
.stays-cols > .link-btn,
#ov-grid .viz-card > .link-btn {
  margin-top: auto;
}

/* ================================================================================================
 * Property status.
 * ============================================================================================= */

.prop-status-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.prop-status-row {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  min-width: 0;
}

/* The status is in the mark's TINT and also in the row's words — never in the tint alone. The three
   tones here are the page's existing status washes, which are the pairs already measured for
   contrast (see --ok-bg / --warn-bg / --danger-bg). */
.prop-status-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(34px * var(--type-scale));
  height: calc(34px * var(--type-scale));
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--ink-faint);
}
.prop-status-mark[data-tone="ok"] {
  background: var(--ok-bg);
  color: var(--ok-ink);
}
.prop-status-mark[data-tone="caution"] {
  background: var(--warn-bg);
  color: var(--warn-ink);
}
.prop-status-mark[data-tone="blocked"] {
  background: var(--danger-bg);
  color: var(--danger-ink);
}
.prop-status-mark .icon {
  width: 17px;
  height: 17px;
}

.prop-status-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.prop-status-top {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
}

.prop-status-k {
  font-size: var(--fs-small);
  font-weight: 600;
}

.prop-status-v {
  font-size: var(--fs-small);
  line-height: 1.35;
}

/* Where the affected guests are — the Wi-Fi row's one actionable line. A step above the provenance
   note in weight, because a room number is something someone acts on. Tabular so a list of rooms
   reads as numbers rather than as prose. */
.prop-status-extra {
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* The provenance line. Quieter than the verdict above it and deliberately not hidden behind a
   tooltip: on the Wi-Fi row this sentence is the difference between a measurement and a report. */
.prop-status-note {
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--ink-faint);
}

/* "Review issue →" — its own line under the note rather than the verdict being the link. Underlining
   "Issue reported · 2 guests affected" made the count look like the destination and left the row with
   no words for what a click does. `align-self` keeps the underline the width of the words. */
.prop-status-go {
  align-self: flex-start;
  margin-top: 0.25rem;
}

/* ===========================================================================================
 * The problems board — the post-checkout calls, grouped by topic, each with a fix
 *
 * Two surfaces share these rules: the Overview's one-row preview and the Reviews tab's full board.
 * `pb-` throughout so nothing here can collide with the review cards it sits above.
 *
 * The two lanes ("what guests said" / "what to change") stack below 48rem rather than shrinking,
 * because a two-word-per-line quote beside a two-word-per-line fix is worse than either full width.
 * ======================================================================================== */

.pb-hint {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.pb-lanes {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-3);
}

@media (min-width: 48rem) {
  .pb-lanes {
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  }
}

.pb-said,
.pb-fix {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
}

.pb-lane-h {
  margin: 0;
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --- the write-up excerpts ------------------------------------------------------------------ */

.pb-quotes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* A rating stripe rather than a fill: the card stays on the page's surface, and the severity read
   is a position-and-length cue that survives both themes and any colour-vision deficiency. */
.pb-quote {
  padding: var(--sp-2);
  border: 1px solid var(--hairline);
  border-inline-start: 3px solid var(--viz-neutral);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.pb-quote[data-rating="1"],
.pb-quote[data-rating="2"] {
  border-inline-start-color: var(--viz-neg);
}
.pb-quote[data-rating="3"] {
  border-inline-start-color: var(--viz-neutral);
}

.pb-quote-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  align-items: baseline;
  margin: 0 0 var(--sp-1);
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.pb-quote-rating {
  font-weight: 650;
  color: var(--ink-soft);
}

.pb-quote-text {
  margin: 0;
  overflow-wrap: anywhere;
}

/* --- the fix -------------------------------------------------------------------------------- */

.pb-fix-text {
  margin: 0;
  padding: var(--sp-2);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--brand-wash);
  overflow-wrap: anywhere;
}

.pb-fix-cite {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* The provider's reasoning, when it had any. Quieter than the fix and set apart from it: the
   imperative is the deliverable, this is why it is being suggested. */
.pb-fix-why {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
  border-inline-start: 2px solid var(--hairline);
  padding-inline-start: var(--sp-2);
}

/* --- the Overview preview: the worst groups, each opening its own sheet ---------------------- */

.pb-groups {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* The same rating stripe the write-up cards carry, from the group's worst call — so "how bad" reads
   the same way whether you are looking at one call or at the topic it rolled up into.
 *
 * "An insight panel, not another generic card": each group is a real surface (--surface, the card
 * face, not the recessed --surface-2 it used to sit on) so the board reads as a stack of findings
 * you could pick up and act on, rather than as striped rows in a table. The rating stripe stays
 * exactly as it was — it is the one genuinely semantic colour here and it is load-bearing. */
.pb-group {
  border: 1px solid var(--hairline);
  border-inline-start: 3px solid var(--viz-neutral);
  border-radius: var(--radius-sm);
  background: var(--surface);
  transition:
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}
.pb-group[data-rating="1"],
.pb-group[data-rating="2"] {
  border-inline-start-color: var(--viz-neg);
}
.pb-group:hover {
  border-color: var(--hairline-strong);
  box-shadow: var(--shadow-1);
}

/* Three columns — rank, body, chevron — so the topic names, the counts and the fixes of three
   stacked cards line up on the same edges instead of each starting wherever its rank ended. */
.pb-group-btn {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--sp-2) var(--sp-3);
  align-items: start;
  width: 100%;
  padding: var(--sp-3);
  border: 0;
  border-radius: inherit;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.pb-group-btn:hover {
  background: var(--brand-wash);
}

.pb-group-rank {
  min-width: 2rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: var(--fs-lead);
  color: var(--ink-faint);
}

.pb-group-body {
  display: grid;
  gap: var(--sp-1);
  min-width: 0;
}

/* Topic left, its counts right, and they wrap to two lines rather than shrinking the numbers. */
.pb-group-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  align-items: baseline;
  justify-content: space-between;
}

.pb-group-topic {
  font-weight: 650;
}

.pb-group-meta {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* The lane label is a `<span>` inside the button — see `groupCard`. */
.pb-group-btn .pb-lane-h {
  display: block;
  margin-top: var(--sp-1);
}

.pb-group-fix {
  overflow-wrap: anywhere;
}

/* A withheld fix is a stated reason, not an instruction — quieter, and marked like the board's note. */
.pb-group-fix-none {
  display: flex;
  gap: var(--sp-2);
  align-items: baseline;
  color: var(--ink-soft);
}
.pb-group-fix-none svg {
  flex: none;
  align-self: center;
}

.pb-group-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  align-items: baseline;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.pb-group-linked {
  font-weight: 600;
  color: var(--ink-soft);
}

.pb-group-btn .pb-go {
  align-self: center;
}

.pb-groups-hint {
  margin: var(--sp-3) 0 0;
}

.pb-all {
  margin-top: var(--sp-3);
  align-self: start;
}

/* --- one group, in the sheet ---------------------------------------------------------------- */

.pb-sheet-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: baseline;
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.pb-sheet-hint {
  margin: var(--sp-3) 0 0;
}

/* --- the board rows ------------------------------------------------------------------------- */

.pb-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.pb-row {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  padding: var(--sp-2);
  background: var(--surface-2);
}
.pb-row[data-open="true"] {
  border-color: var(--hairline-strong);
  background: var(--surface);
}

.pb-row-btn {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.pb-topic {
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* One basis for every row, so every bar starts on the same x — the whole point of a row of bars
     is that their left edges are comparable. Shrinkable rather than `flex: none`: at 360px a fixed
     11rem plus the count, the bar's 4rem floor and the chevron overran the viewport by 13px. Every
     row shares this basis and this shrink factor, so they narrow together and stay aligned. */
  flex: 0 1 11rem;
  max-width: 11rem;
}

.pb-topic-name {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.pb-topic-coarse {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.pb-count {
  flex: none;
  min-width: 2.5rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.pb-go {
  flex: none;
  color: var(--ink-faint);
  transition: transform 120ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .pb-go {
    transition: none;
  }
}
.pb-row[data-open="true"] .pb-go {
  transform: rotate(90deg);
}

/* --- the three-segment bar ------------------------------------------------------------------ */

.pb-bar {
  flex: 1;
  display: flex;
  min-width: 4rem;
  height: 0.75rem;
  border-radius: 999px;
  overflow: hidden;
  background: var(--viz-track);
}

.pb-seg {
  display: block;
  min-width: 2px;
}

.pb-seg-low {
  background: var(--viz-neg);
}

/* The middle segment is a **pattern, not a third hue**. insights.js already measured ok/warn/danger
   as adjacent fills and rejected them: they collapse to ΔE 1.7 under deuteranopia. The hatch is
   defined over `--viz-track`, which has dark-theme overrides, so it must never be a literal colour
   or it disappears in dark. */
.pb-seg-mid {
  background-image: repeating-linear-gradient(
    45deg,
    var(--ink-faint) 0,
    var(--ink-faint) 2px,
    transparent 2px,
    transparent 5px
  );
  background-color: var(--viz-track);
}

.pb-seg-high {
  background: var(--viz-pos);
}

.pb-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.pb-key {
  display: inline-flex;
  gap: var(--sp-1);
  align-items: center;
}

.pb-swatch {
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 2px;
  flex: none;
}

.pb-detail {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--hairline);
}

/* ===========================================================================================
 * Roads, weather & permits
 * ======================================================================================== */

/* Roads, weather and permits are a card each in Alerts, so their separation is the grid's gap and
   their headings are the cards' own. This is the sub-heading inside a card that still groups (the
   Knowledge section's rows by type). */
.ops-h {
  margin: 0;
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.ops-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* ---------------------------------------------------------------------------------------------
 * The Overview's summary of the above.
 *
 * A `<dl>` of label -> counts. Two columns on a `subgrid`-free layout would need fixed widths, so
 * the label column is sized to the longest of three short words and the values wrap beside it.
 * ------------------------------------------------------------------------------------------- */

.ops-sum {
  display: grid;
  gap: var(--sp-2);
  margin: 0;
}

.ops-sum-row {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: var(--sp-2);
  align-items: baseline;
}

.ops-sum-k {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.ops-sum-v {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  align-items: center;
  margin: 0;
  font-size: var(--fs-small);
}

/* The row's cards are equal height, so a "more" link left where the content happens to end floats
   in the middle of the card. Pushed to the base, it reads as the card's footer. */
#ops-all {
  margin-top: auto;
  align-self: flex-start;
}

/* Named only when something is wrong; on an all-clear day this line is absent, not empty. */
.ops-sum-worst {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: var(--sp-2);
  align-items: baseline;
  margin: 0;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--hairline);
}

.ops-sum-worst-v {
  font-size: var(--fs-small);
  font-weight: 600;
}

.ops-row {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-start;
}

/* One width for every status in the list, so the names under each other form a column. Left to size
   itself from its label, the pill put four road names at four different indents — and the loudness
   of "Closed" is carried by its colour, not by being 8px wider than "Open". The label stays centred
   in the pill, so the shorter ones read as deliberate rather than short-changed. */
.ops-row > .pill {
  flex: none;
  margin-top: 0.1rem;
  min-width: 5.5rem;
  justify-content: center;
}

.ops-body {
  min-width: 0;
}

/* Compact, inline — one per road row, not a primary dialog-sized action. */
.ops-report-btn {
  margin-top: var(--sp-1);
  padding: 2px var(--sp-2);
  font-size: var(--fs-micro);
}

.ops-name {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem var(--sp-2);
  align-items: baseline;
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
}

.ops-region {
  font-weight: 400;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.ops-note {
  margin: 0.1rem 0 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  /* A landslide note runs long. Two lines, then clipped — the full text is the `title`. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.ops-weather {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.ops-w-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.1rem var(--sp-2);
  font-size: var(--fs-small);
}

.ops-w-place {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.ops-w-temp {
  font-variant-numeric: tabular-nums;
  font-weight: 650;
}

.ops-w-cond {
  grid-column: 1 / -1;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* RI-005: forward-looking rain/landslide-risk line, only rendered for elevated/high readings —
   reuses the same warn tokens the caution road-status chip uses, so a manager's eye is drawn to
   it the same way, without a third custom color introduced just for this one line. */
.ops-w-risk {
  grid-column: 1 / -1;
  font-size: var(--fs-micro);
  color: var(--warn-ink);
  background: var(--warn-bg);
  border-radius: var(--radius-sm);
  padding: 0.05rem var(--sp-2);
}

/* ===========================================================================================
 * The record sections — one table language for guests, bookings, itineraries, knowledge and
 * team, because they are all "records with a status chip".
 * ======================================================================================== */

/* A row of stats above a table, and deliberately at label size: these are a caption for the table
   under them and must never compete with it for attention. */
.strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}
.strip:empty {
  display: none;
}

.strip-stat {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.strip-label {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.strip-value {
  font-size: var(--fs-lead);
  font-weight: 650;
  line-height: var(--lh-tight);
}

.record-table {
  /* Wide tables scroll inside `.viz-scroll`; without this they compress columns to nothing. */
  min-width: 100%;
  width: auto;
}

/* 10px/16px minimum, which at --type-scale 1.45 becomes 14/23px — what row-three legibility needs.
   --sp-2 vertical was too tight for rows to separate without a gridline doing the work. */
.record-table th,
.record-table td {
  vertical-align: top;
  padding: 0.625rem var(--sp-4);
}

/* Warm zebra, mixed from the ink rather than laid on as a grey: 2.5% is enough to track a row's
   cells across a wide table, low enough that it never competes with the status chips in it. This
   is what replaces vertical gridlines — column separation comes from alignment and space. */
.record-table tbody tr:nth-child(even) {
  background: color-mix(in srgb, var(--ink) 2.5%, transparent);
}

/* --surface-lift, the ramp's one brightest step, reserved for exactly this kind of "you are
   addressing this row" moment. --brand-wash tinted the row gold, which read as a selection state. */
.record-table tbody tr:hover {
  background: var(--surface-lift);
}

/* Numeric columns get tabular figures wherever the table marks one. A column of proportional digits
   is the most common single tell of an unfinished dashboard. */
.record-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.record-table tbody th {
  font-weight: 600;
  white-space: nowrap;
}

/* A chip plus a word in one cell, stacked so the chip never widens the column. */
.cell-stack {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  align-items: flex-start;
}

/* Guest speech and replan reasons run long. Two lines here, the whole string on hover. */
.cell-clip {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  max-width: 26rem;
  white-space: normal;
  color: var(--ink-soft);
}

.cell-mono {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  color: var(--ink-soft);
}

/* knowledge attributes */

.attr-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  max-width: 34rem;
  white-space: normal;
}

.attr {
  display: inline-flex;
  gap: 0.3rem;
  align-items: baseline;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: var(--fs-micro);
}

.attr-k {
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}

.attr-v {
  color: var(--ink);
}

/* Alerts' Permits card: per-destination rows use .ops-list/.ops-note above; the "what you'll
   need" block below them (per traveller type — domestic/foreign) reuses .doc-list. */

.permit-reqs {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.doc-list {
  margin: var(--sp-1) 0 0;
  padding-left: var(--sp-4);
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.doc-list li + li {
  margin-top: 0.2rem;
}

/* The destination's general note (distinct from its permit-process note above it) — set apart
   just enough that the two don't read as one run-on sentence. */
.ops-note-general {
  font-style: italic;
}

/* settings */

.setting-list {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--sp-2) var(--sp-4);
  margin: 0;
}

.setting-k {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.setting-v {
  margin: 0;
  font-size: var(--fs-small);
  overflow-wrap: anywhere;
}

@media (max-width: 40rem) {
  .setting-list {
    grid-template-columns: minmax(0, 1fr);
    gap: 0 0;
  }
  .setting-v {
    margin-bottom: var(--sp-2);
  }
}

/* heatmap */

.heat {
  border-collapse: separate;
  border-spacing: 2px; /* the surface gap doing the separating, rather than borders on cells */
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

.heat th,
.heat td {
  padding: 0.4rem 0.55rem;
  text-align: center;
  font-weight: 400;
  white-space: nowrap;
}

.heat-corner,
.heat-row-h {
  text-align: left;
  font-weight: 600;
  color: var(--ink-soft);
  position: sticky;
  left: 0;
  background: var(--surface);
}

.heat-corner,
.heat-col,
.heat-total-h {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-faint);
}

.heat-cell {
  border-radius: var(--radius-sm);
  min-width: 2.6rem;
  color: var(--ink);
}
/* Bin 0 gets no fill on purpose — an empty cell means "nothing was tagged", which is not the
   palest amount of something. */
.heat-cell[data-bin="0"] {
  background: transparent;
  color: var(--ink-faint);
}
.heat-cell[data-bin="1"] {
  background: var(--heat-1);
}
.heat-cell[data-bin="2"] {
  background: var(--heat-2);
}
.heat-cell[data-bin="3"] {
  background: var(--heat-3);
}
.heat-cell[data-bin="4"] {
  background: var(--heat-4);
}

.heat-total {
  font-weight: 650;
  color: var(--ink);
  border-left: 1px solid var(--hairline);
}

/* scale legend — prints the real count range each step stands for, not "low..high" */

.scale {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  align-items: center;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.scale-label {
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}

.scale-step {
  display: inline-flex;
  gap: 0.35rem;
  align-items: center;
}

.scale-swatch {
  width: 0.9rem;
  height: 0.9rem;
  border-radius: 3px;
  border: 1px solid var(--hairline);
}
.scale-swatch[data-bin="0"] {
  background: transparent;
}
.scale-swatch[data-bin="1"] {
  background: var(--heat-1);
}
.scale-swatch[data-bin="2"] {
  background: var(--heat-2);
}
.scale-swatch[data-bin="3"] {
  background: var(--heat-3);
}
.scale-swatch[data-bin="4"] {
  background: var(--heat-4);
}

/* funnel */

.funnel {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.funnel-row {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.funnel-label {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
}

.funnel-track {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
}

.funnel-bar {
  height: 1.4rem;
  /* Square where it grows from the baseline, 4px rounded at the data end. */
  border-radius: 0 4px 4px 0;
  min-width: 2px;
}
.funnel-bar[data-step="1"] {
  background: var(--funnel-1);
}
.funnel-bar[data-step="2"] {
  background: var(--funnel-2);
}
.funnel-bar[data-step="3"] {
  background: var(--funnel-3);
}

/* Value at the tip, in a text token — the bar carries the colour, the number does not. */
.funnel-value {
  font-size: var(--fs-small);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.funnel-rate {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* ================================================================================================
 * THE REVIEWS SECTION.
 *
 * Three bands, in the order a manager asks the questions: four figures (how are guests rating us),
 * the distribution beside the topics (what do they mention), then the latest strip (what did they
 * say). Everything else this panel used to carry — the conversion funnel, the per-month invites
 * table, the ranked "what to do first" list, the whole by-topic board — either lives under Reports
 * or is one click behind "View all topics". None of it was deleted; it stopped being the headline.
 *
 * The palette is the page's existing one and no new colour is introduced: the KPI and topic marks
 * reuse the measured status washes (--ok-bg/--ok-ink and siblings) that the Overview's card marks
 * already use, and the stars stay --brand-ink champagne. Every figure is text as well as shape —
 * a bar length, a star fill and a chip hue are each restated as a number beside them.
 * ============================================================================================= */

/* Reviews' own grid. Not `.ov-grid`: that goes to three columns at 86rem, and this section's shape
   is two equal panels under a four-up figure row — a third column would leave the distribution and
   the topics on thirds with dead space beside them. `stretch` so the two read across on one
   baseline, which is what puts their headings and their footers on shared lines. */
.rv-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: stretch;
}

@media (min-width: 60rem) {
  .rv-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The two rows that are rows rather than panels sharing one. `1 / -1` on the item itself rather than
   borrowing the Overview's `.ov-full`: that class belongs to that section's grid vocabulary, and a
   Reviews card reaching into it is a coupling that breaks silently if either grid changes. */
.rv-kpis-wrap,
.rv-latest {
  grid-column: 1 / -1;
}

/* --- the four figures ------------------------------------------------------------------------ */

/* `auto-fit` with a 13rem floor rather than a hard `repeat(4, 1fr)`: four cards holding a 27px
   figure and two lines of label need ~13rem each, so the row folds to 2x2 on a tablet and to one
   column on a phone without a breakpoint per step. `minmax(min(13rem, 100%), 1fr)` — the `min()` is
   what stops the floor from overflowing a viewport narrower than the floor itself. */
.rv-kpis {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
}

/* A KPI card is the page's card, one step quieter: same face, same hairline, same radius, no
   hover lift — these are readings, not surfaces you open. */
.rv-kpi {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  justify-content: space-between;
  min-width: 0;
  padding: var(--sp-5);
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}

.rv-kpi-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}

.rv-kpi-k {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink-soft);
}

/* The figure. Display-cut and tabular, like every other headline number on this page, so a 4.1 and
   a 128 in adjacent cards sit on the same stem. */
.rv-kpi-v {
  margin: 0;
  font-family: var(--font-display);
  font-size: calc(30px * var(--type-scale));
  font-weight: 500;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}

/* The comparison chip. Colour is the third encoding, never the first: the arrow says the direction
   and the number carries its own sign, so this survives greyscale and forced colours. */
.rv-kpi-move {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.35rem;
  align-items: baseline;
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.rv-kpi-move .icon {
  width: 0.9rem;
  height: 0.9rem;
  align-self: center;
}

.rv-kpi-move-v {
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.rv-kpi-move[data-tone="ok"] .icon,
.rv-kpi-move[data-tone="ok"] .rv-kpi-move-v {
  color: var(--ok-ink);
}
.rv-kpi-move[data-tone="danger"] .icon,
.rv-kpi-move[data-tone="danger"] .rv-kpi-move-v {
  color: var(--danger-ink);
}
.rv-kpi-move[data-tone="flat"] .icon,
.rv-kpi-move[data-tone="flat"] .rv-kpi-move-v {
  color: var(--ink-soft);
}

/* The line a card prints INSTEAD of a chip, when there is nothing to compare against. Same size and
   ink as the chip's trailing words, so the row of four keeps one baseline either way. */
.rv-kpi-hint {
  margin: 0;
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--ink-faint);
}

/* ------------------------------------------------------------------------------------------------
 * The tinted circular mark — on a KPI card and on a topic row.
 *
 * The same device the Overview's card headings use (`#ov-grid .viz-mark`), and deliberately its own
 * class rather than that selector widened: those rules are scoped to `#ov-grid` on purpose, and
 * unscoping them would restyle Reports and Alerts, which have no marks in their markup and were not
 * part of this. Every pair below is one of this page's existing measured washes, so no new colour
 * was introduced — and the glyph is never load-bearing: every card and row says in words what its
 * mark says in colour.
 * ---------------------------------------------------------------------------------------------- */
.rv-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(40px * var(--type-scale));
  height: calc(40px * var(--type-scale));
  border-radius: 50%;
  /* The neutral fallback, so a mark with an unknown tone is still a circle rather than a bare glyph
     floating beside the label — which is what a new `stay_review.ASPECTS` key would otherwise get. */
  background: var(--surface-2);
  color: var(--ink-soft);
}

.rv-mark .icon {
  width: 19px;
  height: 19px;
}

.rv-mark-sm {
  width: calc(30px * var(--type-scale));
  height: calc(30px * var(--type-scale));
}

.rv-mark-sm .icon {
  width: 15px;
  height: 15px;
}

.rv-mark[data-tone="ok"] {
  background: var(--ok-bg);
  color: var(--ok-ink);
}
.rv-mark[data-tone="warn"] {
  background: var(--warn-bg);
  color: var(--warn-ink);
}
.rv-mark[data-tone="danger"] {
  background: var(--danger-bg);
  color: var(--danger-ink);
}
.rv-mark[data-tone="brand"] {
  background: var(--brand-wash-strong);
  color: var(--brand-ink);
}
/* Info: the palest step of the chart ramp — this page's existing pale blue — under the same navy
   ink `brand` takes. Distinguished from `brand` by lightness as well as hue, so it survives
   greyscale. */
.rv-mark[data-tone="info"] {
  background: var(--heat-1);
  color: var(--brand-ink);
}
.rv-mark[data-tone="gold"] {
  background: var(--champagne-wash-strong);
  color: var(--warn-ink);
}
.rv-mark[data-tone="accent"] {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
}

/* --- ratings distribution -------------------------------------------------------------------- */

.rv-dist-count {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-faint);
}

.rv-dist {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
}

/* Band label, track, then the count — three columns so five rows share one grid and the bars all
   start and end on the same two lines. `auto` on the last column rather than a fixed width: the
   count text is tabular, so it sizes itself and never clips a four-figure property. */
.rv-dist-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--sp-3);
  align-items: center;
}

.rv-dist-band {
  display: flex;
  gap: 0.2rem;
  align-items: center;
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

.rv-dist-band .icon {
  width: 0.85rem;
  height: 0.85rem;
  color: var(--star-ink);
}

/* The track is the empty half of the bar and carries its own wash, so a band with no ratings still
   draws a full-width rule rather than vanishing — an absent row reads as a missing rating, and the
   count beside it says 0. */
.rv-dist-track {
  position: relative;
  min-width: 0;
  height: 9px;
  border-radius: var(--radius-pill);
  background: var(--champagne-wash);
  overflow: hidden;
}

.rv-dist-bar {
  height: 100%;
  width: var(--w);
  border-radius: var(--radius-pill);
  /* The bars are the rating scale, so they take the star colour and the track takes the champagne
     wash under it — one material, two weights, rather than a blue bar under gold glyphs. */
  background: var(--star-ink);
  animation: viz-grow-x 520ms var(--ease) both;
}

.rv-dist-n {
  display: flex;
  gap: 0.35rem;
  align-items: baseline;
  margin: 0;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.rv-dist-n-v {
  font-weight: 650;
}

.rv-dist-n-p {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* The average, at the foot of the card. `margin-top: auto` is what pins it to the bottom so it sits
   level with the topics card's own footer beside it — both cards are column flexes stretched to a
   shared height by `.rv-grid`. */
.rv-dist-avg {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  justify-content: space-between;
  margin: auto 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-small);
}

.rv-dist-avg-k {
  color: var(--ink-soft);
}

.rv-dist-avg-v {
  display: inline-flex;
  gap: 0.25rem;
  align-items: center;
  padding: 2px var(--sp-2);
  border-radius: var(--radius-pill);
  background: var(--champagne-wash-strong);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.rv-dist-avg-v .icon {
  width: 0.85rem;
  height: 0.85rem;
  color: var(--star-ink);
}

/* --- top topics ------------------------------------------------------------------------------ */

.rv-topics {
  list-style: none;
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
}

/* Mark, topic, rating, count — four columns, so five rows line up down every one of them. The rating
   column is `auto` and the count `auto`: both are tabular and size themselves, and the topic takes
   what is left. Hairlines between rows rather than around them: five bordered boxes in a card is a
   table pretending to be cards. */
.rv-topic {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) 0;
}

.rv-topic + .rv-topic {
  border-top: 1px solid var(--hairline);
}

.rv-topic-k {
  margin: 0;
  min-width: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.rv-topic-rating {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  margin: 0;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.rv-topic-rating-v {
  font-weight: 650;
}

.rv-topic-n {
  margin: 0;
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* "A rating is about the stay, not about the aspect" — the server ships this sentence and the card
   prints it, because "Food scored 3.8" is not a claim this data supports. */
.rv-topics-hint {
  margin: var(--sp-3) 0 0;
}

.rv-topics-all,
.rv-all {
  align-self: flex-start;
  font-size: var(--fs-small);
}

.rv-topics-all {
  margin-top: auto;
}

/* --- the latest-reviews strip ---------------------------------------------------------------- */

/* Prev/next, in the card's head beside the heading. */
.rv-steps {
  display: flex;
  flex: none;
  gap: var(--sp-2);
}

.rv-step[disabled] {
  opacity: 0.4;
  cursor: default;
}

/* Four across on a wide screen, folding to two then one. `auto-fit` with an 15rem floor rather than
   a scroller: a horizontally-scrolling strip inside a page that already scrolls vertically is two
   axes to manage, and the prev/next pair is a better control than a swipe nobody discovers. This is
   also what keeps the section off a horizontal scrollbar at every width — see the `min()` note on
   `.rv-kpis`. */
.rv-strip {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  align-items: stretch;
}

/* The sheet's copy of the strip holds every write-up in the window, so it wraps at a prose measure
   rather than to four columns — and its cards are not height-matched, because a full write-up is as
   long as it is. */
.rv-strip-all {
  grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr));
  align-items: start;
  margin-top: var(--sp-3);
}

/* A review card. The recessed surface (--surface-2) rather than the card face, because it sits
   INSIDE a card: same-on-same would leave four invisible panes.
 *
 * The left stripe restates the stars, which are themselves restated as text — a third encoding of a
 * value that is never colour-only. 1 and 2 stars are the ones a manager must not scroll past. */
.rv-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
  padding: var(--sp-4);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--hairline-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.rv-card[data-rating="1"],
.rv-card[data-rating="2"] {
  border-left-color: var(--viz-neg);
}
.rv-card[data-rating="5"] {
  border-left-color: var(--viz-pos);
}

.rv-card-top {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  min-width: 0;
}

/* The guest's initials on a lavender disc — the same mark the Guests board uses (`.gst-avatar`), and
   its own class only because that one is sized for a table row. */
.rv-av {
  display: grid;
  flex: none;
  place-items: center;
  width: calc(34px * var(--type-scale));
  height: calc(34px * var(--type-scale));
  border-radius: 50%;
  background: var(--brand-wash-strong);
  color: var(--brand-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.rv-who {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  min-width: 0;
}

.rv-name {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 650;
  overflow-wrap: anywhere;
}

.rv-when {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* The date sits at the card's right edge, which is where the reference puts it. `margin-left: auto`
   rather than `justify-content: space-between` on the row, so the avatar and the name stay together
   as one group when the name wraps. */
.rv-date {
  flex: none;
  margin-left: auto;
  white-space: nowrap;
}

/* Clamped, not truncated. Clipping the string in JS would throw the rest of a guest's sentence
   away; four lines here keeps it whole, selectable and findable, and the sheet shows it in full.
   `max-width` matches `.viz-sub`'s measure for the one-column case. */
.rv-text {
  margin: 0;
  max-width: 62ch;
  font-size: var(--fs-small);
  line-height: 1.55;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
}

/* In the sheet a card is the record, not a preview, so the prose is not clamped there. */
.rv-strip-all .rv-text {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

/* The aspect chips are pushed to the card's foot so four cards of unequal prose still line their
   tags up — the strip is height-matched by `align-items: stretch` above. */
.rv-card > .review-aspects {
  margin-top: auto;
}

/* --- the strip's footer: count, dots, "View all reviews" ------------------------------------- */

.rv-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
  justify-content: space-between;
  margin-top: var(--sp-4);
}

.rv-count {
  margin: 0;
  min-width: 0;
}

.rv-dots {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  /* Centred between the count and the button, which is where a pagination indicator belongs — and
     `auto` margins rather than `justify-content` so it stays centred as the other two change width. */
  margin-inline: auto;
}

/* A dot is a real button and has to be reachable: WCAG 2.5.8 wants 24px, and an 8px pip cannot get
   there on its own. So the pip is drawn with `::after` and the button around it is the target. */
.rv-dot {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.rv-dot::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--hairline-strong);
  transition: background var(--dur) var(--ease);
}

.rv-dot:hover::after {
  background: var(--ink-faint);
}

/* The page you are on. Size AND fill, not fill alone — a hue-only difference between two 8px pips is
   not a difference on a projector or in greyscale, and `aria-current` carries it for a screen
   reader either way. */
.rv-dot[aria-current="true"]::after {
  width: 10px;
  height: 10px;
  background: var(--brand-ink);
}

/* --- the section's own footnotes ------------------------------------------------------------- */

/* Where the prose comes from, once for the whole strip rather than on all four cards. Quiet but
   never hidden: the write-ups are summarised from the call and are never the guest's exact words,
   and this is the line that stops a manager reading one as a quote. Borrows the cards' inset so it
   lines up with their text instead of hanging off the page's left edge. */
.rv-note {
  margin: var(--sp-4) 0 0;
  padding-inline: var(--sp-5);
  max-width: 78ch;
  font-size: var(--fs-micro);
  font-style: italic;
  line-height: 1.5;
  color: var(--ink-faint);
}

.rv-unsummarised {
  padding-inline: var(--sp-5);
}

.rv-unsummarised:empty {
  display: none;
}

/* --- Reviews at narrow widths ---------------------------------------------------------------- */

/* Everything above already folds on its own — `.rv-grid` at 60rem, and the three `auto-fit` grids
   by content — so these are the two places where content, not layout, runs out of room. Nothing
   here scrolls sideways: every grid floors with `minmax(min(Xrem, 100%), 1fr)`, so the tracks
   collapse with the viewport instead of forcing the page wider than it. */
@media (max-width: 34rem) {
  /* A topic row's four columns come to ~19rem before the label has any room. Two rows instead: the
     mark and the topic on the first, the rating and the count on the second, indented past the mark
     so the second line reads as belonging to the first rather than as a row of its own. */
  .rv-topic {
    grid-template-columns: auto minmax(0, 1fr);
    row-gap: var(--sp-2);
  }

  .rv-topic-rating {
    grid-column: 2;
    justify-content: flex-start;
  }

  .rv-topic-n {
    grid-column: 2;
  }

  /* The strip's footer stacks, and the dots lose their auto margins — centred between two items that
     are now above and below them is not centred at all. */
  .rv-foot {
    flex-direction: column;
    align-items: flex-start;
  }

  .rv-dots {
    margin-inline: 0;
  }

  /* The footnotes lose the cards' inset: at this width the cards are near flush with the page edge,
     so borrowing --sp-5 indents them past the text they qualify. */
  .rv-note,
  .rv-unsummarised {
    padding-inline: 0;
  }
}

.stars {
  display: flex;
  flex-wrap: wrap;
  gap: 0.1rem var(--sp-2);
  align-items: center;
  margin: 0;
}

/* The rating band. Wraps to its own line in a 20rem card rather than squeezing the stars, which is
   why `.stars` above flex-wraps — the row was `nowrap` when it held only five glyphs and a number. */
.stars-band {
  font-size: var(--fs-micro);
  color: var(--ink-soft);
}

.stars-marks {
  display: inline-flex;
  gap: 0.1rem;
  /* Champagne brass, not the navy --brand-ink these used to take: a star rating is the one figure a
     reader expects to be gold, and --star-ink is that hue at a lightness the cream canvas can carry.
     See the token's note for why the champagne fills themselves cannot be used here. */
  color: var(--star-ink);
}

/* The span carries the size (chat.css `.icon`); overriding it here keeps five stars on one line
   next to `--fs-micro` text instead of at the 19px control size. */
.stars-marks .icon {
  width: 0.95rem;
  height: 0.95rem;
}

/* One path, filled or not — same glyph either way, so the difference is weight and not shape. */
.star-on > svg {
  fill: currentColor;
}
.star-off {
  opacity: 0.4;
}

.stars-text {
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

/* Provenance, on every card. Quiet but never hidden: the prose above is summarised from the call,
   not quoted, and this is the line that stops a manager reading it as the guest's own words. It
   sits directly under the text it qualifies rather than in the head, where it would read as
   metadata about the call instead of a caveat about the sentence. */
.review-provenance {
  margin: 0;
  font-size: var(--fs-micro);
  font-style: italic;
  color: var(--ink-faint);
}

/* The aspects the guest actually raised, from `stay_review.aspects_in`. A tag here is grounded —
   the compose step rejects a write-up that names a concrete aspect the guest never mentioned — so
   these are safe to skim as "what this call was about".
 *
 * Not `.pill`: that is a single status badge and carries `margin-left: auto`, which in a flex row
 * shoves the first chip to the right edge and strands the rest. It is also uppercase and 700, which
 * at five chips per card outweighs the review text they annotate. */
.review-aspects {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
}

.review-aspects > li {
  font-size: var(--fs-micro);
  /* --ink-soft, not --ink-faint, for the reason given at the heat-legend rule below: --fs-micro at
     --ink-faint's contrast survives a monitor and disappears on a projector. */
  color: var(--ink-soft);
  padding: 2px var(--sp-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

/* The calls that produced no write-up. A `<details>` because the count is the point and the reasons
   are the follow-up — and because it must stay on screen in a window with zero cards, where it is
   the only thing distinguishing "we made no calls" from "the rails rejected every one". */
.reviews-unsummarised {
  margin-top: var(--sp-3);
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.reviews-unsummarised > summary {
  cursor: pointer;
  /* WCAG 2.5.8: the same 24px floor as .table-toggle, which --fs-micro text alone does not reach. */
  min-height: 24px;
  display: flex;
  align-items: center;
}

/* `summary` is now part of the shared focus rule near the top of this file, so this only has to
   add the radius the ring should follow. */
.reviews-unsummarised > summary:focus-visible {
  border-radius: var(--radius-sm);
}

.reviews-reasons {
  list-style: none;
  margin: var(--sp-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

/* "Normal, and not a figure to improve." Capped at the same measure as `.viz-sub` because it is a
   paragraph to read, not a label to scan — and it has to actually be read, since it is what stops
   the count above from being treated as a score. */
.reviews-unsummarised-note {
  margin: var(--sp-2) 0 0;
  max-width: 62ch;
  line-height: 1.55;
  color: var(--ink-soft);
}

/* table twins */

/* min-height for the same WCAG 2.5.8 reason as .link-btn: --fs-micro against chat.css's shared .btn
   padding measured 21px tall, under the 24px floor. Set here rather than on .btn, which is shared
   with the guest app. */
.table-toggle {
  align-self: flex-start;
  min-height: 24px;
  font-size: var(--fs-micro);
}

/* Same deal as .viz-scroll: `contain` stops a sideways swipe on a table turning into the browser's
   back gesture. */
.viz-table {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.data-table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--fs-small);
}

.data-table caption {
  text-align: left;
}

.data-table th,
.data-table td {
  padding: 0.35rem var(--sp-2);
  border-bottom: 1px solid var(--hairline);
  text-align: left;
  white-space: nowrap;
}

/* On --surface-2 with a strong bottom rule, so the header reads as a header rather than as the
   first row set in small caps. --ink-soft over --ink-faint for the same projector-legibility reason
   as every other micro-label on the page, and --track-micro so it matches them. */
.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-soft);
  border-bottom: 1px solid var(--hairline-strong);
}

.data-table tbody th {
  font-weight: 600;
}

/* Digits align in columns here, so tabular figures are right. */
.data-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ===========================================================================================
 * Narrow: the rail becomes an off-canvas drawer behind a hamburger.
 *
 * Not laid down into horizontal strips: a long scroller gives no sense of where you are among
 * thirteen sections, and turning a vertical tablist horizontal makes both
 * `aria-orientation="vertical"` and page.js's roving Up/Down tabindex describe an axis that no
 * longer exists. A drawer keeps the rail vertical, so the markup and keyboard model stay true.
 *
 * The trap is `inert` on the two siblings, set from page.js — the technique dialog.js uses, and
 * the reason there is no hand-rolled Tab cycle here.
 * ======================================================================================== */

/* ===========================================================================================
 * The hero stacks BEFORE the rail becomes a drawer, and this breakpoint exists because of a
 * measured break rather than a round number.
 *
 * The desktop hero puts the greeting and the control strip on one row, `1fr auto`. The control strip
 * is a fixed ~500px (property pill, day, bell, Refresh), so the greeting's column is whatever is
 * left — and at 1024px, with the 264px rail still in place, that left 155px. Measured: the title set
 * to five lines in a 155px column beside a 502px strip. The rail does not become a drawer until
 * 62rem, so nothing rescued it before this.
 *
 * From here down the hero takes the SAME composition the tablet does: chrome pinned top-right,
 * elastic space, then the greeting directly above the summary strip at the foot. Both get the full
 * width, and the greeting lands on the conifer ridge — which is the easiest part of this frame to
 * light. See --hero-scrim below.
 * ======================================================================================== */

@media (max-width: 78rem) {
  /* The type pool flips to a BOTTOM RAMP, because the type moves — and here that is a straight win
     rather than a compromise.
   *
     The greeting now sits over the CONIFER RIDGE, the darkest region in the frame: measured over the
     band's bottom 30%, the brightest pixel runs 0.05 to 0.22, where ivory type needs at most 0.10 of
     scrim to clear 4.5:1. The desktop pool needs 0.50 because it is lighting sky; this ramp is
     lighting shadow.
   *
     So the alphas are set for margin, not necessity: 0.58 at the foot, clear by 56%. The whole top
     half of the picture — sky, the full range, the alpenglow — is left completely untouched at this
     width, which is more unveiled photograph than the desktop composition gets.
   *
     The pills need nothing from this: they are opaque ivory surfaces (see the chrome note). */
  :root {
    --hero-scrim: linear-gradient(
      to top,
      rgba(var(--hero-scrim-rgb), 0.58) 0%,
      rgba(var(--hero-scrim-rgb), 0.44) 26%,
      rgba(var(--hero-scrim-rgb), 0.18) 42%,
      rgba(var(--hero-scrim-rgb), 0) 56%
    );
  }

  /* Four rows: [hamburger | chrome] at the top, elastic space, the greeting, the strip. The first
     column is `auto` and holds the hamburger, which only exists below 62rem — above that it is
     `display: none`, the column collapses to zero, and the chrome simply spans the full width. One
     layout serving both ranges rather than two nearly-identical ones.
   *
     A grid rather than the flex column the other sections use, for the same reason as desktop: the
     strip has to span both columns, and `margin-bottom: auto` on the hamburger cannot express "these
     two share a line and those two share the foot".
   *
     TALLER than the 30vh the ivory hero used, not shorter. That hero was type over a dissolve and the
     content below mattered more than the view; this one carries the five operational figures inside
     it, so the same height would leave no photograph between the pills and the strip. 26rem floors it
     at ~416px on a 1024-tall tablet, of which the top ~44% is clear picture. */
  .mgr-top.mgr-top--overview {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto 1fr auto auto;
    min-height: clamp(26rem, 46vh, 34rem);
    align-items: start;
    column-gap: var(--sp-3);
    row-gap: var(--sp-4);
    /* X, not Y, is what matters from here down: the box is narrower than the band's 2.90:1, so `cover`
       scales by HEIGHT and crops horizontally instead. 78% holds the main range and keeps the lodge's
       lit shoulder in frame; 50% would centre on the range and cut the lodge off entirely. The band's
       full height is shown, which is why the ridge is reliably at the foot. */
    background-position: 78% 40%;
  }

  /* `width: auto` against the `.mgr-chrome { width: 100% }` the ≤62rem block sets: that rule is for
     the flex column the other sections use, where the strip has to fill its line. Here it is a grid
     item and has to stay its own width or `justify-self` has nothing to move. */
  .mgr-top--overview .mgr-chrome {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    width: auto;
    justify-content: flex-end;
  }

  .mgr-top--overview .view-head {
    grid-column: 1 / -1;
    grid-row: 3;
  }

  .mgr-top--overview .hero-strip {
    grid-column: 1 / -1;
    grid-row: 4;
  }

  /* Five cells will not sit side by side in a 745px hero without the hint lines becoming unreadable,
     so the strip becomes a scroller rather than a squeeze: one row, each cell at least 10.5rem,
     snapping so a swipe lands on a cell boundary. It stops scrolling on its own as soon as the hero
     can hold all five (about 56rem of strip), because `1fr` lets the columns grow past the floor.
   *
     Wrapping to 3-then-2 was the alternative and is worse: the dividers are inline-start rules, so a
     wrapped row would draw one down the left edge of the fourth cell, and five figures in two unequal
     rows stops reading as one instrument. */
  .hero-strip {
    grid-auto-flow: column;
    grid-template-columns: none;
    grid-auto-columns: minmax(10.5rem, 1fr);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
  }

  .hero-cell {
    padding: var(--sp-4);
    scroll-snap-align: start;
  }
}

@media (max-width: 62rem) {
  .mgr {
    grid-template-columns: minmax(0, 1fr);
    /* Same empty-third-row trap as the desktop grid, and worse here: the drawer is `position: fixed`
       so it is not a grid item at all, leaving only two in-flow children for three tracks. */
    grid-template-rows: auto 1fr;
  }

  .nav-toggle {
    display: inline-flex;
  }

  .mgr-side {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 60;
    height: 100dvh;
    width: min(19rem, 84vw);
    /* Off-canvas by default, and `visibility` rather than `display` so the slide can animate and so
       a closed drawer is not reachable by Tab even before page.js sets `inert`. */
    transform: translateX(-100%);
    visibility: hidden;
    overflow-y: auto;
    border-right: 1px solid var(--header-line);
    box-shadow: none;
    transition:
      transform var(--dur) var(--ease),
      visibility var(--dur) step-end;
  }

  body.nav-open .mgr-side {
    transform: none;
    visibility: visible;
    box-shadow: var(--shadow-3);
    transition:
      transform var(--dur) var(--ease),
      visibility 0s;
  }

  /* The drawer scrolls as one column; the nav lists do not scroll inside it.
   *
     On desktop the rail is a fixed-height column and `.side-nav { overflow-y: auto }` plus
     `flex: 1 1 auto` on the primary group is right — the list scrolls inside a rail that cannot grow.
     In the drawer that same pair squeezed the primary group to whatever was left after the lockup and
     footer and clipped it mid-row: "Knowledge" was sliced in half and "Settings" was unreachable
     inside a nested scroller with no scrollbar. Natural height plus the drawer's own scroll instead. */
  .side-nav {
    overflow-y: visible;
  }
  .side-nav[aria-labelledby="nav-primary-h"] {
    flex: none;
  }

  .nav-scrim {
    position: fixed;
    inset: 0;
    z-index: 55;
    background: rgba(10, 30, 28, 0.55);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
  }

  /* The hero becomes a column, and this is a correctness fix as much as a layout one.
   *
   * As a wrapping row it laid out [hamburger | view-head] on the first line under
   * `justify-content: space-between`, which threw the title to the RIGHT edge — and because the
   * chrome then wrapped to its own line, `align-items: flex-end` bottom-aligned the title within
   * its own short line rather than within the hero. Measured at 768px the title landed at 26% of
   * the hero's height: straight on the photograph, where the dissolve is still ~5% opaque and the
   * deep forest ink the hero now uses has nothing to sit on.
   *
   * As a column it stacks honestly: hamburger pinned top-left by `margin-bottom: auto`, then the
   * title and the controls pushed down into the cream foot where the ink is legible — the same
   * composition as desktop, one axis narrower.
   *
   * The photo also gets less height here: on a tablet the content below matters more than the
   * view. The dissolve is proportional, so it lands correctly at any height. */
  .mgr-top {
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-3);
    padding: var(--sp-5) var(--sp-4) var(--sp-4);
  }

  /* The hamburger appears at this width, so it takes the first cell of the hero's top row — the row
     the ≤78rem block above set up for the chrome. Everything else about that layout already holds. */
  .mgr-top--overview .nav-toggle {
    grid-column: 1;
    grid-row: 1;
    margin-bottom: 0;
  }

  .nav-toggle {
    align-self: flex-start;
    margin-bottom: auto;
  }

  .mgr-chrome {
    width: 100%;
  }

  /* Page inset comes back a step: at this width --sp-6 either side was a tenth of the viewport
     spent on margin. */
  .mgr-insights {
    padding: var(--sp-4) var(--sp-4) var(--sp-6);
    gap: var(--sp-4);
  }
}

/* ===========================================================================================
 * Phone: the hero stops wrapping and starts laying itself out.
 *
 * `--sp-6 --sp-5` of hero padding spends a quarter of a phone viewport before the first number, and
 * a `flex-wrap` control strip broke into whatever lines the label widths happened to produce — over
 * the photo, at three different widths. Instead: the property picker takes a full line, then
 * Refresh takes the next — the actor chip that used to share it now lives in the rail.
 * ======================================================================================== */

@media (max-width: 40rem) {
  :root {
    /* The smaller, reframed candidate — a phone hero never renders anywhere near 1600px wide, and
       more importantly the wide band is the wrong FRAMING here: at roughly 1:1 `cover` on a 2.90:1
       band scales by height and shows a ~35%-wide vertical sliver of it. hero-lodge-sm.jpg is 1.49:1
       and framed on peak-plus-lodge, so a phone gets the range and the property rather than a
       detail of one of them. See scripts/make-hero-lodge.ps1. */
    --hero-photo: url("/img/hero-lodge-sm.jpg");

    /* The bottom ramp deepens, and unlike the previous hero's phone block that is a small change
       rather than a rewrite.
     *
       What made the old phone case hard was that the type was DARK: it needed a near-opaque cream
       band (0.97 through the text, holding 0.95 to nearly half the box), which is a stain rather
       than a veil, and the whole phone composition had to be arranged around minimising it. Light
       type has the opposite problem, and this frame solves it for free: the sm crop's foot is the
       same conifer ridge as the wide band, luminance 0.02-0.14, and the greeting sits on it.
     *
       0.66 rather than the ≤62rem block's 0.58, and clearing at 62% rather than 56%, because the
       text stack is taller relative to a phone hero (the subtitle wraps to three lines at 390px)
       and because `cover` at ~1:1 shows the sm crop's full height, which puts a little more lit
       snow into the ramp's tail than the tablet case does. Both numbers still leave the top third
       of the picture completely clear. */
    --hero-scrim: linear-gradient(
      to top,
      rgba(var(--hero-scrim-rgb), 0.66) 0%,
      rgba(var(--hero-scrim-rgb), 0.52) 30%,
      rgba(var(--hero-scrim-rgb), 0.22) 46%,
      rgba(var(--hero-scrim-rgb), 0) 62%
    );
  }

  .mgr-top {
    padding: var(--sp-5) var(--sp-3) var(--sp-4);
  }

  /* A phone hero keeps real picture height — proportionally MORE than desktop, not less. Measured at
     375x812: the hamburger takes the top 92px, the control strip wraps to two rows of pills (108px),
     the greeting stack is another 106px and the summary strip 86px, all of it anchored to the two
     ends. What is left is the ~108px band above the pills, and that band is where `cover` on the sm
     crop puts the sky and the summit — so it is the part of the frame worth keeping.
   *
     58vh (471px at 812) is what buys that band. The 48vh the previous hero used would close it
     entirely, because that hero carried no figures inside it: the strip alone is 119px with its
     inset. Anything below the 28rem floor and the pills meet the greeting.
   *
     Two classes, not one: the ≤62rem block's floor is `.mgr-top.mgr-top--overview`, so a
     single-class selector here loses to it and this override would silently do nothing. */
  .mgr-top.mgr-top--overview {
    min-height: clamp(28rem, 58vh, 36rem);
    /* The chrome needs the whole width at this size (the pills go full-bleed below), so it drops
       under the hamburger instead of sharing its line. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto 1fr auto auto;
  }

  .mgr-top--overview .nav-toggle {
    grid-row: 1;
  }
  .mgr-top--overview .mgr-chrome {
    grid-column: 1;
    grid-row: 2;
    justify-self: stretch;
  }
  .mgr-top--overview .view-head {
    grid-row: 4;
  }
  .mgr-top--overview .hero-strip {
    grid-row: 5;
  }

  /* Narrower cells so two are in view at once and the swipe is short — five cells at the tablet's
     10.5rem floor is a lot of travel on a phone. The badge goes; at this width it was taking a
     third of a cell to repeat what the label says. */
  .hero-strip {
    grid-auto-columns: minmax(9rem, 1fr);
  }

  .hero-cell {
    padding: var(--sp-3) var(--sp-4);
  }

  .hero-cell-badge {
    display: none;
  }

  .mgr-insights {
    padding: var(--sp-4) var(--sp-3) var(--sp-6);
  }

  .prop-switch,
  .mgr-ident {
    flex: 1 1 100%;
  }

  .prop-select {
    flex: 1 1 auto;
    min-width: 0;
    max-width: none;
  }

  .mgr-chrome .btn {
    flex: 1 1 0;
    justify-content: center;
  }
}

/* ===========================================================================================
 * Touch targets
 *
 * A 44px minimum wherever the pointer is coarse or the viewport is narrow — the same controls at
 * their desktop size are fine under a mouse, so this is not applied globally. Where 44px would
 * distort a deliberately small control (the inline link buttons) the hit area is grown with a
 * transparent overlay and the visual box left alone.
 * ======================================================================================== */

@media (pointer: coarse), (max-width: 62rem) {
  :root {
    --touch: 44px;
  }

  .skip-link,
  .nav-item,
  .side-theme,
  .side-logout,
  .filter,
  .btn,
  .prop-select {
    min-height: var(--touch);
  }

  /* These two were sized off their label alone, so the added height needs matching width or they
     read as tall slots rather than pills. */
  .filter,
  .btn {
    padding-inline: var(--sp-3);
  }

  .mgr-chrome {
    --chrome-h: var(--touch);
  }

  .link-btn {
    position: relative;
  }
  .link-btn::after {
    content: "";
    position: absolute;
    top: 50%;
    left: calc(var(--sp-1) * -1);
    right: calc(var(--sp-1) * -1);
    height: var(--touch);
    transform: translateY(-50%);
  }
}

/* ===========================================================================================
 * Motion
 *
 * Three moments, not scattered effects:
 *
 *   1. **A sheet arriving.** The scrim fades and the panel rises a few pixels — spatial
 *      continuity, so the issue reads as coming forward out of the list rather than replacing it.
 *   2. **A figure being measured.** When the Overview or Insights paints, the NPS line draws left to
 *      right, its wash follows, the dots land as it passes them, and the bars grow from their
 *      baselines. It runs once per data change, which is the point: it marks *new numbers*, not
 *      page decoration.
 *   3. **Changing view.** The rail's pill takes its fill, the top bar's title and subtitle are
 *      replaced (page.js rebuilds those two nodes, so the entrance fires on the swap with no
 *      animation restarted from JS), and the panel rises in. One gesture ties the control pressed
 *      to the thing that changed.
 *
 * Everything else is a hover state. Deliberately NOT animated: issue cards (paintList runs on
 * every selection, so an entrance would re-fire on each click), sparklines (up to 30 rows drawing
 * at once is noise, not information), and heatmap cells individually (28 staggered fades read as
 * a loading screen). The whole block is off under prefers-reduced-motion.
 *
 * Also deliberately NOT animated: the page crossfading on a theme change. --header-bg is a
 * gradient and cannot transition, so every surface would fade except the rail. The knob's slide is
 * the feedback instead.
 * ======================================================================================== */

@keyframes viz-draw {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes viz-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes viz-pop {
  from {
    opacity: 0;
    transform: scale(0.4);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes viz-grow-y {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}

@keyframes viz-grow-x {
  from {
    width: 0;
  }
  to {
    width: var(--w);
  }
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes slide-in-x {
  from {
    opacity: 0;
    transform: translateX(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes scrim-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes panel-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.985);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --- modals ------------------------------------------------------------------------------- */

.dlg-scrim {
  animation: scrim-in var(--dur) var(--ease) both;
}

.dlg-panel,
.sheet-panel {
  animation: panel-in 260ms var(--ease) both;
}

/* --- the rail: one entrance, left to right ------------------------------------------------- */

.side-brand,
.side-group-h,
.nav-item {
  animation: slide-in-x 320ms var(--ease) both;
}
/* A per-item delay without thirteen literal rules. Capped by the last selector below, so a longer
   rail never turns the entrance into a queue you sit and watch. */
.nav-item:nth-child(1) { animation-delay: 40ms; }
.nav-item:nth-child(2) { animation-delay: 70ms; }
.nav-item:nth-child(3) { animation-delay: 100ms; }
.nav-item:nth-child(4) { animation-delay: 130ms; }
.nav-item:nth-child(5) { animation-delay: 160ms; }
.nav-item:nth-child(n + 6) { animation-delay: 180ms; }

/* --- a view becoming visible -------------------------------------------------------------- */

.mgr-insights:not([hidden]) {
  animation: rise 260ms var(--ease) both;
}

/* The title and subtitle are fresh nodes on every view change, so this fires on the swap. */
.view-title {
  animation: rise 280ms var(--ease) both;
}
.view-sub {
  animation: rise 280ms var(--ease) 50ms both;
}

/* --- the trend: line draws, wash follows, dots land ------------------------------------- */

.viz-line {
  /* pathLength="1" is set in insights.js, so 1 unit is the whole path regardless of geometry. */
  stroke-dasharray: 1;
  animation: viz-draw 900ms var(--ease) both;
}

.viz-wash {
  animation: viz-fade 500ms var(--ease) 300ms both;
}

.viz-dot {
  transform-box: fill-box;
  transform-origin: center;
  animation: viz-pop 260ms var(--ease) both;
  animation-delay: calc(260ms + var(--i, 0) * 90ms);
}

.viz-value {
  animation: viz-fade 400ms var(--ease) 700ms both;
}

/* --- diverging bars grow from the shared baseline ---------------------------------------- */

.viz-pos,
.viz-neg {
  transform-box: fill-box;
  animation: viz-grow-y 520ms var(--ease) both;
}
/* Each arm grows away from the baseline it starts at, so the pair opens outward. */
.viz-pos {
  transform-origin: bottom;
}
.viz-neg {
  transform-origin: top;
}

/* --- funnel and category bars grow from the left, in rank order -------------------------- */

.funnel-bar,
.cat-bar {
  width: var(--w);
  animation: viz-grow-x 620ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 110ms);
}

/* --- the donut's bands sweep in, in stacking order --------------------------------------- */

/* Each band is one dash on a shared circle, so it cannot be drawn by animating dashoffset — that
   property is what *positions* it. Scaling the band about the ring's centre reads as the same
   gesture and leaves the geometry alone. */
.donut-arc {
  transform-origin: center;
  animation: viz-fade 420ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 120ms);
}

.donut-total,
.donut-total-label {
  animation: viz-fade 400ms var(--ease) 360ms both;
}

/* --- the queue: rows arrive top-down, worst first ---------------------------------------- */

.queue-list > li {
  animation: rise 300ms var(--ease) both;
}
.queue-list > li:nth-child(2) {
  animation-delay: 50ms;
}
.queue-list > li:nth-child(3) {
  animation-delay: 100ms;
}
.queue-list > li:nth-child(4) {
  animation-delay: 150ms;
}

/* --- the heatmap fades as one block, not 28 cells ---------------------------------------- */

.heat {
  animation: viz-fade 420ms var(--ease) both;
}

/* --- hover micro-interactions ------------------------------------------------------------ */

/* A queue row slides toward the sheet it opens rather than lifting — it is a list of destinations,
   not a grid of cards. */
.queue-btn {
  transition:
    border-color var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}
.queue-btn:hover {
  transform: translateX(3px);
}
.queue-btn .icon {
  transition: transform var(--dur-fast) var(--ease);
}
.queue-btn:hover .icon {
  transform: translateX(2px);
}

/* Card and tile hover, and the restraint here is deliberate.
 *
 * Both used to lift 1-2px onto a brighter fill with a full rim-light and --shadow-3. On a page
 * where the pointer crosses a dozen panels on its way anywhere, that turned incidental mouse
 * movement into a rippling animation — and a card that visibly rises when you are not going to
 * click it is telling you something untrue.
 *
 * These panels are not buttons; only the controls *inside* them are. So hover is now the smallest
 * mark that still says "the pointer is here": the border warms toward green, and the contact
 * shadow deepens by one step. No transform, no fill change, no glow. The genuinely clickable
 * things (.queue-btn, .pb-group-btn) keep their own stronger states further up. */
.viz-card:hover {
  border-color: rgba(61, 98, 136, 0.3);
  box-shadow: var(--shadow-2);
}

.nav-item,
.side-theme,
.side-logout {
  transition:
    background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.record-table tbody tr {
  transition: background var(--dur-fast) var(--ease);
}
/* A nudge toward the panel it opens. Not on the selected item: it is already where it goes. */
.nav-item:hover:not([aria-selected="true"]) {
  transform: translateX(2px);
}

.side-theme-track {
  transition:
    background var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}
.side-theme-knob {
  transition:
    left var(--dur) var(--ease),
    background var(--dur) var(--ease);
}

.prop-select,
.side-account {
  transition:
    border-color var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease);
}
/* Inside the pill, so it is pill-shaped too — a rounded rectangle nested in a rounded pill reads
   as two controls rather than one. */
.prop-switch .prop-select {
  padding: 0.25rem var(--sp-3);
  border-radius: var(--radius-pill);
}

.prop-select:hover {
  border-color: var(--brand);
}

/* Motion is decoration here — every one of these conveys something that is also in the markup,
   so removing all of it costs nothing. `animation: none` rather than a 0s duration, so no
   animation-fill-mode leaves an element stuck at its `from` state. */
@media (prefers-reduced-motion: reduce) {
  .skip-link,
  [data-loading],
  .queue-btn,
  .queue-btn .icon,
  .viz-card,
  .nav-item,
  .side-theme,
  .side-theme-track,
  .side-theme-knob,
  .prop-select,
  .side-account,
  .side-logout,
  .rv-dot::after,
  .record-table tbody tr {
    transition: none;
  }

  /* The hover lift is motion, not just a transition — without this it still jumps, it only stops
     easing. The rim-light and shadow stay: those are colour, not movement. */
  .viz-card:hover {
    transform: none;
  }

  /* The drawer appears instead of sliding, and the hamburger swaps to an X with no rotation. The
     `visibility` step is kept so a closed drawer stays untabbable. */
  .mgr-side,
  body.nav-open .mgr-side,
  .nav-toggle-bars,
  .nav-toggle-bars::before,
  .nav-toggle-bars::after {
    transition: none;
  }

  .dlg-scrim,
  .dlg-panel,
  .sheet-panel,
  .side-brand,
  .side-group-h,
  .nav-item,
  .mgr-insights:not([hidden]),
  .view-title,
  .view-sub,
  .viz-line,
  .viz-wash,
  .viz-dot,
  .viz-value,
  .viz-pos,
  .viz-neg,
  .funnel-bar,
  .rv-dist-bar,
  .cat-bar,
  .donut-arc,
  .donut-total,
  .donut-total-label,
  .queue-list > li,
  .heat {
    animation: none;
  }

  .queue-btn:hover,
  .queue-btn:hover .icon,
  .nav-item:hover:not([aria-selected="true"]) {
    transform: none;
  }

  /* A toast still has to APPEAR and still has to leave — it is a notification, not decoration, so
     suppressing the animation outright would either flash it in with no transition at all or, worse,
     leave one on screen forever. Both keyframes are replaced with a plain opacity fade of the same
     duration, so the timers in toast.js still line up with what the eye sees. */
  .toast {
    animation: toast-fade-in 240ms var(--ease) both;
  }
  .toast[data-leaving] {
    animation: toast-fade-out 220ms var(--ease) both;
  }
  .toast--link {
    transition: none;
  }
}

/* The reduced-motion pair for toasts: no movement, no scale, same timings. */
@keyframes toast-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes toast-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* Forced colours: the heat fills and funnel steps stop carrying meaning, so fall back to the
   number in each cell plus a visible border per cell. Same for every `--brand` fill — the selected
   rail item, the badge, the avatar and the switch knob all need an outline to stay visible. */
@media (forced-colors: active) {
  .heat-cell,
  .scale-swatch,
  .legend-swatch {
    border: 1px solid CanvasText;
    forced-color-adjust: none;
  }
  .funnel-bar,
  .cat-bar,
  .split-swatch {
    border: 1px solid CanvasText;
  }

  /* The distribution's bars and its track are both washes, so both flatten to Canvas and five rows
     become five empty rules. Outlining the track and filling the bar with CanvasText keeps the
     comparison readable; the count and share beside every row carry the numbers regardless. */
  .rv-dist-track {
    border: 1px solid CanvasText;
  }
  .rv-dist-bar {
    background: CanvasText;
    forced-color-adjust: none;
  }

  /* The dots lose their fill, so the active page becomes indistinguishable. The size step survives
     on its own, and `Highlight` is the system's "this one" pair — same treatment `.seg-btn` gets. */
  .rv-dot::after {
    border: 1px solid CanvasText;
  }
  .rv-dot[aria-current="true"]::after {
    background: Highlight;
    forced-color-adjust: none;
  }
  /* The ring's bands stop being distinguishable; the legend beside it carries every number. */
  .donut-arc {
    forced-color-adjust: none;
  }
  .nav-item[aria-selected="true"] {
    border-color: CanvasText;
  }
  .nav-badge,
  .side-account-avatar,
  .side-theme-knob {
    border: 1px solid CanvasText;
  }

  /* The segmented control's selection is a navy fill, which forced colours discard — leaving three
     identical cells and no way to tell which window is active. `Highlight` is the system's own
     "selected" pair and is the only correct answer here. */
  .seg-btn[aria-pressed="true"] {
    background: Highlight;
    color: HighlightText;
    forced-color-adjust: none;
  }
  .viz-seg {
    border: 1px solid CanvasText;
  }

  /* The heading marks and the toast's tone edge are both washes, so both go. A border keeps the mark
     a circle rather than a floating glyph, and the toast keeps its own outline. */
  #ov-grid .viz-mark,
  .rv-mark,
  .rv-av,
  .toast-mark,
  .mgr-day-icon {
    border: 1px solid CanvasText;
  }
  .toast {
    border: 1px solid CanvasText;
  }

  /* Forced colours discard the background-image, so the hero becomes flat Canvas and the strip's
     translucent surface with it. The strip's dividers and its own edge are the only things telling
     five figures apart, so they are restated in system colours; the greeting takes CanvasText
     because --hero-ink is an ivory that would vanish. */
  .mgr-top--overview {
    color: CanvasText;
  }
  .mgr-top--overview .view-title,
  .mgr-top--overview .view-sub,
  .hero-cell-hint {
    color: CanvasText;
  }
  .hero-strip {
    border: 1px solid CanvasText;
  }
  .hero-cell + .hero-cell {
    border-inline-start: 1px solid CanvasText;
  }
  .hero-cell-badge {
    border: 1px solid CanvasText;
  }
}

/* -------------------------------------------------------------------------------------------
 * Requests — the guest-request queue (second in the rail).
 * ----------------------------------------------------------------------------------------- */

/* ## The one colour this file adds outside the palette, and why
 *
 * Everything else on this dashboard acts in --brand (the dusty blue). Requests does not: it is the
 * only screen whose whole job is "a guest is waiting — do something", and the action that answers
 * that has to out-rank every other control on the page at a glance. A blue Take Action beside blue
 * nav, blue chips and blue links is the same weight as its own surroundings.
 *
 * The hue is not new to the system — it is the warm orange the palette already carries as the
 * caution semantic (--warn-line #c2571f), taken a step darker so ivory text clears AA on it. That
 * keeps it a member of this palette rather than an import from another one.
 *
 * Measured, not eyeballed, worst pairing first:
 *   --req-act-ink (#fff8f1) on --req-act (#b0501a)        4.96:1  — clears AA at 13px bold
 *   --req-act-ink on --req-act-hover (#96430f)            6.43:1  — hover darkens, so it can only rise
 *   --warn-ink (#8f3d10) on the Active segment's wash     passes on --surface; the wash is 12% of it
 *
 * Hover DARKENS rather than lightens, the opposite of .btn-brand: that button's fill is dark enough
 * that lightening raises its contrast, and this one's is not.
 *
 * Scoped to the two action boards, deliberately. These tokens must not be reachable from any other
 * section — a warm orange leaking into Overview or Reviews would make the accent meaningless.
 *
 * Bookings is the second board, and it qualifies on the same test rather than by being next in the
 * rail: it is a work surface where a row carries something a person has to *do* — cut a key, close
 * a folio — and its Edit button is that row's action. The rest of the dashboard reports; these two
 * are worked. Adding a third section here should be argued from that sentence, not from taste. */
#view-requests,
#view-bookings {
  --req-act: #b0501a;
  --req-act-hover: #96430f;
  --req-act-ink: #fff8f1;
  --req-act-wash: color-mix(in srgb, var(--req-act) 12%, transparent);
  --req-act-wash-strong: color-mix(in srgb, var(--req-act) 20%, transparent);
  gap: var(--sp-4);
}

/* Dark keeps the fill unchanged, the same call this file makes for --brand. What changes is its
   EDGE, and that is not cosmetic.
 *
 * Measured in dark, the fill sits 2.81:1 against the row it lands on — under the 3.0 floor for a
 * graphical object. The fix is not to lighten it: the label needs 4.5:1 against the same fill, and
 * satisfying both at once leaves a luminance window about 0.007 wide. A colour picked to sit inside
 * a window that narrow is one rounding error from failing either test.
 *
 * So the boundary is drawn instead of implied — a warm rim, which reads at high contrast against
 * charcoal whatever the fill does, plus a lit top edge for the lift a drop shadow cannot provide
 * when there is nothing darker than the background to cast onto. Same trick the palette's own dark
 * --shadow tokens use. The washes are color-mix against `transparent`, so they re-derive for free. */
:root[data-theme="dark"] #view-requests,
:root[data-theme="dark"] #view-bookings {
  --req-act-shadow:
    inset 0 0 0 1px rgba(255, 233, 214, 0.24),
    inset 0 1px 0 rgba(255, 233, 214, 0.3);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) #view-requests,
  :root:not([data-theme]) #view-bookings {
    --req-act-shadow:
      inset 0 0 0 1px rgba(255, 233, 214, 0.24),
      inset 0 1px 0 rgba(255, 233, 214, 0.3);
  }
}

/* ------------------------------------------------------------------------------ the KPI row */

/* Four figures, one question each. `auto-fit` rather than a fixed 4: on a laptop they are a row, on
   a tablet two rows of two, and on a phone a column — without a breakpoint per step. */
/* Explicit counts rather than `auto-fit`, which was the obvious choice and the wrong one: with four
   items it lands on three-plus-an-orphan across a wide band of laptop and tablet widths, and a lone
   card on a second line reads as a fifth figure that failed to load. 4 / 2 / 1, never 3. */
.req-kpis {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
}

@media (max-width: 78rem) {
  .req-kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 34rem) {
  .req-kpis {
    grid-template-columns: minmax(0, 1fr);
  }
}

.req-kpi {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  min-width: 0;
  padding: var(--sp-4);
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}

/* The mark is tinted by what the figure MEANS, which is the only colour on the card — the number
   itself stays ink, because four coloured numbers in a row is a scoreboard, not a status. */
.req-kpi-mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--ink-soft);
}

.req-kpi[data-kpi="waiting"] .req-kpi-mark {
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.req-kpi[data-kpi="priority"] .req-kpi-mark {
  background: var(--danger-bg);
  color: var(--danger-ink);
}

.req-kpi[data-kpi="resolved"] .req-kpi-mark {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

.req-kpi[data-kpi="open"] .req-kpi-mark {
  background: var(--info-bg);
  color: var(--info-ink);
}

.req-kpi-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.req-kpi-k {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.req-kpi-v {
  font-size: calc(30px * var(--type-scale));
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
  /* Tabular, so four cards in a row do not jitter their baselines as the counts change. */
  font-variant-numeric: tabular-nums;
}

/* No trend arrow and no percentage: nothing here stores yesterday's figure, so there is nothing
   honest to compare against. This line carries a fact derived from the same rows instead. */
.req-kpi-sub {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* --------------------------------------------------------------------------- the filter row */

.req-controls {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* ONE row. Every filter a manager has lives here, so narrowing the queue is never a hunt through
   a drawer — which is also why there is no "Filters" button anywhere on this screen. */
.req-filterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}

.req-stay {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

.req-stay-k {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  white-space: nowrap;
}

.req-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.req-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 34px;
  padding: 0.25rem var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.req-chip:hover:not([aria-pressed="true"]) {
  background: var(--surface-2);
  color: var(--ink);
}

/* The selected stage is a filled chip, not a tinted one: this is the single most-changed control
   on the screen and "which stage am I looking at" must be answerable without comparing shades. */
.req-chip[aria-pressed="true"] {
  background: var(--brand);
  color: var(--on-brand);
}

.req-chip-n {
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

.req-filterbar-end {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}

/* The priority combobox. Same module as the property switcher, so the popup below is `.prop-list`
   and inherits its whole surface — only the trigger is restyled for this row. */
.req-select {
  position: relative;
}

.req-select-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 38px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.req-select-btn:hover {
  border-color: var(--hairline-strong);
}

.req-select-chev {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  opacity: 0.6;
}

.req-select-list {
  /* Left-aligned to its trigger, unlike the property list which hangs off the top bar's right edge. */
  right: auto;
  left: 0;
}

/* Active / Resolved. A segmented control rather than two chips, because they are two halves of one
   choice and a manager must never be able to see neither. */
.req-segs {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.req-seg {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 32px;
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: calc(var(--radius-sm) - 2px);
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

.req-seg:hover:not([aria-pressed="true"]) {
  color: var(--ink);
}

/* The warm accent, not a pale yellow: this shares a hue with Take Action on purpose, so "what I am
   looking at" and "what I can do about it" read as one system. */
.req-seg[aria-pressed="true"] {
  background: var(--req-act-wash);
  color: var(--warn-ink);
  box-shadow: inset 0 0 0 1px var(--req-act-wash-strong);
}

.req-seg-n {
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

/* ------------------------------------------------------------------------------- the search */

/* The live pill on the left, the search on the right. One row rather than two, because neither of
   them is worth a band of its own above the queue. */
.req-searchrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

/* Overrides `.live-pill`'s own `margin-left: auto`, which is written for a card header where the
   pill is the last thing on the line. Here it is the first. */
.req-searchrow .req-live {
  margin: 0 auto 0 0;
  align-self: center;
}

.req-search {
  position: relative;
  width: min(22rem, 100%);
}

/* Drawn, not an icon element: it is decoration inside a labelled field, and a `<span>` with two
   pseudo-elements costs nothing next to another inline SVG in the markup. */
.req-search-mark {
  position: absolute;
  top: 50%;
  left: var(--sp-3);
  width: 11px;
  height: 11px;
  margin-top: -7px;
  border: 1.8px solid var(--ink-faint);
  border-radius: 50%;
  opacity: 0.8;
  pointer-events: none;
}

.req-search-mark::after {
  content: "";
  position: absolute;
  top: 9px;
  left: 8px;
  width: 5px;
  height: 1.8px;
  border-radius: 2px;
  background: var(--ink-faint);
  transform: rotate(45deg);
}

.req-search-input {
  width: 100%;
  min-height: 42px;
  padding: 0 var(--sp-4) 0 calc(var(--sp-3) + 20px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
}

.req-search-input::placeholder {
  color: var(--ink-faint);
}

.req-search-input:hover {
  border-color: var(--hairline-strong);
}

/* Chrome and Safari draw their own clear button, in their own colours, at their own size. */
.req-search-input::-webkit-search-cancel-button {
  appearance: none;
}

/* -------------------------------------------------------------------------------- the table */

.req-card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  overflow: hidden;
}

/* The table keeps its shape and scrolls sideways instead of reflowing: eight columns collapsed
   into stacked cards is a different screen, and a duty manager who learned the row order on a
   desktop should not have to relearn it on a tablet. */
.req-tablewrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

/* The floor below which the columns would start crushing each other, not a target width: on any
   screen wider than this the table simply fills its card. */
.req-table {
  min-width: 54rem;
}

.req-table thead th {
  padding: var(--sp-3);
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
  text-align: left;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* Roomy vertically, tight horizontally: the row height is what makes the queue scannable, and the
   gutters are what decide whether eight columns fit a laptop before the wrap has to scroll. */
.req-table tbody th,
.req-table tbody td {
  padding: var(--sp-4) var(--sp-3);
  border-bottom: 1px solid var(--hairline);
  text-align: left;
  font-weight: 400;
  vertical-align: middle;
}

.req-table tbody tr:last-child th,
.req-table tbody tr:last-child td {
  border-bottom: 0;
}

.req-table tbody tr {
  transition: background-color var(--dur) var(--ease);
}

.req-table tbody tr:hover {
  background: var(--surface-2);
}

/* A row a guest is actively blocked on gets a warm edge — the only row-level colour in the table,
   so it cannot be confused with hover or with a status. */
.req-table tbody tr[data-needs-you="1"] th:first-child {
  box-shadow: inset 3px 0 0 var(--req-act);
}

.req-c-prio,
.req-c-room,
.req-c-stage,
.req-c-wait,
.req-c-status {
  width: 1%;
  white-space: nowrap;
}

/* The only column that gets the leftover width — it carries the guest's own wording, which is the
   one cell whose value is not a fixed-width token. */
.req-c-req {
  min-width: 15rem;
}

.req-c-action {
  width: 1%;
  white-space: nowrap;
}

/* Pills inside a table cell: the shared `.pill` is built for a card header, where `margin-left:auto`
   pushes it to the right edge. In a cell that silently right-aligns every chip in the column. */
.req-prio,
.req-status,
.req-stage {
  margin-left: 0;
  padding: 3px var(--sp-2);
  border-color: transparent;
}

.req-title-cell {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  min-width: 0;
}

/* One glyph per request kind, so the queue can be scanned by shape before it is read. */
.req-title-mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--ink-soft);
}

.req-title-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.req-title {
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--ink);
}

/* The guest's own wording, clipped rather than wrapped: a row that grows to three lines breaks the
   scan rhythm of the whole table, and the full text is one click away under View. */
.req-sub,
.req-note {
  overflow: hidden;
  font-size: var(--fs-micro);
  color: var(--ink-soft);
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 20rem;
}

.req-note {
  color: var(--ink-faint);
  font-style: italic;
}

.req-room {
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Quieter than a room number and not italic-apologetic either: it is a normal state on any board
   before the morning's arrivals are keyed in, and it clears from Bookings without touching this.
   Same size as the number it stands in for — colour alone carries "not yet", because shrinking the
   text as well reads as a footnote about missing data rather than as the state of the room. */
.req-room-none {
  font-size: var(--fs-small);
  color: var(--ink-faint);
}

.req-guest {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-small);
  color: var(--ink);
  white-space: nowrap;
}

.req-guest-mark {
  width: calc(15px * var(--type-scale));
  height: calc(15px * var(--type-scale));
  color: var(--ink-faint);
}

/* Stage tints stay low-chroma on purpose. Priority and status are the loud columns; this one only
   has to be distinguishable at a glance, not to compete with them. */
.req-stage {
  background: var(--surface-2);
  color: var(--ink-soft);
}

.req-stage-pre {
  background: color-mix(in srgb, var(--brand) 10%, transparent);
  color: var(--brand-ink);
}

.req-stage-arrival {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}

.req-stage-instay {
  background: var(--info-bg);
  color: var(--info-ink);
}

.req-stage-post {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

/* How long a guest has waited, coloured by how bad that is. Thresholds are about the guest's
   experience, not about priority: a low-priority ask ignored for two hours is still two hours. */
.req-wait {
  font-size: var(--fs-small);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

.req-wait[data-wait="slow"] {
  color: var(--warn-ink);
}

.req-wait[data-wait="late"] {
  color: var(--danger-ink);
  font-weight: 700;
}

.req-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.req-view {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 var(--sp-4);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.req-view:hover {
  border-color: var(--hairline-strong);
  background: var(--surface-2);
}

/* The one loud control on the page — see the token note at the top of this section. */
.req-act {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 var(--sp-4);
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--req-act);
  color: var(--req-act-ink);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: var(--req-act-shadow, 0 1px 2px rgba(80, 34, 8, 0.18));
  transition:
    background-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}

.req-act:hover {
  background: var(--req-act-hover);
  box-shadow: var(--req-act-shadow, 0 2px 8px -1px rgba(80, 34, 8, 0.32));
  transform: translateY(-1px);
}

.req-act:active {
  transform: translateY(0);
}

.req-muted {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* ------------------------------------------------------------------- the footer and the pager */

.req-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--hairline);
  background: var(--surface-2);
}

.req-foot:empty {
  display: none;
}

.req-count {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.req-pager {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}

.req-page,
.req-page-step {
  display: grid;
  place-items: center;
  min-width: 32px;
  height: 32px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.req-page:hover:not([aria-current]),
.req-page-step:hover:not(:disabled) {
  border-color: var(--hairline-strong);
  color: var(--ink);
}

.req-page[aria-current="page"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  cursor: default;
}

.req-page-step:disabled {
  opacity: 0.4;
  cursor: default;
}

.req-page-step .icon {
  width: calc(15px * var(--type-scale));
  height: calc(15px * var(--type-scale));
}

/* One chevron path, turned around — a second glyph in icons.js for a mirror image is a glyph that
   can drift out of step with its twin. */
.req-page-back {
  transform: scaleX(-1);
}

.req-page-gap {
  padding: 0 2px;
  color: var(--ink-faint);
}

/* ------------------------------------------------------------------------------- empty states */

.req-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-7) var(--sp-4);
  text-align: center;
}

.req-empty-mark {
  width: calc(28px * var(--type-scale));
  height: calc(28px * var(--type-scale));
  color: var(--ink-faint);
  opacity: 0.6;
}

.req-empty-text {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

/* ------------------------------------------------------- the View sheet and the action sheet */

.req-detail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--hairline);
}

.req-detail-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.req-detail-meta {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--ink-soft);
}

.req-detail-block + .req-detail-block {
  margin-top: var(--sp-5);
}

.req-detail-h {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.req-detail-summary {
  margin: 0;
  font-size: var(--fs-body);
  color: var(--ink);
}

.req-dl-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--sp-3);
}

.req-dl {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.req-dl-k {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.req-dl-v {
  font-size: var(--fs-small);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.req-history {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.req-history-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}

.req-history-status .pill {
  margin-left: 0;
}

.req-history-meta {
  font-size: var(--fs-micro);
  color: var(--ink-soft);
}

.req-history-note {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
  font-style: italic;
}

.req-action-desc {
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.req-action-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* Every option states what it tells the guest. "Confirm" and "Done" are one word apart and mean
   very different things to someone waiting upstairs, so the consequence is not behind a tooltip. */
.req-action-opt {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: var(--sp-3) var(--sp-4);
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition:
    border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}

.req-action-opt:hover {
  border-color: var(--hairline-strong);
  background: var(--surface-2);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(40, 20, 8, 0.12);
}

.req-action-opt:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Confirm — the primary, positive action: a full brand fill so it reads first, not one grey card
   among three. This is the "colourful, high-quality" restyle: each verb now carries its own
   meaning in colour, so Confirm and Decline are never one indistinct pair. */
.req-action-opt[data-action="confirm"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
}
.req-action-opt[data-action="confirm"]:hover {
  background: var(--brand-dark);
  border-color: var(--brand-dark);
}
.req-action-opt[data-action="confirm"] .req-action-why {
  color: color-mix(in srgb, var(--on-brand) 78%, transparent);
}

/* Decline — danger-tinted AT REST, not only on hover, so the destructive option can never be
   clicked by mistake for the neutral one it used to look identical to. */
.req-action-opt[data-action="decline"] {
  background: var(--danger-bg);
  border-color: var(--danger-line);
  color: var(--danger-ink);
}
.req-action-opt[data-action="decline"]:hover {
  background: color-mix(in srgb, var(--danger-line) 20%, var(--danger-bg));
  border-color: var(--danger-ink);
}
.req-action-opt[data-action="decline"] .req-action-why {
  color: color-mix(in srgb, var(--danger-ink) 80%, transparent);
}

/* Resolve — a completion, so an ok/green tint distinct from both the brand primary and neutral. */
.req-action-opt[data-action="resolve"] {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok-ink);
}
.req-action-opt[data-action="resolve"]:hover {
  border-color: var(--ok-ink);
}
.req-action-opt[data-action="resolve"] .req-action-why {
  color: color-mix(in srgb, var(--ok-ink) 80%, transparent);
}

/* The in-sheet decline reason (replaces the old window.prompt). */
.req-decline-reason {
  width: 100%;
  box-sizing: border-box;
  padding: var(--sp-3);
  margin: 0 0 var(--sp-2);
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  resize: vertical;
}
.req-decline-reason:focus-visible {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 2px var(--brand-wash, transparent);
}
.req-decline-err {
  margin: 0 0 var(--sp-2);
  min-height: 1em;
  font-size: var(--fs-micro);
  color: var(--danger-ink);
}

.req-action-verb {
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.01em;
}

.req-action-why {
  font-size: var(--fs-micro);
  color: var(--ink-soft);
}

/* ==============================================================================================
 * The Bookings board
 *
 * Everything structural here is the Requests board's, reused rather than copied: `.req-kpis`,
 * `.req-filterbar`, `.req-chip`, `.req-search`, `.req-card`, `.req-table`, `.req-foot` and the
 * pager are all shared markup, so the two boards cannot drift into looking like two products.
 * What follows is only what Bookings genuinely adds — six KPI cards instead of four, an editable
 * room field, a meals box, a quiet reservation id, and the status tints.
 * ============================================================================================ */

/* Six figures, not four, so the grid steps 6 / 3 / 2 / 1 — never leaving one card alone on a row. */
.bkg-kpis {
  grid-template-columns: repeat(6, minmax(0, 1fr));
}

@media (max-width: 92rem) {
  .bkg-kpis {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 60rem) {
  .bkg-kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 34rem) {
  .bkg-kpis {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The mark is tinted by what the figure means — the same rule the Requests KPIs follow, so the two
   rows of cards read as one system. Today's two working figures are warm; the rest stay neutral. */
.bkg-kpi[data-kpi="arriving"] .req-kpi-mark {
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.bkg-kpi[data-kpi="instay"] .req-kpi-mark {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

.bkg-kpi[data-kpi="departing"] .req-kpi-mark {
  background: var(--req-act-wash);
  color: var(--req-act);
}

.bkg-kpi[data-kpi="total"] .req-kpi-mark,
.bkg-kpi[data-kpi="revenue"] .req-kpi-mark {
  background: var(--info-bg);
  color: var(--info-ink);
}

/* Money is the one KPI whose value is not a small integer, so it is allowed to be smaller rather
   than forcing every other card's number down to fit "₹1,72,000". */
.bkg-kpi[data-kpi="revenue"] .req-kpi-v {
  font-size: calc(24px * var(--type-scale));
}

/* Sits where the Requests board puts its live pill — the left half of the row above the table. */
.bkg-note {
  margin: 0 auto 0 0;
  align-self: center;
  max-width: 46rem;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* Twelve columns, so the floor before sideways scrolling is higher than the Requests board's. */
.bkg-table {
  min-width: 76rem;
}

.bkg-c-resv,
.bkg-c-room,
.bkg-c-date,
.bkg-c-nights,
.bkg-c-source,
.bkg-c-meals,
.bkg-c-amount,
.bkg-c-status,
.bkg-c-edit {
  width: 1%;
  white-space: nowrap;
}

/* The two columns carrying prose-length values get whatever width is left over. */
.bkg-c-guest {
  min-width: 11rem;
}

.bkg-c-type {
  min-width: 9rem;
}

/* Nights and money are read down the column, so they are right-aligned and tabular. */
.bkg-c-nights,
.bkg-c-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.bkg-table thead th.bkg-c-nights,
.bkg-table thead th.bkg-c-amount {
  text-align: right;
}

.bkg-c-amount {
  font-weight: 600;
}

.bkg-guest {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

/* Initials, not a photograph: the PMS has no guest images, and a row of identical grey silhouettes
   would be decoration. Two letters give the eye something to find its place on when scanning. */
.bkg-avatar {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--brand-wash);
  color: var(--brand-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.bkg-guest-name {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
}

/* Compact and secondary, as an identifier that is looked up rather than read. Clipped instead of
   wrapped so one long reservation id cannot set the height of every row. */
.bkg-resv {
  display: inline-block;
  overflow: hidden;
  max-width: 11rem;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  text-overflow: ellipsis;
  vertical-align: middle;
}

/* ------------------------------------------------------------------ the room number, editable */

/* A field, drawn as a field. No pencil, no reveal-on-hover: assigning rooms is the morning's work,
   and anything between the cursor and the number is friction paid twenty times a shift. */
.bkg-room {
  width: 5.5rem;
  min-height: 38px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  text-align: center;
  /* Border only. The fill is transitioned nowhere on purpose: it changes exactly once, when the
     field disables itself for the length of a save, and animating that made a theme switch leave
     every input frozen part-way between the two palettes' surfaces. */
  transition: border-color var(--dur) var(--ease);
}

/* An assigned room is the one thing on this row a receptionist scans for down the column, so it is
   the only bold value in the table. */
.bkg-room[data-assigned="1"] {
  font-weight: 700;
  letter-spacing: 0.01em;
}

/* Unassigned is ordinary placeholder text — not italic, not orange, not flagged. At nine in the
   morning most of the board is unassigned; that is the normal state of a booking, not a fault,
   and a table where half the rows are highlighted has highlighted nothing. */
.bkg-room::placeholder {
  color: var(--ink-faint);
  font-weight: 400;
  font-style: normal;
  opacity: 1;
}

.bkg-room:hover:not(:disabled) {
  border-color: var(--hairline-strong);
}

/* Disabled means "the server is answering", which is why it dims rather than greys out: the number
   the receptionist just typed has to stay readable while it is being saved. */
.bkg-room:disabled {
  background: var(--surface-2);
  opacity: 0.7;
  cursor: progress;
}

/* The dialog's copy of the same field, sized for a form rather than a table cell. */
.bkg-room-lg {
  width: 100%;
  text-align: left;
}

/* ------------------------------------------------------------------------------------- meals */

/* One line of tags, not four stacked bullets: this is one of twelve columns, and a cell four lines
   deep sets the height of every row on the board. Only what IS included is listed — a struck-through
   "Lunch" would be twice the ink for a fact the reader can already infer. And in meals, never in
   plan codes: "MAP" is shorthand a new receptionist has to be taught. */
/* Check-out date stacked over the property's checkout policy time (see bookings.js checkOutCell). */
.bkg-checkout {
  display: inline-flex;
  flex-direction: column;
  line-height: 1.15;
}
.bkg-checkout-time {
  font-size: var(--fs-micro);
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.bkg-meals {
  display: inline-flex;
  flex-wrap: nowrap;
  gap: 0;
  margin: 0;
  padding: 2px var(--sp-2);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  list-style: none;
}

.bkg-meal {
  display: flex;
  align-items: center;
  font-size: var(--fs-micro);
  line-height: 1.4;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* A separator *between* items rather than a bullet on each: four leading dots read as a checklist
   of four things to do, where "Room · Breakfast" reads as one plan. */
.bkg-meal + .bkg-meal::before {
  content: "·";
  padding: 0 5px;
  opacity: 0.55;
}

/* ------------------------------------------------------------------------------- the edit form */

/* Two columns of short fields — room type beside room number, check-in beside check-out — because
   each pair is one decision. Collapses to one column on a narrow dialog rather than squeezing two
   date inputs into half a phone's width. */
.bkg-edit-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3) var(--sp-4);
}

@media (max-width: 34rem) {
  .bkg-edit-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

.bkg-field {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 5px;
}

/* Same weight and colour as `.req-dl-k`, so a field's label and a read-only line's label read as
   one column of keys rather than two kinds of thing. */
.bkg-field-k {
  font-size: var(--fs-micro);
  font-weight: 600;
  color: var(--ink-faint);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.bkg-input {
  width: 100%;
  min-height: 38px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  transition: border-color var(--dur) var(--ease);
}

.bkg-input:hover:not(:disabled) {
  border-color: var(--hairline-strong);
}

.bkg-input:disabled {
  background: var(--surface-2);
  opacity: 0.7;
  cursor: progress;
}

/* A native date input carries its own picker button, which needs the room the padding would eat. */
.bkg-input[type="date"] {
  padding-right: var(--sp-2);
}

.bkg-hint {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* The meal plans, as radios. Compact rows rather than cards: four short lines is the whole control,
   and anything larger would make what the guest eats the biggest thing in the dialog. */
.bkg-meals-pick {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: var(--sp-4) 0 0;
  padding: 0;
  border: 0;
}

.bkg-meals-pick legend {
  margin-bottom: var(--sp-2);
  padding: 0;
}

.bkg-meals-opt {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px var(--sp-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--dur) var(--ease);
}

.bkg-meals-opt:hover {
  background: var(--surface-2);
}

.bkg-meal-radio {
  flex-shrink: 0;
  margin: 0;
  accent-color: var(--brand);
}

.bkg-meals-opt-t {
  font-size: var(--fs-small);
  color: var(--ink);
}

/* What follows from the fields above, set apart by a rule rather than a box: these are consequences
   of the form, not more of it, and a card around them would read as another thing to fill in. */
.bkg-derived {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-6);
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--hairline);
}

.bkg-derived-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.bkg-derived-v {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.bkg-derived-hint {
  flex-basis: 100%;
  margin: 0;
}

/* The three facts nobody may edit, in the same key/value rhythm the case sheet uses — but across
   one row rather than down three, because they are here to be confirmed at a glance, not read.

   Separated by space rather than a third hairline: two rules in a dialog this short is a divided
   form, three is a stack of boxes. The bold derived figures and these faint key/value pairs already
   read as different kinds of thing. */
.bkg-readonly {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-6);
  margin-top: var(--sp-4);
}

.bkg-readonly .req-dl {
  min-width: 0;
}

/* Delete, alone at the bottom and as far from Save as the dialog allows. Ghost rather than solid:
   the confirm dialog it opens is where the action gets a red button, and two red buttons in a row
   would make the destructive one look like the ordinary one. */
.bkg-danger {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--hairline);
}

.bkg-delete {
  color: var(--danger-ink);
}

/* --------------------------------------------------------------------------- source and status */

.bkg-source,
.bkg-status {
  margin-left: 0;
  padding: 3px var(--sp-2);
  border-color: transparent;
}

/* Low-chroma tints, the same discipline `.req-stage-*` follows: this column has to be
   distinguishable at a glance, not to compete with the room number for attention. */
.bkg-status-arriving {
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.bkg-status-instay {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

.bkg-status-departing {
  background: var(--req-act-wash);
  color: var(--req-act);
}

.bkg-status-upcoming {
  background: color-mix(in srgb, var(--brand) 10%, transparent);
  color: var(--brand-ink);
}

.bkg-status-checked_out {
  background: var(--surface-2);
  color: var(--ink-faint);
}

/* The row's one action, and the only warm control on the board — same token as Take Action on the
   Requests board, so "the thing to click" looks the same on both. Never black. */
.bkg-edit {
  padding: 0 var(--sp-3);
}

/* ------------------------------------------------------------------------------- narrow screens */

@media (max-width: 60rem) {
  /* The filter row stacks rather than squeezing: a priority dropdown and a segmented control jammed
     against the stage chips is how a one-row filter bar becomes unreadable. */
  .req-filterbar {
    align-items: stretch;
    flex-direction: column;
  }

  .req-filterbar-end {
    justify-content: space-between;
  }

  .req-search {
    align-self: stretch;
    width: 100%;
  }
}

/* The connection pill. A dead feed must LOOK dead — a stale dashboard that still reads "Live" is
   the worst outcome available here, because a manager who trusts it stops refreshing. */
.live-pill {
  margin: 0 0 0 auto;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.live-pill::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
}

.live-pill[data-state="idle"] {
  display: none;
}

.live-pill[data-state="live"] {
  color: var(--ok-ink);
}

.live-pill[data-state="connecting"],
.live-pill[data-state="reconnecting"] {
  color: var(--warn-ink);
}

.live-pill[data-state="offline"] {
  color: var(--danger-ink);
}

/* A global `:where(button, …):focus-visible` rule already covers this; nothing extra is needed and
   a second declaration here would only be a place for the two to disagree. */

/* New work must be visible without hunting for it. */
@keyframes row-flash {
  from {
    background: var(--info-bg);
  }
  to {
    background: transparent;
  }
}

.record-table tbody tr.row-flash {
  animation: row-flash 1.6s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  /* The flash is the only signal that a row is new, so it is held rather than dropped — a steady
     tint instead of an animation. */
  .record-table tbody tr.row-flash {
    animation: none;
    background: var(--info-bg);
  }

  /* Take Action's hover lift is decoration; its colour change is the affordance and stays. */
  .req-act:hover {
    transform: none;
  }
}

@media (forced-colors: active) {
  /* Every one of these says something with a fill, which forced colours discard. The pressed
     filter chips and the current page are the worst of it: without a border, "which stage am I
     looking at" and "which page am I on" both become unanswerable. */
  .req-chip[aria-pressed="true"],
  .req-seg[aria-pressed="true"],
  .req-page[aria-current="page"],
  .req-act {
    border: 1px solid ButtonText;
  }

  /* The warm edge marking a row someone is waiting on is a box-shadow, which is dropped outright. */
  .req-table tbody tr[data-needs-you="1"] th:first-child {
    border-left: 3px solid Highlight;
  }
}

/* ------------------------------------------------------------------ the sample-data disclosure

   Shown only when the server says these rows are seeded demo data (`sample_data` on
   /api/manager/properties, painted by page.js's paintSampleBanner).

   ## Why it is a footer bar, and why it is still here

   Two earlier shapes were both wrong. A full-width strip pinned across the TOP of the viewport
   cost a permanent band above the hero and was the first thing anyone saw, which made a finished
   product read as a staging environment. Replacing it with a floating corner chip fixed that and
   introduced a worse problem: an unanchored rounded rectangle sitting ON TOP of the first card of
   "Needs a decision", which is both a toast (a transient shape used for a permanent statement)
   and an obstruction.

   What it must NOT become is invisible. The reason this element exists is documented on
   paintSampleBanner: the seeded properties carry the names of real hotels, so a dashboard of "The
   Summit" guests, complaints and NPS shown without a caveat asserts by omission that the chain is
   already a customer. That claim is made by a screenshot of *any* panel, so it has to survive
   scrolling and be in frame in a screengrab of any part of the page.

   A footer bar satisfies both. It is architecture rather than an overlay — full width of the
   content column, flush to the bottom edge, square corners, sharing the rail's own hairline
   vocabulary — so it reads as part of the shell the way an application status bar does. It is
   `fixed` so it survives scrolling, and `.mgr-main` reserves exactly its height as padding
   (--disclosure-h, below), so no content can ever end up underneath it. It starts at the rail's
   right edge, so it never covers the navigation either.

   It keeps its full sentence and its `role="status"`, so screen readers and copy-paste get
   exactly what the original strip gave them.

   Muted ink on the page's own surface, with a single small amber dot as the caveat marker. The
   previous amber-on-amber slab was correct when the element was a chip that had to announce
   itself; as a piece of shell it only has to be *legible and permanent*, and a full amber bar
   across the foot of every screen would be a louder claim than the caveat itself. */
.sample-banner {
  position: fixed;
  left: var(--rail-w);
  right: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--disclosure-h);
  padding: 0.35rem var(--sp-6);
  border-top: 1px solid var(--hairline);
  background: var(--surface);
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.01em;
}

/* The caveat marker. A 6px amber dot carries "this is a qualification" at a glance without
   tinting the whole bar — the same job the old amber background did, at 1/300th of the area. */
.sample-banner::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--warn-line);
}

:root[data-theme="dark"] .sample-banner {
  background: var(--paper);
}

/* Below the rail's drawer z-index (55) and its scrim, so an open drawer covers the bar rather
   than the bar punching through it. */
@media (max-width: 62rem) {
  .sample-banner {
    left: 0;
    padding-inline: var(--sp-4);
  }
}

@media (max-width: 40rem) {
  .sample-banner {
    padding-inline: var(--sp-3);
  }
}

/* ===========================================================================================
 * The Guests board, and one guest's page
 *
 * Built on the Requests board's furniture — `.req-kpi`, `.req-filterbar`, `.req-chip`,
 * `.req-select`, `.req-card`, `.req-table`, `.req-pager` all carry this section too, so there is
 * one set of board styles on this dashboard rather than three that drift. Everything below is
 * either a fifth KPI column the shared grid does not have, or something genuinely new here: the
 * guest cell, the stay cell's two lines, the attention flags, and the guest's own page.
 * =========================================================================================== */

/* Five figures, not four. The shared `.req-kpis` grid is a hard `repeat(4, …)`, and a fifth card
   wrapping alone onto a second row reads as an afterthought rather than as one of the five things
   a duty manager checks. It steps down at the same breakpoints the four-up grid does. */
.gst-kpis {
  grid-template-columns: repeat(5, minmax(0, 1fr));
}

@media (max-width: 88rem) {
  .gst-kpis {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 60rem) {
  .gst-kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 34rem) {
  .gst-kpis {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Tinted by what the figure MEANS, following the same rule as the other two boards: only the mark
   is coloured, never the number. Attention is the one red one — it is the only figure here that
   is work rather than a measurement. */
.gst-kpi[data-kpi="arriving"] .req-kpi-mark {
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.gst-kpi[data-kpi="instay"] .req-kpi-mark {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

.gst-kpi[data-kpi="departing"] .req-kpi-mark {
  background: var(--info-bg);
  color: var(--info-ink);
}

.gst-kpi[data-kpi="attention"] .req-kpi-mark {
  background: var(--danger-bg);
  color: var(--danger-ink);
}

/* The Attention card's action, standing where that card's sub-line would be. Sized and positioned
   exactly like `.req-kpi-sub` so the five cards keep one baseline grid — it reads as the caption it
   replaces, in the brand colour, rather than as a button dropped into a row of figures. */
.gst-kpi-act {
  align-self: start;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-micro);
  color: var(--brand-ink);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.gst-kpi-act:hover {
  text-decoration: underline;
}

.gst-kpi-act:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 2px;
}

.gst-kpi[data-kpi="total"] .req-kpi-mark {
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--brand-ink);
}

/* The search row has no live pill on this board — `.req-searchrow` uses `margin-left: auto` on
   that pill to push the field right, so without one the field would sit left of everything. */
.gst-searchrow {
  justify-content: flex-end;
}

/* ------------------------------------------------------------------------------------ the table */

.gst-table {
  min-width: 62rem;
}

.gst-c-room,
.gst-c-lang,
.gst-c-pref,
.gst-c-dates,
.gst-c-attention {
  white-space: nowrap;
}

.gst-c-guest {
  min-width: 13rem;
}

.gst-c-stay {
  min-width: 9.5rem;
}

/* The stay read as one span. Tabular figures so the day numbers line up down the column rather than
   drifting with the width of each month's name. */
.gst-c-dates {
  font-variant-numeric: tabular-nums;
}

.gst-c-attention {
  min-width: 9rem;
}

/* The chevron column is an affordance, not data: it takes only the width it needs and never a
   heading's worth of it. */
.gst-c-go {
  width: 1px;
  padding-left: 0;
  text-align: right;
}

/* The whole row is the target. A "View" button in the last column would give one action two hit
   areas that behave differently under the keyboard, and would make the other seven columns dead
   space in a table whose every row leads somewhere. */
.gst-row {
  cursor: pointer;
}

.gst-row:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--brand);
}

.gst-row .gst-go {
  color: var(--ink-faint);
  transition: transform var(--dur) var(--ease), color var(--dur) var(--ease);
}

.gst-row:hover .gst-go,
.gst-row:focus-visible .gst-go {
  transform: translateX(2px);
  color: var(--brand-ink);
}

.gst-guest {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

/* The initials mark, shared by the table, the party list and the guest page's header. Warm rather
   than grey: it is the one place a person appears on a board of bookings. */
.gst-avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--brand) 14%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--brand) 26%, transparent);
  color: var(--brand-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.gst-avatar-sm {
  width: 28px;
  height: 28px;
}

.gst-avatar-lg {
  width: 56px;
  height: 56px;
  font-size: var(--fs-lead);
}

.gst-guest-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.gst-guest-name {
  font-weight: 600;
  color: var(--ink);
}

/* The party, under the name. This is the fact that turns a directory row into an operational one:
   "2 Adults · 1 Child" is how many towels, how many covers at breakfast, whether a cot goes up. */
.gst-guest-sub {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.gst-stay {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
}

/* When the desk next has to do something about this guest. Quiet, because the chip above it
   carries the status and this only qualifies it. */
.gst-stay-note {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.gst-status {
  margin-left: 0;
}

.gst-status-arriving {
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.gst-status-instay {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

.gst-status-departing {
  background: var(--info-bg);
  color: var(--info-ink);
}

.gst-status-upcoming {
  background: color-mix(in srgb, var(--brand) 10%, transparent);
  color: var(--brand-ink);
}

.gst-status-checked_out {
  background: var(--surface-2);
  color: var(--ink-faint);
}

.gst-room {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink);
}

/* Not tinted, not italic, not flagged. An unassigned room at 09:00 is the normal state of an
   arrival, and a column where half the rows shout is a column that says nothing. */
.gst-room-none {
  font-weight: 400;
  color: var(--ink-faint);
}

.gst-attention {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.gst-flag {
  margin-left: 0;
}

/* -------------------------------------------------------------- the feedback call column ----- */

.gst-c-feedback {
  white-space: nowrap;
}

/* The consent switch above, the Call now button and its status line below — the same two-line
   rhythm .gst-stay beside it already uses. */
.gst-feedback {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
}

.gst-consent {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Stands in for the switch when the row has no guest id to write a consent change against — see
   `feedbackCallCell` in guests.js. Quiet rather than flagged: a gap in what today's API sends is
   not this guest's fault. */
.gst-consent-unavailable {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* This dashboard's first real switch control — everywhere else that reads as a toggle (.viz-filters,
   the theme control) is a segmented control or a checkbox, not a track-and-thumb. Sized off this
   row rather than off an existing switch, because there isn't one to match; the shape (pill track,
   round thumb, colour fill for "on") is the one every OS switch already uses, so it needs no legend
   of its own. */
.gst-consent-toggle {
  --gst-switch-w: 34px;
  --gst-switch-h: 20px;
  position: relative;
  flex: none;
  width: var(--gst-switch-w);
  height: var(--gst-switch-h);
  padding: 0;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.gst-consent-toggle[aria-checked="true"] {
  background: var(--ok-ink);
  border-color: var(--ok-ink);
}

.gst-consent-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: calc(var(--gst-switch-h) - 4px);
  height: calc(var(--gst-switch-h) - 4px);
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
  transition: transform var(--dur) var(--ease);
}

.gst-consent-toggle[aria-checked="true"] .gst-consent-thumb {
  transform: translateX(calc(var(--gst-switch-w) - var(--gst-switch-h)));
}

.gst-consent-toggle:disabled {
  cursor: default;
  opacity: 0.6;
}

.gst-consent-toggle:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.gst-consent-label {
  font-size: var(--fs-micro);
  font-weight: 600;
  color: var(--ink-soft);
}

.gst-call {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

/* Same footprint and language as .req-view: a quiet secondary action. This is a convenience beside
   the automatic post-stay sweep, not the one thing the row exists to make you do, so it does not
   reach for .req-act's loud brand fill. */
.gst-call-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 28px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-micro);
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.gst-call-btn:hover:not(:disabled) {
  border-color: var(--hairline-strong);
  background: var(--surface-2);
}

.gst-call-btn:disabled {
  cursor: default;
  opacity: 0.7;
}

.gst-call-mark {
  width: calc(14px * var(--type-scale));
  height: calc(14px * var(--type-scale));
}

/* Empty until a call is placed and refused or accepted — see `callGuestNow` in page.js. */
.gst-call-status {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

.gst-call-status[data-tone="ok"] {
  color: var(--ok-ink);
}

.gst-call-status[data-tone="blocked"] {
  color: var(--danger-ink);
}

/* --------------------------------------------------------------------------- one guest's page */

#guests-detail {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.gst-detail-head {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* The way back, and the first thing in the reading order — a page that replaced a list has to say
   so before it says anything else. */
.gst-back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
}

.gst-back:hover {
  color: var(--brand-ink);
}

.gst-back-mark {
  transform: rotate(180deg);
}

.gst-detail-top {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}

.gst-detail-who {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  min-width: 0;
}

.gst-detail-id {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
}

.gst-detail-nameline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}

.gst-detail-name {
  margin: 0;
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: var(--lh-tight);
  color: var(--ink);
}

/* Room and room type on one line, and the type alone when no number has been assigned — the type
   is a real fact about the booking, and it is what reception needs when they come to assign one. */
.gst-detail-room {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.gst-detail-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: 2px;
}

.gst-meta-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

.gst-meta-mark {
  color: var(--ink-faint);
}

/* `.btn-solid`/`.btn-brand` carry colour and no metrics — every other use of them on this page
   sits in a dialog footer that sizes them. This one stands on its own, so it states its own, at
   the same 36px floor `.btn-ghost` uses across the dashboard. */
.gst-detail-cta {
  flex: none;
  padding: calc(var(--sp-1) + 2px) var(--sp-5);
  min-height: calc(36px * var(--type-scale));
  font-weight: 600;
}

/* Three cards across on a desktop, in reading order: the booking on the top row, the service on
   the bottom. They reflow to two and then one rather than shrinking — a key/value list at a third
   of a laptop's width wraps every value onto its own line. */
.gst-detail-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4);
  align-items: start;
}

@media (max-width: 78rem) {
  .gst-detail-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 52rem) {
  .gst-detail-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

.gst-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
  padding: var(--sp-5);
  background: var(--card-face);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}

.gst-card-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.gst-card-mark {
  color: var(--ink-faint);
}

.gst-card-h {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink);
}

/* Only the card that is carrying a problem is tinted, and only at its edge. Tinting every card
   would make the one that matters indistinguishable from the five that do not. */
.gst-card-attention {
  border-color: var(--danger-line);
}

.gst-card-attention .gst-card-mark {
  color: var(--danger-ink);
}

.gst-card-clear .gst-card-mark {
  color: var(--ok-ink);
}

.gst-card-action {
  align-self: flex-start;
  margin-top: var(--sp-1);
}

.gst-party {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.gst-party-k {
  margin: 0;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The head-count, above the people it counts. Ruled off below rather than above, so it reads as
   the card's own summary line and not as part of the primary-guest block under it. */
.gst-party-count {
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--hairline);
}

.gst-party-rest {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--hairline);
}

.gst-person {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

.gst-person-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.gst-person-name {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
}

.gst-person-sub {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}

.gst-identity {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  align-items: flex-start;
}

.gst-id-status {
  margin-left: 0;
}

.gst-identity .req-dl-list {
  width: 100%;
}

/* A short paragraph inside a card — the pending-ID note, a guest note, the documents caveat. */
.gst-note {
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-body);
  color: var(--ink-soft);
}

/* The one warning this page can raise on its own: a stated preference the property cannot
   actually honour, which nothing else on the dashboard would surface. */
.gst-warn {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3);
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  border-radius: var(--radius-sm);
  color: var(--warn-ink);
  font-size: var(--fs-micro);
  line-height: var(--lh-body);
}

.gst-prefs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.gst-pref {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  font-size: var(--fs-small);
  color: var(--ink);
}

.gst-pref-mark {
  flex: none;
  margin-top: 1px;
  color: var(--ok-ink);
}

.gst-pref-k {
  color: var(--ink-faint);
}

.gst-attention-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.gst-issue {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.gst-issue-h {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--danger-ink);
}

.gst-issue-row {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-left: var(--sp-3);
  border-left: 2px solid var(--danger-line);
}

.gst-issue-what {
  font-size: var(--fs-small);
  color: var(--ink);
}

.gst-issue-when {
  font-size: var(--fs-micro);
  color: var(--ink-faint);
}

/* ------------------------------------------------- a booking change awaiting approval, in Requests */

/* A before-and-after table, not a key/value list. The manager's decision is a comparison — "305 or
   512" — and two columns is what a comparison looks like. Only the fields being changed appear. */
.bkg-req-table {
  width: 100%;
  margin: 0 0 var(--sp-2);
  border-collapse: collapse;
  font-size: var(--fs-small);
}

.bkg-req-table thead th {
  padding: 0 var(--sp-3) 4px 0;
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-align: left;
  text-transform: uppercase;
}

.bkg-req-table tbody th,
.bkg-req-table tbody td {
  padding: 5px var(--sp-3) 5px 0;
  border-top: 1px solid var(--hairline);
  text-align: left;
  vertical-align: top;
}

.bkg-req-k {
  color: var(--ink-faint);
  font-weight: 600;
  white-space: nowrap;
}

/* The current value is context, the requested one is the decision — so only the second is emphasised.
   Struck-through "before" was tried and read as though the change had already happened, which is the
   one impression a pending request must not give. */
.bkg-req-was {
  color: var(--ink-soft);
}

.bkg-req-now {
  color: var(--ink);
  font-weight: 700;
}

/* A multi-day itinerary, grouped by date. A flat stop list plus "Days: 3" left a shift guessing
   which of twelve stops belonged to Saturday; these are the same `days: [{date, stops}]` the
   backend already returns. One-day itineraries never reach this — they keep the single-line form. */
.itin-days {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  max-width: 26rem;
}

.itin-day {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

/* The date is the thing being scanned for, so it carries the weight and the stops recede. */
.itin-day-label {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
}

.itin-day-stops {
  font-size: 0.82rem;
  white-space: normal;
  color: var(--ink-soft);
}
