/* Caveat, the one HAND on this console: the journal entry's "-Name @ time"
   signature and nothing else. Loaded HERE rather than as a <link> in a page
   head, because app.js draws that card into two different shells (admin.mjs
   and admin/shell.mjs) and a font remembered in one head and forgotten in
   the other is a signature that renders as a fallback on half the console.
   The stylesheet that names --script is the thing that knows it needs it.

   tokens.css is where Bricolage is imported the same way, and is a CHECKED
   COPY of the site's (admin-tokens-check), so it must not grow a font the
   site does not have. This file is ours. @import has to precede every rule. */
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@500;600&display=swap');
/* The Rova console. docs/2026-09-05-admin-redesign.md.
 *
 * Loads AFTER tokens.css (the site's own, a checked copy) and takes its colours,
 * type and radii from there: --cream, --ink and its alphas, --coral, --crimson,
 * --peach, --blush, --display, --r-*. What this file adds is the console's own
 * vocabulary -- a rail, a topbar, panels, chips, a month grid, a drawer -- and a
 * dark theme by re-pointing the same tokens, so no component knows which theme
 * it is in. Semantic colours (good, warn, bad) are separate from the accent and
 * used only for state.
 *
 * tokens.css also defines .btn, .chip and .card for the marketing pages. The
 * console's .btn and .chip below restate every property they need rather than
 * inheriting, so a change to the site's buttons cannot quietly restyle a Save.
 */

/* :host as well as :root, so the same tokens resolve inside admin mode's shadow
   root on the trip page (mode.js), where there is no :root of ours. */
:root, :host {
  --paper: #FFFFFF;
  --rail: #FBF3E2;
  --good: #1E7A3D; --good-bg: #1E7A3D1F;
  --warn: #9A5A12; --warn-bg: #FF885233;
  --bad: #C8102E;  --bad-bg: #C8102E1A;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  /* A HAND, for the one thing on this console that is a signature and not a
     label (owner, Sept 10 2026: the journal entry's "-Name @ time"). Defined
     HERE and not in tokens.css, which is a checked copy of the site's
     (admin-tokens-check) and must not grow a token the site does not have.
     Caveat is loaded beside Bricolage in admin.mjs' head; the fallbacks are
     the platform hands, so the line still reads as writing if the webfont
     never arrives. */
  --script: "Caveat", "Bradley Hand", "Segoe Script", "Snell Roundhand", cursive;
  --shadow: 0 18px 50px -20px #0B0D0A4D;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --cream: #14150F; --cream-2: #1C1D16; --ink: #F4EEDF;
    --ink-70: #F4EEDFB3; --ink-62: #F4EEDF9E; --ink-50: #F4EEDF80; --ink-30: #F4EEDF4D; --ink-12: #F4EEDF1F; --ink-08: #F4EEDF14;
    --paper: #1C1D16; --rail: #101109; --peach: #5A3A24; --blush: #56292F;
    --good: #6FCF8C; --good-bg: #6FCF8C26; --warn: #FFB27A; --warn-bg: #FF885233; --bad: #FF6B7E; --bad-bg: #FF6B7E26;
    --shadow: 0 18px 50px -20px #00000099;
  }
}
:root[data-theme="dark"] {
  --cream: #14150F; --cream-2: #1C1D16; --ink: #F4EEDF;
  --ink-70: #F4EEDFB3; --ink-62: #F4EEDF9E; --ink-50: #F4EEDF80; --ink-30: #F4EEDF4D; --ink-12: #F4EEDF1F; --ink-08: #F4EEDF14;
  --paper: #1C1D16; --rail: #101109; --peach: #5A3A24; --blush: #56292F;
  --good: #6FCF8C; --good-bg: #6FCF8C26; --warn: #FFB27A; --warn-bg: #FF885233; --bad: #FF6B7E; --bad-bg: #FF6B7E26;
  --shadow: 0 18px 50px -20px #00000099;
}

* { box-sizing: border-box; }
body { margin: 0; background: var(--cream); color: var(--ink); font-family: var(--display); font-size: 15px; line-height: 1.5; -webkit-font-smoothing: antialiased; font-variation-settings: "opsz" 14; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; }
[hidden] { display: none !important; }
.tnum { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); font-size: 12.5px; }
h1, h2, h3 { margin: 0; text-wrap: balance; letter-spacing: -0.02em; }
h1 { font-size: 26px; font-weight: 700; }
h2 { font-size: 18px; font-weight: 600; }
.eyebrow { font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-62); }
.muted { color: var(--ink-62); }
.boot { padding: 40px; color: var(--ink-62); font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.16em; text-transform: uppercase; }
:focus-visible { outline: 3px solid var(--crimson); outline-offset: 2px; }

/* ── Gate ─────────────────────────────────────────────────── */
/* The site's palette in a cabinet: pixels, square corners and hard shadows
   where everything behind the gate uses soft radii. Deliberately only here.
   The wordmark is drawn by pixelWord() in app.js, so this styles a <svg> of
   1x1 rects: crispEdges keeps the pixels square at any width. */
.gate { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
        gap: 26px; padding: 24px 24px 64px; position: relative;
        background: radial-gradient(110% 70% at 50% 0%, var(--peach) 0%, transparent 66%), var(--cream); }
/* Scanlines, over the whole field and under nothing: pointer-events none so
   every click still reaches the form. */
.gate::after { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.5;
        background: repeating-linear-gradient(to bottom, transparent 0 4px, var(--ink-08) 4px 5px); }
.cab { position: relative; z-index: 1; width: min(460px, 100%); background: var(--paper);
       border: 3px solid var(--ink); border-radius: 5px; box-shadow: 8px 8px 0 0 var(--ink); overflow: hidden; }
.cab-top { display: flex; align-items: center; gap: 9px; padding: 9px 16px; background: var(--ink); color: var(--cream);
           font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; }
.led { width: 8px; height: 8px; background: var(--coral); border: 0; padding: 0; border-radius: 0; cursor: pointer;
       position: relative; flex: none; }
/* The LED is also the staff door for a thumb (Shift+A on a keyboard): the
   hit area is 28px square around an 8px light, and it turns crimson while
   the cabinet is turned over. */
.led::before { content: ""; position: absolute; inset: -10px; }
.led.staff { background: var(--crimson); box-shadow: 0 0 0 2px var(--cream); }
/* The name goes home to the marketing site; the arrow at the far right of the
   row is the same door (Tabler's external-link, at the row's cap height). */
.cab-top .home { display: inline-flex; align-items: center; gap: 7px; color: inherit; text-decoration: none; }
.cab-top .home:hover, .cab-top .exit:hover { color: var(--coral); }
.cab-top .exit { margin-left: auto; display: inline-flex; color: inherit; padding: 4px; margin-right: -4px; }
.cab-top .exit svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2;
                     stroke-linecap: round; stroke-linejoin: round; }
/* The cabinet's screen. The file is 166x87, the picture's own pixel grid, so
   the browser is doing a roughly 2.4x upscale here, and image-rendering keeps
   the blocks square instead of blurring them back into the anti-aliasing the
   grid was recovered from. crisp-edges first and pixelated second, in that
   order: the later declaration wins wherever both are understood, and
   pixelated is the one that means nearest-neighbour. Height comes from the
   aspect ratio, so the cabinet never jumps when the image lands. */
.cab-art { display: block; border-bottom: 3px solid var(--ink); background: var(--ink); line-height: 0; }
.cab-art:focus-visible { outline-offset: -3px; }
.cab-art img { display: block; width: 100%; height: auto; aspect-ratio: 166 / 87;
               image-rendering: crisp-edges; image-rendering: pixelated; }
/* The small Rova below the cabinet, on the field rather than in the box:
   rova.js mounts one of her moments as an <img> and swaps it for the next,
   through all of them, with nothing said about it. Pixelated so the browser
   scales the SVG's pixels square, though as vector rects they are square
   already; z-index puts her over the scanlines like the cabinet. */
/* A BUTTON since v2, not a link: she is the console's own door (the cabinet
   already links to the website twice above her). The reset is here because the
   traveller's console scopes its own resets under .uc and this one is not. */
.small-rova { position: relative; z-index: 1; line-height: 0; display: block;
              border: 0; background: transparent; padding: 0; cursor: pointer; }
.small-rova:focus-visible { outline: 2px solid var(--coral); outline-offset: 4px; }
.small-rova img { display: block; image-rendering: crisp-edges; image-rendering: pixelated; }
.small-rova:hover img { filter: drop-shadow(0 0 0 var(--coral)) drop-shadow(2px 2px 0 var(--coral)); }
/* Under her, a terminal line: what she is for, typed and retyped. */
.term { position: relative; z-index: 1; margin-top: -8px; font-family: var(--mono); font-size: 16px; line-height: 1.5;
        color: var(--ink-70); text-align: center; max-width: 440px; min-height: 3em; letter-spacing: 0.02em; }
.term .prompt { color: var(--coral-2); font-weight: 700; }
.term .term-name { color: var(--ink); font-weight: 700; }
.term .term-sep { display: none; }
.term.has-desc .term-sep { display: inline; }
.term .cur { display: inline-block; width: 9px; height: 17px; background: var(--ink); vertical-align: -2px; margin-left: 2px;
             animation: term-blink 1s steps(2) infinite; }
@keyframes term-blink { to { opacity: 0; } }
/* The pill at the foot: which door. Two positions, the lit one is where you
   are. Quiet by design (owner, 2026-09-06): almost nobody arriving here is
   staff, so this is a way back for the few who are, not a choice the page
   asks of everyone. No cabinet border, no shadow, no coral slab -- the one
   you are on is inked and underlined, and the tap target stays 40px. */
.pill { position: relative; z-index: 1; display: inline-flex; gap: 4px; border: 0; background: none; box-shadow: none; }
.pill button { border: 0; border-bottom: 2px solid transparent; background: transparent; padding: 8px 12px; font-family: var(--mono);
               font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-62); cursor: pointer; min-height: 40px; }
.pill button.on { color: var(--ink-70); border-bottom-color: var(--ink-30); font-weight: 700; }
.pill button:hover { color: var(--ink); }
/* The trail along the very foot of the gate: the small Rova (28px, one
   screen pixel a drawn pixel) walking the full width, chasing a ball and
   stopping for her moments (Rova.trail in rova.js). The medium one under
   the cabinet keeps to what stays inside her frame; the movements that
   leave it are down here (owner, 2026-09-06). Absolute, so the centred cabinet does
   not move; the gate's bottom padding keeps the pill clear of her; the
   strip is as tall as her widest frame's box at that size, so a run or a
   jump stays inside it. */
.trail { position: absolute; left: 0; right: 0; bottom: 0; height: 36px; z-index: 1; overflow: hidden; pointer-events: none; }
.trail .rova-walker { position: absolute; left: 0; bottom: 0; will-change: transform; }
.trail .rova-ball { position: absolute; left: 0; bottom: 0; image-rendering: pixelated; }
.trail .rova-ball.rolling { animation: rova-bounce 260ms ease-out infinite alternate; }
@keyframes rova-bounce { to { bottom: 6px; } }
.trail img { display: block; }
.trail img[hidden] { display: none; }
.cab-body { padding: 26px 24px 22px; }
.gate .gate-form { display: flex; flex-direction: column; gap: 11px; }
.gate .gate-form[hidden] { display: none; }
/* The number is the front door and now reads like it (owner, 2026-09-06):
   the label inked rather than grey, the field a size you cannot miss, and
   the button the biggest thing in the cabinet. The pill at the foot came
   down as this went up -- the two changes are one decision. */
.gate .lbl { font-family: var(--mono); font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink); font-weight: 700; text-align: center; }
.gate .lede { color: var(--ink-70); font-size: 14px; line-height: 1.45; text-align: center; }
/* The number: a fixed +1 on the left, the digits landing as (000) 000-0000.
   The prefix steps aside when a number arrives with its own +. */
.gate .tel { display: flex; border: 2px solid var(--ink); background: var(--cream); }
.gate .tel .cc { display: flex; align-items: center; padding: 0 0 0 13px; font-family: var(--mono); font-size: 16px;
                 letter-spacing: 0.1em; color: var(--ink-50); }
.gate .tel.intl .cc { display: none; }
.gate .tel { min-height: 62px; }
.gate .tel .cc { font-size: 19px; }
.gate .tel input { flex: 1; border: 0; min-width: 0; font-size: 19px; letter-spacing: 0.06em; }
.gate .tel input:focus { box-shadow: none; }
.gate .tel:focus-within { border-color: var(--coral); box-shadow: 3px 3px 0 0 var(--coral); }
.gate input.code { text-align: center; font-size: 26px; letter-spacing: 0.42em; padding-left: 0.42em; }
.gate .hello { font-family: var(--display); font-weight: 800; font-size: 26px; letter-spacing: -0.01em; color: var(--ink);
               text-align: center; margin: 6px 0 4px; }
.btn8.go { font-size: 17px; min-height: 56px; }
/* Text Login Link: the one thing on this screen to press. */
.gate #pgSendBtn { min-height: 60px; font-size: 17px; }
.gate .pg-row { display: flex; justify-content: space-between; gap: 12px; }
/* The wipe: a coral slab crosses the screen left to right, the console is
   drawn under it, and it leaves to the right. Two positions, like the button. */
.wipe { position: fixed; inset: 0; z-index: 90; background: var(--coral); transform: translateX(-102%);
        display: grid; place-items: center; transition: transform .5s cubic-bezier(.7,0,.2,1); pointer-events: none; }
.wipe.in { transform: translateX(0); }
.wipe.out { transform: translateX(102%); }
.wipe-in { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.wipe-in .pix { width: 220px; height: auto; color: var(--cream); filter: drop-shadow(5px 5px 0 var(--ink)); }
.wipe-in span { font-family: var(--mono); font-size: 12px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .term .cur { animation: none; } .wipe { transition: none; } }
.gate p { margin: 0; color: var(--ink-62); font-size: 14px; }
.wordmark { display: flex; justify-content: center; padding: 4px 0 2px; }
.pix { fill: currentColor; shape-rendering: crispEdges; }
.wordmark .pix { width: 188px; height: auto; color: var(--coral); filter: drop-shadow(4px 4px 0 var(--ink)); }
.gate .tag { font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em;
             text-transform: uppercase; color: var(--ink-62); text-align: center; margin: 10px 0 18px; }
.gate input { min-height: 48px; padding: 10px 13px; border-radius: 0; border: 2px solid var(--ink);
              background: var(--cream); font-family: var(--mono); font-size: 16px; letter-spacing: 0.1em; }
.gate input:focus { outline: none; border-color: var(--coral); box-shadow: 3px 3px 0 0 var(--coral); }
/* The one button in the console with a press. steps(2) rather than a smooth
   ease: a cabinet button has two positions, not a curve. */
.btn8 { min-height: 48px; border: 2px solid var(--ink); border-radius: 0; background: var(--coral); color: #0B0D0A;
        font-family: var(--display); font-weight: 800; font-size: 15px; letter-spacing: 0.1em; text-transform: uppercase;
        cursor: pointer; box-shadow: 4px 4px 0 0 var(--ink); transition: transform .06s steps(2), box-shadow .06s steps(2); }
.btn8:hover { background: var(--coral-2); }
.btn8:active { transform: translate(4px, 4px); box-shadow: 0 0 0 0 var(--ink); }
.gate .err { color: var(--bad); font-family: var(--mono); font-size: 12.5px; min-height: 18px; text-align: center; }

/* ── The number, centred and using the width (owner, 2026-09-07) ──────────
   The field was already the loudest thing in the cabinet; it was not using
   the room it had. The group is centred, the digits are centred in it, and
   the `+1` sits against them rather than against the far left edge, so the
   number reads as one thing instead of a prefix and a gap. The cabinet grew
   with it. Nothing here changes what the field ACCEPTS: formatPhoneTyping
   and phoneToSend are untouched, and a leading + still drops the prefix
   through .tel.intl exactly as before. */
.gate .tel { justify-content: center; gap: 2px; padding: 0 10px; }
.gate .tel .cc { padding: 0; color: var(--ink-62); }
/* The input TAKES the width rather than being capped to a character count.
   A ch cap was tried at 12 and again at 15 and clipped its own placeholder
   both times: the field is monospaced at 19px with letter-spacing on top, so
   the character box is wider than `ch` reports. Filling the row is also what
   was asked for, and the digits stay centred inside it. */
.gate .tel input { flex: 1 1 auto; text-align: center; padding-left: 4px; padding-right: 4px; }

/* The release on the gate. The signed-in shells hang .ver in the clearance
   under a fixed bar and pull it up with a negative margin; there is no bar
   here, so it sits quietly under the pill on its own. */
.gate .ver { position: relative; z-index: 1; margin-top: 0; padding: 2px 0 0; text-align: center; }
@media (prefers-reduced-motion: reduce) { .btn8 { transition: none; } }

/* ── App frame ────────────────────────────────────────────── */
.app { display: grid; grid-template-columns: 236px 1fr; min-height: 100vh; }
.rail { background: var(--rail); border-right: 1px solid var(--ink-12); padding: 18px 14px; display: flex; flex-direction: column; gap: 18px; position: sticky; top: 0; height: 100vh; overflow-y: auto; }
.brand { display: flex; align-items: center; gap: 10px; padding: 4px 8px; text-decoration: none; }
.brand .mark { width: 30px; height: 30px; border-radius: 7px; background: var(--coral); display: grid; place-items: center; color: #0B0D0A; }
.brand .mark .pix { width: 13px; height: auto; }
.brand b { font-weight: 700; letter-spacing: -0.01em; }
.brand small { display: block; font-size: 11.5px; color: var(--ink-62); font-weight: 500; }
.tripsel { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-2); background: var(--paper); border: 1px solid var(--ink-12); cursor: pointer; text-align: left; width: 100%; text-decoration: none; }
.tripsel .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-30); flex-shrink: 0; }
.tripsel .dot.live { background: var(--good); }
.tripsel .dot.soon { background: var(--coral); }
.tripsel b { font-weight: 600; font-size: 14px; display: block; line-height: 1.25; }
.tripsel span { font-size: 12px; color: var(--ink-62); }
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav .grp { padding: 14px 10px 6px; }
.nav a { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 10px; text-decoration: none; font-weight: 500; color: var(--ink-70); position: relative; }
.nav a svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
.nav a:hover { background: var(--ink-08); color: var(--ink); }
.nav a.on { background: var(--ink); color: var(--cream); }
.nav a .cnt { margin-left: auto; font-size: 11.5px; font-weight: 700; padding: 1px 7px; border-radius: var(--r-pill); background: var(--coral); color: #0B0D0A; }
.nav a.dim { opacity: 0.55; }
.rail .foot { margin-top: auto; display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; color: var(--ink-62); padding: 0 8px; }
.rail .foot a { color: inherit; }
/* The phone's last tab. Styled as a nav link so the bar reads as one row of
   the same thing, and hidden entirely where the whole rail is already up. */
.nav .more { display: none; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 10px; font: inherit; font-weight: 500; color: var(--ink-70); background: none; border: 0; cursor: pointer; text-align: left; }
.nav .more svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }

.main { min-width: 0; }
/* The release, at the foot of both shells. Pulled up into .page's own bottom
   padding (60px desktop, 96px on a phone for the tab bar) with a negative
   margin, so a number nobody is looking for costs the layout nothing and, on
   a handset, still sits clear above the bar. */
.ver { margin-top: -38px; padding: 0 28px 20px; text-align: right; font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; color: var(--ink-30); }
.topbar { display: flex; align-items: center; gap: 14px; padding: 14px 28px; border-bottom: 1px solid var(--ink-12); background: var(--cream); position: sticky; top: 0; z-index: 5; }
.crumbs { display: flex; gap: 8px; align-items: center; font-size: 14px; color: var(--ink-62); min-width: 0; overflow: hidden; white-space: nowrap; }
.crumbs a { text-decoration: none; }
.crumbs b { color: var(--ink); font-weight: 600; }
.topbar .spacer { flex: 1; }
.banner { background: var(--crimson); color: #fff; font-weight: 700; padding: 10px 28px; }

.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 9px 14px; border-radius: var(--r-pill); border: 1.5px solid var(--ink-12); background: transparent; color: var(--ink); cursor: pointer; font-family: var(--display); font-weight: 600; font-size: 14px; letter-spacing: 0; line-height: 1.2; white-space: nowrap; text-decoration: none; transition: border-color .15s ease, background .15s ease; min-height: 38px; }
.btn:hover { border-color: var(--ink); background: transparent; color: var(--ink); }
.btn.ink { background: var(--ink); color: var(--cream); border-color: var(--ink); }
.btn.ink:hover { background: var(--ink); color: var(--cream); }
.btn.coral { background: var(--coral); color: #0B0D0A; border-color: var(--coral); }
.btn.coral:hover { background: var(--coral-2); border-color: var(--coral-2); color: #0B0D0A; }
.btn.danger { color: var(--bad); border-color: var(--bad-bg); }
.btn.danger:hover { border-color: var(--bad); background: transparent; color: var(--bad); }
.btn.armed { background: var(--bad); border-color: var(--bad); color: #fff; }
.btn.sm { padding: 6px 11px; font-size: 13px; min-height: 32px; }
.btn:disabled { opacity: 0.45; cursor: default; }
.btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.page { padding: 26px 28px 60px; display: flex; flex-direction: column; gap: 22px; }
.pagehead { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.pagehead p { margin: 4px 0 0; color: var(--ink-62); max-width: 62ch; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* ── Chips ────────────────────────────────────────────────── */
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: var(--r-pill); font-family: var(--display); font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; background: var(--ink-08); color: var(--ink-70); border: 0; }
.chip.good { background: var(--good-bg); color: var(--good); }
.chip.warn { background: var(--warn-bg); color: var(--warn); }
.chip.bad { background: var(--bad-bg); color: var(--bad); }
.chip.peach { background: var(--peach); color: var(--ink); }
.chip.inkc { background: var(--ink); color: var(--cream); }

/* ── Panels, stats, lists ─────────────────────────────────── */
.panel { background: var(--paper); border: 1px solid var(--ink-12); border-radius: var(--r-3); }
.panel .ph { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px 0; }
.panel .ph a { font-size: 13px; color: var(--ink-62); }
.panel .pb { padding: 14px 20px 20px; }
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.stat { padding: 16px 18px; border-radius: var(--r-2); background: var(--paper); border: 1px solid var(--ink-12); text-decoration: none; display: block; }
.stat .k { font-size: 12.5px; color: var(--ink-62); font-weight: 500; }
.stat .v { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; margin-top: 2px; line-height: 1.1; }
.stat .s { font-size: 12.5px; color: var(--ink-62); margin-top: 6px; }
.stat.hot { background: var(--peach); border-color: transparent; }
.two { display: grid; grid-template-columns: 1.4fr 1fr; gap: 18px; }
.list { display: flex; flex-direction: column; }
.li { display: flex; gap: 14px; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid var(--ink-08); }
.li:last-child { border-bottom: 0; }
.li.click { cursor: pointer; }
.li.click:hover .t b { text-decoration: underline; }
.li .who { width: 34px; height: 34px; border-radius: 50%; background: var(--cream-2); display: grid; place-items: center; font-weight: 700; font-size: 13px; flex-shrink: 0; border: 1px solid var(--ink-12); }
.li .who.blush { background: var(--blush); }
.li .who.peach { background: var(--peach); }
.li .t { flex: 1; min-width: 0; }
.li .t b { font-weight: 600; }
.li .t p { margin: 2px 0 0; color: var(--ink-70); font-size: 14px; overflow-wrap: anywhere; }
.li .t small { color: var(--ink-62); font-size: 12.5px; }
.li .act { display: flex; gap: 6px; flex-shrink: 0; }
.empty { color: var(--ink-62); font-size: 14px; }
.note { font-size: 13px; color: var(--ink-62); border-left: 2px solid var(--coral); padding-left: 12px; }
.note.bad { border-color: var(--bad); color: var(--bad); }
.msg { font-size: 14px; min-height: 0; }
.msg.bad { color: var(--bad); }
.msg.good { color: var(--good); }
.msg.warn { color: var(--warn); }
.msg:empty { display: none; }
.kv { display: grid; grid-template-columns: 180px 1fr; gap: 10px 16px; font-size: 14px; margin: 0; }
.kv dt { color: var(--ink-62); }
.kv dd { margin: 0; overflow-wrap: anywhere; }
.pin { font-family: var(--mono); font-size: 20px; font-weight: 700; letter-spacing: 0.1em; }

/* ── Forms ────────────────────────────────────────────────── */
.fs { display: flex; flex-direction: column; gap: 10px; }
.fs .eyebrow { display: flex; justify-content: space-between; align-items: center; }
.f { display: grid; grid-template-columns: 110px 1fr; gap: 10px; align-items: start; }
.f.one { grid-template-columns: 1fr; }
.f label { font-size: 13px; color: var(--ink-62); font-weight: 500; padding-top: 9px; }
.f.one label { padding-top: 0; }
.f input, .f select, .f textarea, .fs input.wide { width: 100%; padding: 9px 11px; border-radius: 10px; border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font-size: 15px; }
.f input:focus, .f select:focus, .f textarea:focus { outline: none; border-color: var(--coral); }
.f input:disabled, .f textarea:disabled { opacity: 0.6; }
.f textarea { min-height: 72px; resize: vertical; font-family: var(--mono); font-size: 13px; line-height: 1.5; }
.f .pair { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
.f .hint { grid-column: 2; font-size: 12px; color: var(--ink-62); margin-top: -4px; }
.gauge { flex: 1; height: 5px; border-radius: 3px; background: var(--ink-08); overflow: hidden; min-width: 30px; }
.gauge i { display: block; height: 100%; background: var(--good); }
.gauge.limit i { background: var(--coral-2); }
.gauge.over i { background: var(--crimson); }

/* ── Trips list ───────────────────────────────────────────── */
.tripgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.tcard { padding: 18px 20px; border-radius: var(--r-3); background: var(--paper); border: 1px solid var(--ink-12); text-decoration: none; display: flex; flex-direction: column; gap: 10px; min-height: 150px; color: var(--ink); }
.tcard:hover { border-color: var(--ink); }
.tcard h3 { font-size: 17px; font-weight: 700; letter-spacing: -0.015em; }
.tcard .meta { font-size: 13px; color: var(--ink-62); }
.tcard .bar { margin-top: auto; display: flex; gap: 6px; flex-wrap: wrap; }
.tcard.live { background: var(--ink); color: var(--cream); border-color: transparent; }
.tcard.live .meta, .tcard.live .eyebrow { color: #F4EEDF9E; }
.tcard.dim { opacity: 0.6; }
.section-h { display: flex; align-items: baseline; gap: 10px; margin-top: 8px; }
.section-h .eyebrow { margin: 0; }
.section-h small { color: var(--ink-62); }

/* ── Calendar ─────────────────────────────────────────────── */
.caltools { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.seg { display: inline-flex; border: 1.5px solid var(--ink-12); border-radius: var(--r-pill); padding: 2px; }
.seg button { border: 0; background: transparent; padding: 6px 12px; border-radius: var(--r-pill); font-weight: 600; font-size: 13px; cursor: pointer; color: var(--ink-62); }
.seg button.on { background: var(--ink); color: var(--cream); }
.legend { display: flex; gap: 14px; font-size: 12.5px; color: var(--ink-62); margin-left: auto; flex-wrap: wrap; }
.legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; vertical-align: -1px; margin-right: 5px; }
.legend i.r { border-radius: 50%; }
.month { margin-bottom: 18px; }
.month h3 { font-size: 15px; font-weight: 600; margin: 0 0 8px 4px; color: var(--ink-70); }
.cal { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
.dow { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-50); font-weight: 600; padding: 0 4px 4px; }
.cell { min-height: 108px; border-radius: 12px; background: var(--paper); border: 1px solid var(--ink-12); padding: 8px 9px 9px; display: flex; flex-direction: column; gap: 4px; cursor: pointer; text-align: left; transition: transform .12s ease, border-color .12s ease; color: var(--ink); font: inherit; }
.cell:hover { border-color: var(--ink); transform: translateY(-1px); }
.cell.blank { background: transparent; border-style: dashed; cursor: default; }
.cell.blank:hover { transform: none; border-color: var(--ink-12); }
.cell.rest { background: var(--cream-2); }
.cell.today { outline: 2px solid var(--coral); outline-offset: 1px; }
.cell.sel { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.cell .d { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--ink-62); font-weight: 600; }
.cell .d .n { font-weight: 700; color: var(--ink); }
.cell .city { font-weight: 700; font-size: 14px; letter-spacing: -0.01em; line-height: 1.2; overflow-wrap: anywhere; }
.cell .sub { font-size: 12px; color: var(--ink-62); line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.cell .foot { margin-top: auto; display: flex; align-items: center; gap: 6px; }
.cell .h { font-size: 11px; color: var(--ink-62); font-weight: 600; }
.flags { display: inline-flex; gap: 3px; align-items: center; margin-right: 6px; vertical-align: middle; }
.flag { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.flag.edit { background: var(--coral); }
.flag.prop { background: var(--crimson); }
.flag.drift { background: var(--warn); }
.bike { font-size: 11px; font-weight: 700; color: var(--ink-70); }
.agenda { display: flex; flex-direction: column; padding: 6px 12px; }
.ag { display: grid; grid-template-columns: 90px 1fr 180px 160px 130px; gap: 14px; align-items: center; padding: 12px 10px; border-bottom: 1px solid var(--ink-08); cursor: pointer; text-align: left; background: transparent; border-left: 0; border-right: 0; border-top: 0; color: var(--ink); width: 100%; }
.ag:last-child { border-bottom: 0; }
.ag:hover { background: var(--ink-08); }
.ag .dd b { display: block; font-size: 15px; }
.ag .dd span { font-size: 12px; color: var(--ink-62); }
.ag .rt { font-size: 13.5px; color: var(--ink-70); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ag .rt b { color: var(--ink); font-weight: 600; }
.ag .c4 { display: flex; align-items: center; gap: 8px; }
.ag .c5 { display: flex; gap: 4px; flex-wrap: wrap; }

/* ── Day drawer ───────────────────────────────────────────── */
.scrim { position: fixed; inset: 0; background: #0B0D0A66; opacity: 0; pointer-events: none; transition: opacity .18s ease; z-index: 20; }
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(580px, 100vw); background: var(--paper); border-left: 1px solid var(--ink-12); box-shadow: var(--shadow); transform: translateX(100%); transition: transform .22s ease; z-index: 21; display: flex; flex-direction: column; }
.open .scrim { opacity: 1; pointer-events: auto; }
.open .drawer { transform: none; }
@media (prefers-reduced-motion: reduce) { .drawer, .scrim, .cell, .toast { transition: none; } }
.dh { padding: 20px 24px 14px; border-bottom: 1px solid var(--ink-12); display: flex; flex-direction: column; gap: 10px; }
.dh .row1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dh .row1 .x { margin-left: auto; }
.dh h2 { font-size: 22px; letter-spacing: -0.02em; }
.dh .sub { color: var(--ink-62); font-size: 14px; }
.dbody { padding: 18px 24px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 22px; }
.place { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 12px; border: 1.5px solid var(--ink-12); background: var(--cream); }
.place .pn { flex: 1; min-width: 0; }
.place .pn b { display: block; font-weight: 600; font-size: 14px; }
.place .pn small { color: var(--ink-62); font-size: 12px; }
.dfoot { padding: 14px 24px; border-top: 1px solid var(--ink-12); display: flex; gap: 10px; align-items: center; background: var(--paper); flex-wrap: wrap; }
.dfoot .status { font-size: 13px; color: var(--ink-62); margin-right: auto; }
.dfoot .status b { color: var(--warn); font-weight: 600; }
.toast { position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 20px); background: var(--ink); color: var(--cream); padding: 12px 18px; border-radius: var(--r-pill); font-weight: 600; opacity: 0; transition: all .2s ease; z-index: 30; pointer-events: none; font-size: 14px; max-width: min(90vw, 640px); text-align: center; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.bad { background: var(--crimson); color: #fff; }

/* ── Settings ─────────────────────────────────────────────── */
.settings { display: grid; grid-template-columns: 1fr; gap: 18px; max-width: 760px; }

/* ── The rail by tier, Today, History (docs/2026-09-05-admin-v2.md) ── */
.nav .grp { display: flex; align-items: baseline; gap: 8px; }
.nav .grp small { font-size: 10.5px; letter-spacing: 0.04em; text-transform: none; font-weight: 500; color: var(--ink-50); }
.nav a.rec { color: var(--ink-50); font-weight: 500; }
.nav a.rec:hover { color: var(--ink); }
.nav a.rec.on { color: var(--cream); }
.today { display: grid; grid-template-columns: 1.5fr 1fr; gap: 18px; }
.daycard { background: var(--ink); color: var(--cream); border-radius: var(--r-3); padding: 22px 24px; display: flex; flex-direction: column; gap: 14px; }
.daycard .eyebrow { color: var(--cream); opacity: 0.62; }
.daycard h2 { font-size: 22px; letter-spacing: -0.02em; overflow-wrap: anywhere; }
.daycard .rows { display: grid; grid-template-columns: 84px 1fr; gap: 6px 14px; font-size: 14px; margin: 0; }
.daycard .rows dt { color: var(--cream); opacity: 0.62; }
.daycard .rows dd { margin: 0; overflow-wrap: anywhere; }
.daycard .loadline { display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--cream); opacity: 0.85; }
.daycard .loadline .gauge { flex: 1; height: 6px; background: rgba(255, 250, 240, 0.15); }
.daycard .acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.daycard .btn { border-color: rgba(255, 250, 240, 0.25); color: var(--cream); }
.daycard .btn:hover { border-color: var(--cream); }
.daycard .btn.coral { color: #0B0D0A; border-color: var(--coral); }
.line { display: flex; align-items: center; gap: 10px; font-size: 14px; padding: 10px 0; border-bottom: 1px solid var(--ink-08); flex-wrap: wrap; }
.line:last-child { border-bottom: 0; }
.line .k { color: var(--ink-62); min-width: 110px; }
.line .hist { margin-left: auto; font-size: 12.5px; color: var(--ink-50); text-decoration: none; }
.line .hist:hover { color: var(--ink); text-decoration: underline; }
.tick { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--ink-50); }
.tick i { width: 7px; height: 7px; border-radius: 50%; background: var(--good); display: inline-block; }
.tick.fail { color: var(--bad); } .tick.fail i { background: var(--bad); }
.tick.wait i { background: var(--warn); }
.panel .ph .search { flex: 1; min-width: 0; padding: 8px 11px; border-radius: 10px; border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font-size: 14px; }
.panel .ph .search:focus { outline: none; border-color: var(--coral); }
.toggle-line { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-62); white-space: nowrap; }
.tl { display: flex; flex-direction: column; }
.ev { display: grid; grid-template-columns: 130px 96px 1fr auto; gap: 14px; align-items: baseline; padding: 9px 4px; border-bottom: 1px solid var(--ink-08); font-size: 13.5px; color: var(--ink-70); }
.ev:last-child { border-bottom: 0; }
.ev .t { color: var(--ink-50); font-variant-numeric: tabular-nums; font-size: 12.5px; white-space: nowrap; }
.ev .kind { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; color: var(--ink-50); }
.ev .body { min-width: 0; }
.ev .body b { color: var(--ink); font-weight: 600; }
.ev .body p { margin: 2px 0 0; overflow-wrap: anywhere; }
.ev .lk a { font-size: 12.5px; color: var(--ink-50); }
@media (max-width: 980px) {
  .today { grid-template-columns: 1fr; }
  .ev { grid-template-columns: 1fr; gap: 2px; }
  .ev .kind { display: none; }
}

/* ── Conversations (phase 2 of the second pass; a phone-shaped screen since Sept 6 2026) ──
   The list and the thread fill the height app.js measures for them (sizeConvo)
   and scroll on their own; the compose is pinned to the bottom of the thread.
   Under 980px the thread is a screen of its own that slides over the list
   (.convo.open), pushed onto history so the back gesture returns to the list. */
.convo { display: grid; grid-template-columns: 320px 1fr; background: var(--paper); border: 1px solid var(--ink-12); border-radius: var(--r-3); overflow: hidden; min-height: 460px; position: relative; }
.threads { border-right: 1px solid var(--ink-12); display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; min-height: 0; }
.threads .lh { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 16px; background: var(--paper); border-bottom: 1px solid var(--ink-08); }
.threads .lh b { font-size: 15px; display: block; }
.threads .lh small { font-size: 12px; color: var(--ink-62); }
.th { display: grid; grid-template-columns: 38px 1fr auto; gap: 10px; padding: 12px 16px; border: 0; border-bottom: 1px solid var(--ink-08); cursor: pointer; text-align: left; background: transparent; width: 100%; font: inherit; color: inherit; transition: background .15s ease; }
.th:hover { background: var(--ink-08); }
.th.on { background: var(--cream-2); box-shadow: inset 3px 0 0 var(--coral); }
.th.dim { opacity: 0.6; }
.th.heldth { box-shadow: inset 3px 0 0 var(--crimson); }
.th .av, .thread .hd .av { width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center; font-weight: 700; font-size: 14px; background: var(--cream-2); border: 1px solid var(--ink-12); flex-shrink: 0; }
.th .av.peach, .thread .hd .av.peach { background: var(--peach); }
.th.party .av { background: var(--ink); color: var(--cream); border-color: transparent; }
/* Every morning (Sept 7 2026): the standing daily message as a thread of
   its own at the foot of the list, since it is a conversation Rova has
   with the whole party every day. Coral rather than ink, because it is the
   one row that is about what WILL be said rather than what was. */
.th.daily .av.sun, .thread .hd .av.sun { background: var(--coral); color: var(--cream); border-color: transparent; font-size: 17px; }
/* The editor IS the pane, so it scrolls like a thread and carries the
   panel's padding itself -- there is no compose pinned under it, because
   the daily message is saved rather than sent. */
.msgs.dailypane { padding: 14px 16px 20px; display: block; }
.msgs.dailypane .f { margin-bottom: 12px; }
.msgs.dailypane textarea { width: 100%; }
.th b { font-size: 14px; font-weight: 600; display: block; }
.th p { margin: 2px 0 0; font-size: 12.5px; color: var(--ink-62); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.th > div { min-width: 0; }
.th .when { font-size: 11.5px; color: var(--ink-50); white-space: nowrap; }
.thread { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--paper); }
.thread .pane { display: flex; flex-direction: column; flex: 1; min-height: 0; animation: paneIn .2s ease; }
@keyframes paneIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.thread .hd { padding: 12px 16px; border-bottom: 1px solid var(--ink-08); display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
/* The title truncates the way its subtitle already did. .hdacts never
   shrinks, so without this a wide chip beside a long name (Every morning,
   Sept 7 2026) wrapped the h2 onto two lines and the chip sat on top of
   it at 375px. */
.thread .hd h2 { font-size: 16px; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread .hd .who { min-width: 0; display: flex; flex-direction: column; }
.thread .hd .who span { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread .hd .btn { margin-left: auto; }
.thread .hd .back { display: none; margin-left: 0; padding: 6px; min-height: 36px; width: 36px; border-color: transparent; }
.thread .hd .back svg { width: 20px; height: 20px; }
/* Two controls in the header once a thread can be taken over, so the pair goes
   to the right together rather than the first one claiming the whole gap. */
.thread .hd .hdacts { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
.thread .hd .hdacts .btn { margin-left: 0; }
/* The send time is in the editor below and on the thread's own row, so on
   a phone the header does not spend a third of its width restating it. */
@media (max-width: 979px) { .thread .hd .hdacts .chip { display: none; } }
/* Text login link beside Take over (Sept 18 2026): on a phone it is its key
   alone, or the two buttons squeeze the name to two letters. Armed, the words
   come back, so the confirming tap says what it confirms. */
.thread .hd .hdacts .ll { gap: 6px; }
@media (max-width: 979px) { .thread .hd .hdacts .ll:not(.armed) span { display: none; } .thread .hd .hdacts .ll:not(.armed) { padding-left: 10px; padding-right: 10px; } }

/* A held thread says so, loudly and in the reader's way, because a takeover
   nobody remembers is a traveller texting a void (048). Crimson rather than
   coral: coral is this console's ordinary accent and would read as decoration. */
.held { padding: 10px 16px; background: color-mix(in srgb, var(--crimson) 8%, var(--paper)); border-bottom: 1px solid color-mix(in srgb, var(--crimson) 22%, transparent); font-size: 13px; line-height: 1.45; flex-shrink: 0; }
.held b { color: var(--crimson); }
.compose.held { border-top-color: color-mix(in srgb, var(--crimson) 22%, transparent); background: color-mix(in srgb, var(--crimson) 4%, var(--paper)); }
.compose.held textarea:focus { border-color: var(--crimson); }
.nav a .cnt.held { background: var(--crimson); color: var(--cream); }
.msgs { padding: 18px 16px; display: flex; flex-direction: column; gap: 10px; flex: 1; overflow-y: auto; overscroll-behavior: contain; min-height: 0; }
.div { text-align: center; font-size: 11.5px; color: var(--ink-50); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; margin: 8px 0 2px; }
.m { max-width: 76%; display: flex; flex-direction: column; gap: 4px; }
.m .b { padding: 10px 14px; border-radius: 18px; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; white-space: pre-wrap; }
.m.in { align-self: flex-start; } .m.in .b { background: var(--cream-2); border-bottom-left-radius: 5px; }
.m.out { align-self: flex-end; align-items: flex-end; } .m.out .b { background: var(--ink); color: var(--cream); border-bottom-right-radius: 5px; }
.m.out.auto .b { background: var(--peach); color: var(--ink); }
.m small { font-size: 11.5px; color: var(--ink-50); display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.m small .chip { padding: 1px 6px; font-size: 10px; }
.m small .lnk { border: 0; background: transparent; color: var(--ink-50); text-decoration: underline; cursor: pointer; font: inherit; padding: 0; }
/* The compose is pinned: the thread scrolls above it and it stays under the
   thumb. The box grows with the words to a few lines (app.js growWith). */
.compose { padding: 10px 12px 10px 12px; border-top: 1px solid var(--ink-08); display: flex; gap: 8px; align-items: flex-end; background: var(--paper); flex-shrink: 0; }
.compose textarea { flex: 1; min-width: 0; padding: 10px 14px; border-radius: 20px; border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font-size: 15px; line-height: 1.35; font-family: var(--display); resize: none; min-height: 42px; max-height: 132px; }
.compose textarea:focus { outline: none; border-color: var(--coral); }
.compose .btn { flex-shrink: 0; min-height: 42px; }
.compose .vtoggle { flex-shrink: 0; width: 42px; height: 42px; border-radius: 50%; border: 1.5px solid var(--ink-12); background: transparent; color: var(--ink-62); font-family: var(--mono); font-size: 14px; cursor: pointer; transition: border-color .15s ease, background .15s ease, color .15s ease; }
.compose .vtoggle:hover, .compose .vtoggle.on { border-color: var(--ink); color: var(--ink); }
.compose .vtoggle.on { background: var(--cream-2); }
.ooOut { padding: 0 16px; flex-shrink: 0; max-height: 200px; overflow-y: auto; } .ooOut:empty { display: none; } .ooOut .place { margin-bottom: 10px; }
.msg#msg-oneoff, .msg#msg-reply { padding: 0 16px 8px; flex-shrink: 0; }
@media (max-width: 980px) {
  .convo { grid-template-columns: 1fr; }
  .convo { border: 0; border-radius: 0; min-height: 320px; }
  /* .page.convo-page, not .convo-page: the responsive block below restates
     .page's padding at the same specificity and would win by coming later. */
  .page.convo-page { padding: 0; gap: 0; }
  .page.convo-page .pagehead, .page.convo-page > .note { display: none; }
  .threads { position: absolute; inset: 0; border-right: 0; transition: transform .28s cubic-bezier(.2, .8, .2, 1), opacity .28s ease; }
  .thread { position: absolute; inset: 0; transform: translateX(100%); transition: transform .28s cubic-bezier(.2, .8, .2, 1); box-shadow: -18px 0 40px -28px #0B0D0A; }
  .convo.open .thread { transform: none; }
  .convo.open .threads { transform: translateX(-24%); opacity: 0.55; pointer-events: none; }
  .thread .pane { animation: none; }
  .thread .hd .back { display: inline-flex; }
  .m { max-width: 86%; }
}
@media (prefers-reduced-motion: reduce) { .thread, .threads, .th, .compose .vtoggle { transition: none; } .thread .pane { animation: none; } }

/* ── A sheet (Sept 6 2026): a bottom sheet on a phone, a centred card on a desktop ── */
.sheetscrim { position: fixed; inset: 0; background: #0B0D0A66; opacity: 0; pointer-events: none; transition: opacity .2s ease; z-index: 40; }
.sheetscrim.show { opacity: 1; pointer-events: auto; }
.sheet { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -46%); width: min(560px, calc(100vw - 32px)); max-height: min(86vh, 760px); background: var(--paper); border: 1px solid var(--ink-12); border-radius: var(--r-3); box-shadow: var(--shadow); z-index: 41; opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .24s cubic-bezier(.2, .8, .2, 1); display: flex; flex-direction: column; overflow: hidden; }
.sheet.show { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%); }
.sheet .sh { padding: 16px 20px 12px; border-bottom: 1px solid var(--ink-08); display: flex; align-items: flex-start; gap: 12px; }
.sheet .sh h2 { font-size: 20px; letter-spacing: -0.02em; }
.sheet .sh .x { margin-left: auto; }
.sheet .sb { padding: 16px 20px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 14px; min-height: 0; }
.sheet .sb .f textarea { font-family: var(--display); font-size: 15px; min-height: 84px; }
.sheet .sb .vars { margin-top: 8px; }
.sheet .sf { padding: 12px 20px; border-top: 1px solid var(--ink-08); display: flex; gap: 10px; align-items: center; background: var(--paper); }
.sheet .sf .spacer { flex: 1; }
.sheet .ooOut { padding: 0; max-height: none; }
.people { display: flex; flex-wrap: wrap; gap: 8px; }
.person { display: inline-flex; align-items: center; gap: 8px; padding: 5px 14px 5px 5px; border-radius: var(--r-pill); border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); cursor: pointer; font: inherit; text-align: left; transition: border-color .15s ease, background .15s ease, color .15s ease; min-height: 40px; }
.person .av { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; font-weight: 700; font-size: 12px; background: var(--cream-2); border: 1px solid var(--ink-12); flex-shrink: 0; }
.person .av.peach { background: var(--peach); }
.person .pn b { display: block; font-size: 14px; font-weight: 600; line-height: 1.15; }
.person .pn small { font-size: 11.5px; color: var(--ink-62); }
.person:hover { border-color: var(--ink); }
.person.on { border-color: var(--ink); background: var(--ink); color: var(--cream); }
.person.on .pn small { color: var(--cream); opacity: 0.7; }
.person.on .av { background: var(--coral); color: #0B0D0A; border-color: transparent; }
.person.all { padding: 5px 14px; font-weight: 600; }
.switch { display: flex; align-items: flex-start; gap: 12px; cursor: pointer; font-size: 13.5px; line-height: 1.4; color: var(--ink-70); }
.switch b { color: var(--ink); }
.switch input { appearance: none; -webkit-appearance: none; flex-shrink: 0; width: 42px; height: 26px; border-radius: 13px; background: var(--ink-12); position: relative; margin: 0; cursor: pointer; transition: background .2s ease; }
.switch input::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--paper); box-shadow: 0 1px 3px #0B0D0A4D; transition: transform .2s ease; }
.switch input:checked { background: var(--crimson); }
.switch input:checked::after { transform: translateX(16px); }
@media (max-width: 700px) {
  .sheet { left: 0; right: 0; top: auto; bottom: 0; width: auto; max-height: 92dvh; border-radius: 20px 20px 0 0; border-bottom: 0; transform: translateY(100%); }
  .sheet.show { transform: none; }
}
@media (prefers-reduced-motion: reduce) { .sheet, .sheetscrim, .person, .switch input, .switch input::after { transition: none; } }

/* ── The checklist (Sept 7 2026) ──────────────────────────────────────────
   The last card of the old console to get a successor here. The tick sits in
   the .li's `who` slot, so it is sized and aligned like every other avatar on
   a list row rather than being a loose checkbox in a column of circles. */
.clbox { width: 34px; height: 34px; flex-shrink: 0; display: grid; place-items: center; cursor: pointer; }
.clbox input { appearance: none; -webkit-appearance: none; width: 22px; height: 22px; margin: 0; cursor: pointer;
  border: 1.5px solid var(--ink-30); border-radius: 7px; background: var(--cream); position: relative; transition: background .15s ease, border-color .15s ease; }
.clbox input:hover { border-color: var(--ink); }
.clbox input:checked { background: var(--good); border-color: transparent; }
.clbox input:checked::after { content: ''; position: absolute; left: 7px; top: 3px; width: 5px; height: 10px;
  border: solid #0B0D0A; border-width: 0 2.5px 2.5px 0; transform: rotate(45deg); }
.clbox input:focus-visible { outline: 2px solid var(--coral); outline-offset: 2px; }
/* .pair is scoped to `.f .pair`, so a list row needs its own. */
.clrow { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }
.btn.ico { display: inline-grid; place-items: center; padding: 6px 9px; }
input.cltext { width: 100%; padding: 7px 10px; border-radius: 9px; border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font: inherit; font-size: 14.5px; }
input.cltext:focus { outline: none; border-color: var(--coral); }
/* A packed line reads as done without going grey enough to be hard to edit.
   :has() only -- the checkbox is inside .clbox, so a sibling combinator from
   the input cannot reach .t at all, and writing one as a fallback is dead CSS
   that reads like a fallback. Where :has() is missing the tick alone says it. */
.li:has(.clbox input:checked) .cltext { color: var(--ink-62); text-decoration: line-through; text-decoration-color: var(--ink-30); }
@media (prefers-reduced-motion: reduce) { .clbox input { transition: none; } }

/* ── The values the owner sets, under the daily message (Sept 6 2026) ── */
.setvals { display: flex; flex-direction: column; gap: 10px; }
.sv { display: grid; grid-template-columns: 150px 1fr; gap: 6px 12px; align-items: start; }
.sv code { font-family: var(--mono); font-size: 13px; padding-top: 10px; }
.sv input { width: 100%; padding: 9px 11px; border-radius: 10px; border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font-size: 15px; }
.sv input:focus { outline: none; border-color: var(--coral); }
.sv small { display: block; margin-top: 4px; font-size: 12px; color: var(--ink-62); }
.setvals-wrap label .chip { margin-left: 6px; vertical-align: middle; }
@media (max-width: 560px) { .sv { grid-template-columns: 1fr; } .sv code { padding-top: 0; } }

/* ── Responsive ───────────────────────────────────────────── */
@media (max-width: 980px) {
  .app { grid-template-columns: 1fr; }
  /* The tab bar. Ten sections plus All trips do not fit a handset at a 44px
     minimum, and the rail's foot -- Sign out and the old console -- was hidden
     outright, so until Sept 7 2026 a phone could not sign out at all. What is
     on the bar is PRIMARY plus More; More turns the bar into the whole rail. */
  .rail { position: fixed; bottom: 0; top: auto; height: auto; width: 100%; flex-direction: row; padding: 6px 8px; border-right: 0; border-top: 1px solid var(--ink-12); z-index: 10; gap: 0; overflow: visible; }
  .brand, .tripsel, .rail .foot, .nav .grp { display: none; }
  .nav { flex-direction: row; width: 100%; justify-content: space-around; }
  .nav a { flex-direction: column; gap: 3px; font-size: 11px; padding: 6px 4px; min-width: 44px; }
  .nav a .cnt { margin: 0; position: absolute; top: 0; right: 2px; }
  .nav a:not(.prim) { display: none; }
  .nav .more { display: flex; flex-direction: column; gap: 3px; font-size: 11px; padding: 6px 4px; min-width: 44px; align-items: center; }

  /* Open: the same rail, upright, over the screen. Nothing is drawn twice --
     these are the identical links with the identical badges. */
  .rail.open { top: 0; height: 100dvh; flex-direction: column; padding: 18px 14px 8px; overflow-y: auto; gap: 14px; border-top: 0; }
  .rail.open .brand, .rail.open .tripsel { display: flex; }
  .rail.open .foot { display: flex; padding-bottom: 8px; }
  .rail.open .nav { flex-direction: column; justify-content: flex-start; }
  .rail.open .nav .grp { display: flex; }
  .rail.open .nav a { flex-direction: row; gap: 10px; font-size: 14px; padding: 11px 10px; }
  .rail.open .nav a:not(.prim) { display: flex; }
  .rail.open .nav a .cnt { position: static; margin-left: auto; }
  .rail.open .nav a svg { width: 18px; height: 18px; }
  .rail.open .nav .more { flex-direction: row; gap: 10px; font-size: 14px; padding: 11px 10px; align-items: center; color: var(--ink-50); }
  body.rail-open { overflow: hidden; }
  .page { padding: 18px 16px 96px; }
  .ver { padding: 0 16px 20px; }
  .topbar { padding: 12px 16px; flex-wrap: wrap; }
  .stats, .tripgrid { grid-template-columns: 1fr 1fr; }
  .two { grid-template-columns: 1fr; }
  .cal { gap: 4px; }
  .cell { min-height: 84px; padding: 6px; }
  .cell .sub { display: none; }
  .ag { grid-template-columns: 80px 1fr; }
  .ag .c3, .ag .c4, .ag .c5 { display: none; }
  .f { grid-template-columns: 1fr; }
  .f label { padding-top: 0; }
  .f .hint { grid-column: 1; }
  .kv { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .stats, .tripgrid { grid-template-columns: 1fr; }
  /* Icons only on the tab bar: five labels do not fit a phone. The label
     stays in the DOM for screen readers and as the tooltip, and comes back
     the moment More turns the bar into the rail. */
  .nav a, .nav .more { font-size: 0; gap: 0; }
  .nav a svg, .nav .more svg { width: 22px; height: 22px; }
  .nav a .cnt { font-size: 11px; }
  .rail.open .nav a, .rail.open .nav .more { font-size: 14px; gap: 10px; }
  .rail.open .nav a svg, .rail.open .nav .more svg { width: 18px; height: 18px; }
  /* The month grid at phone width: date and town only, no "Day N", no hours. */
  .cell { min-height: 64px; padding: 5px; gap: 2px; }
  .cell .d span:last-child { font-size: 0; }
  .cell .d .flags { margin: 0; }
  .cell .city { font-size: 11px; line-height: 1.15; }
  .cell .h { display: none; }
  .legend { display: none; }
  .pagehead p { display: none; }
}

/* A date in a table stays on one line. */
.tbl .nw { white-space: nowrap; }

/* ── The third pass (Sept 6 2026): variables, the tell compose, journal thumbs ── */
.vars { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.vars .hint { flex-basis: 100%; }
.var { display: inline-flex; align-items: baseline; gap: 6px; padding: 4px 10px; border-radius: var(--r-pill); border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12.5px; cursor: pointer; line-height: 1.3; max-width: 100%; }
.var i { font-style: normal; font-family: var(--display); color: var(--ink-62); font-size: 12px; max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.var:hover { border-color: var(--ink); }
.var.off { color: var(--ink-62); border-style: dashed; background: transparent; }
.var.per { border-color: var(--coral); }
.var.live { border-color: var(--ink-12); background: var(--cream-2); }
/* The {first_name} switch on the held reply box: it is a state, not a
   chip that inserts, so it reads as pressed rather than as available. */
.var.on { background: var(--coral); border-color: var(--coral); color: var(--cream); }
/* The text Rovas. The picture IS the label, so it is drawn a size up and
   never ellipsised; the row sits under the variables in the same panel. */
.vars.art { margin-top: 8px; }
.var.pic { font-size: 14px; letter-spacing: .5px; padding: 5px 10px; }
/* The sentences you can tick (050, Sept 7 2026). A block is a STATE, not a
   chip that inserts, so it borrows .var.on's pressed look rather than
   inventing a second one -- and the label is words rather than a token, so it
   drops the mono face the variable chips use. The picture leads, at a size
   that reads on a phone without becoming the control. */
.vars.blocks { margin-top: 8px; }
.var.blk { font-family: var(--display); font-size: 13px; align-items: center; }
.var.blk .em { font-size: 14px; line-height: 1; }
.var.blk[aria-pressed="false"] { color: var(--ink-62); }
.var.blk[aria-pressed="false"]:hover { color: var(--ink); }
/* The chips, behind the { } button in the compose since Sept 6 2026: sixteen of
   them under the box pushed the box off a phone screen. Closed by default. */
.composevars { max-height: 0; overflow: hidden; padding: 0 16px; transition: max-height .22s ease, padding .22s ease; flex-shrink: 0; }
.composevars.show { max-height: 260px; overflow-y: auto; padding-bottom: 10px; }
.place.preview { border-color: var(--coral); }
.tell { margin: 6px 0 4px 48px; padding: 14px 16px; border-radius: var(--r-3); border: 1.5px solid var(--coral); background: var(--cream); display: flex; flex-direction: column; gap: 10px; }
.tell .hint { font-size: 13px; color: var(--ink-70); margin: 0; }
.tell textarea { width: 100%; padding: 9px 11px; border-radius: 10px; border: 1.5px solid var(--ink-12); background: var(--paper); color: var(--ink); font-size: 14px; font-family: var(--display); }
.tell label { padding-top: 0; display: flex; gap: 8px; align-items: center; }
.tcard img.thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 10px; background: var(--cream-2); }
@media (max-width: 700px) { .tell { margin-left: 0; } }

/* ── The proposal card in the Inbox (Sept 6 2026) ──────────────────────────
   The card itself is the site's, and it brings its own stylesheet: everything
   under .prop is defined by trips/change-day.js and is deliberately NOT
   restated here, because a second copy of a rule is the drift this whole
   arrangement exists to avoid (tools/admin-card-check.mjs).
   What is here is only the seating: the card was written to sit alone in a
   page's column, and in the Inbox it sits inside a row that already has an
   avatar, a heading and a line of buttons.
   docs/2026-09-06-one-card-two-readers.md. */
.prop-row .prop { margin: 10px 0 4px; }
/* The rail takes ~260px, so the card's 186px map column would leave the route
   line about twelve characters wide on a laptop. Below the panel's own
   breakpoint the map goes under the strip, which is what the card already does
   on a phone. */
@media (max-width: 1180px) { .prop-row .prop-lead { grid-template-columns: 1fr; } .prop-row .prop-map { max-width: 320px; } }
/* The card's own quiet button is the See-all-days control, the one thing that
   survives actions:false. It is not an answer, so it does not borrow the
   weight of the coral Accept beside it. */
.prop-row .prop-acts { margin-top: 4px; }

/* ── The phone door (session 2, 2026-09-06) ────────────────────────────────
   A second, sibling form under the adventure-code one. Ordinary size: the
   big-print rule below is for a member once signed in, not for this door,
   which anyone -- staff included, since it lives on the one shared gate --
   might land on first. */
/* The phone door is the cabinet's default face since Sept 6 2026 (.tel,
   .code and .hello above); what is left here is the small text button under
   the code field. */
.pg-resend { background: none; border: none; padding: 6px 0; font-family: inherit; font-size: 12.5px; font-weight: 600; color: var(--crimson); cursor: pointer; text-decoration: underline; text-align: center; }
.pg-resend:disabled { color: var(--ink-50); cursor: default; text-decoration: none; }

/* ══ The traveller's console, v4 ═══════════════════════════════════════════
 *
 * Ported from rova/design/console-v4/console.css, which stays where it is as
 * the sandbox this was designed in. Everything is scoped under .uc, and .uc is
 * a CONTAINER rather than the viewport: the console lays itself out against
 * the width of the box it is mounted in, not the window, which is why the same
 * file draws a phone-shaped bar and a desk rail with no media query between
 * them. app.js mounts it into #app, which becomes .uc.
 *
 * Colour, type and radii come from the head of this file. What this block adds
 * is the console's own surfaces and, above all, its MOTION: one easing set, one
 * duration set, and one rule every animation in here obeys -- it moves content
 * that is already drawn and already interactive. Nothing waits on a transition,
 * and @media (prefers-reduced-motion) at the foot switches the lot off without
 * changing a single layout.
 *
 * The staff console does not read one line of this, and body.member's big-print
 * layer is GONE with the screens it was written for: it made every control 56px
 * and every word 20px on four screens that now size themselves, and two layers
 * both claiming to own the traveller's type is how a control ends up 17px in
 * one place and 20px in another.
 */
/* The traveller's console, v4. A DESIGN SANDBOX: nothing here is loaded by
 * src/admin/shell.mjs and nothing in src/ loads it.
 *
 * Everything is scoped under .uc, and .uc is a CONTAINER rather than the
 * viewport: the console lays itself out against the width of the frame it is
 * dropped into, so the same file draws a phone inside the harness and a full
 * window with no frame at all. That is also what makes porting it back
 * cheap -- the live console mounts into #app, which becomes .uc.
 *
 * Colour, type and radii come from ../../src/admin/tokens.css. This file adds
 * the console's own surfaces and, above all, its MOTION: one easing set, one
 * duration set, and one rule that every animation in here obeys -- it moves
 * content that is already drawn and already interactive. Nothing waits on a
 * transition, and @media (prefers-reduced-motion) at the foot switches the lot
 * off without changing a single layout.
 */

/* A REAL HEIGHT ON THE HOST, AND NOT 100% (Sept 8 2026, and it cost the owner
   an evening). The console is an app SHELL: .uc-shell below is a flex item
   that gives its host nothing back (`flex: 1 1 auto` needs a definite height
   on .uc to grow into, same as absolute inset:0 did before Sept 12 2026), and
   `height: 100%` against a body of auto height computes to auto -- which here
   means ZERO, with `overflow: hidden` under it. The console mounted, painted,
   loaded Today and was clipped to a 0px box: a blank cream page, no error
   anywhere, and /api/admin/me/today answering 200 on every reload.

   It never showed in the sandbox because there the console mounts into
   .h-screen inside a device frame carrying `height: calc(100vh - 64px)`
   (design/console-v4/index.html). The v4 port carried the shell and not the
   frame that gave it a height. The trips LIST hid it for another hour: that
   screen is normal flow, so it painted perfectly while every mounted screen
   behind it was invisible.

   dvh rather than vh so a phone's collapsing address bar leaves no bar of cream
   under the tab bar; vh first for anything that has never heard of dvh. The
   staff console is untouched -- it is #app WITHOUT .uc, in normal flow, and
   this selector cannot reach it. */
.uc {
  container-type: inline-size;
  container-name: uc;
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  isolation: isolate;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--display);
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  font-variation-settings: "opsz" 14;

  /* Surfaces the tokens file has no opinion about. */
  --paper: #FFFFFF;
  --rail: #FBF3E2;
  --good: #1E7A3D;
  --warn: #9A5A12;
  --bad: #C8102E;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --lift-1: 0 1px 2px #0B0D0A14, 0 4px 12px -6px #0B0D0A1F;
  --lift-2: 0 2px 6px #0B0D0A14, 0 18px 44px -18px #0B0D0A4D;
  --lift-3: 0 -8px 60px -12px #0B0D0A59;

  /* ── The motion system, in one place ──────────────────────────────────
     Four curves and four durations, and every moving thing in this file
     names one of each. A screen whose animations were each tuned on their
     own is a screen where nothing feels like it belongs to anything else. */
  --e-out:    cubic-bezier(0.22, 1, 0.36, 1);      /* the workhorse: arrivals */
  --e-in-out: cubic-bezier(0.65, 0, 0.35, 1);      /* two-way moves */
  --e-spring: cubic-bezier(0.34, 1.42, 0.5, 1);    /* a press answering back */
  --e-exit:   cubic-bezier(0.4, 0, 1, 1);          /* leaving, which is faster */
  --d-1: 140ms;   /* a press, a tint */
  --d-2: 240ms;   /* a control moving */
  --d-3: 380ms;   /* a screen or a sheet */
  --d-4: 560ms;   /* the line drawing itself */
}
/* ── The dark theme ───────────────────────────────────────────────────────
   The same trick admin.css uses: no component knows which theme it is in,
   because the theme re-points the TOKENS and nothing else. Two selectors and
   not one -- the media query is the system setting, and [data-theme] is a
   deliberate choice that has to win in both directions, which is why the media
   query excludes an explicit light.

   The two surfaces .uc declares for itself need their own pair of rules, since
   a declaration on .uc beats one on :root however specific the :root rule is.
   That is the whole cost of scoping the component's own surfaces to it. */
@media (prefers-color-scheme: dark) {
  /* The dark theme re-points the TOKENS and nothing else, and it does that
     once at the head of this file. The sandbox carried its own copy because
     it had no admin.css to read them from; two copies of one palette is one
     palette that drifts, so only the .uc surfaces below are declared here. */
  :root:not([data-theme="light"]) .uc {
    --paper: #1C1D16; --rail: #101109;
    --good: #6FCF8C; --warn: #FFB27A; --bad: #FF6B7E;
    --lift-1: 0 1px 2px #00000066, 0 4px 12px -6px #00000099;
    --lift-2: 0 2px 6px #00000066, 0 18px 44px -18px #000000A6;
    --lift-3: 0 -8px 60px -12px #000000BF;
  }
}
:root[data-theme="dark"] .uc {
  --paper: #1C1D16; --rail: #101109;
  --good: #6FCF8C; --warn: #FFB27A; --bad: #FF6B7E;
  --lift-1: 0 1px 2px #00000066, 0 4px 12px -6px #00000099;
  --lift-2: 0 2px 6px #00000066, 0 18px 44px -18px #000000A6;
  --lift-3: 0 -8px 60px -12px #000000BF;
}
.uc *, .uc *::before, .uc *::after { box-sizing: border-box; }
/* :where() and NOT a bare `.uc button`. A bare one is (0,1,1) and every
   component rule below it is (0,1,0), so `font: inherit` beat every
   font-size and font-weight this file sets on a button: the tab labels
   drew at the shell's own 17px/400 instead of 11.5px/600, and so did the
   segmented control, the chips and the sheet's buttons. :where() is zero,
   so the reset stays a reset. */
.uc :where(button, input, textarea, select) { font: inherit; color: inherit; }
.uc h1, .uc h2, .uc h3 { margin: 0; letter-spacing: -0.025em; text-wrap: balance; }
.uc p { margin: 0; }
.uc :focus-visible { outline: 3px solid var(--coral); outline-offset: 2px; border-radius: 6px; }
.uc [hidden] { display: none !important; }
/* Chrome is not text. A long press on a tab, the bar or a control should do
   what it does in an app -- nothing -- rather than start a selection and put
   a grey highlight box on the screen. The sections' own words are untouched:
   an entry, a stop and a forecast are all still selectable and copyable. */
.uc-head, .uc-nav, .uc-brand, .uc button, .uc .uc-grab {
  -webkit-user-select: none; user-select: none;
}
.uc button, .uc [role="tab"], .uc a { -webkit-tap-highlight-color: transparent; }
.uc-vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ══ The shell ═════════════════════════════════════════════════════════════
   A bar at the top that never moves, a scroller per section that keeps its own
   place, and a tab bar that is a bottom bar on a handset and a rail on a desk.
   The shell scales back when a sheet comes up, which is the one thing that
   makes a sheet read as being IN FRONT rather than merely on top. */
.uc-shell {
  /* A FLEX ITEM, not `position: absolute; inset: 0`. Absolute ignored the
     stand-in bar's height entirely: .sib is this element's own flow sibling
     inside .uc, so an inset-0 shell always started at .uc's top and drew its
     head/nav UNDER the bar, whatever the bar's height was. flex: 1 1 auto
     lets .uc's column layout stack the bar above and give the shell what is
     left; min-height: 0 is what lets a flex item with overflow:hidden
     children shrink below its content size (same reason .uc-body carries it). */
  position: relative; flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  background: var(--cream);
  transform-origin: 50% 0%;
  transition: transform var(--d-3) var(--e-out), border-radius var(--d-3) var(--e-out), filter var(--d-3) var(--e-out);
  /* NO will-change HERE. A will-change on a transform makes this element a
     BACKDROP ROOT, and a backdrop root is where a descendant's backdrop-filter
     stops sampling: the head and the tab bar both blur what is under them, and
     with this line in they were a flat tint over a perfectly sharp map. The
     transition composites on its own. */
}
.uc.is-sheeted .uc-shell {
  transform: scale(0.935) translateY(10px);
  border-radius: 22px;
  overflow: hidden;
  filter: brightness(0.86);
}

/* ── The bar ─────────────────────────────────────────────────────────────
   52px, always there, and translucent over whatever is scrolling under it.
   Its hairline and its tint come up with --p, which the scroller writes: a
   bar that is opaque over an unscrolled screen is a bar with a seam in it. */
.uc-head {
  position: absolute; top: 0; left: 0; right: 0; z-index: 20;
  height: calc(52px + env(safe-area-inset-top));
  padding-top: env(safe-area-inset-top);
  display: flex; align-items: center; gap: 8px;
  padding-left: 16px; padding-right: 16px;
  --p: 0;
  /* THE BLUR IS ON THE ELEMENT AND THE TINT IS ON THE PSEUDO. On a
     z-index: -1 pseudo the blur samples nothing in Chrome, so the bar was a
     flat wash over a perfectly sharp map. The tint is what fades in with --p;
     the blur may stand the whole time, because at the top of a scroller there
     is only the page's own ground under it and blurring one colour returns it. */
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
}
.uc-head::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  /* The SAME alpha the tab bar uses. Two frosted panes that meet edge to edge
     read as one pane only if they are the same colour; 78 against the bar's 84
     drew a band across the top of the screen. */
  background: color-mix(in srgb, var(--cream) 84%, transparent);
  border-bottom: 1px solid var(--ink-12);
  opacity: var(--p);
  transition: opacity var(--d-1) linear;
}
/* THE BAR IS THE TOP EDGE ON A HANDSET, and the title band sits under it
   (owner, v4 fifth pass, as a consequence of Rova leaving the bar). With her
   gone the band holds nothing until you scroll, and 52px of empty above the
   tabs read as a broken toolbar; the same 52px below them read as the page's
   own top margin, and then the day's title fades into exactly the place the
   day begins. Nothing changed height and nothing is hidden: the scroller
   reserves both, as it did.

   Its hairline is the lower edge of the chrome now, so it comes back -- and
   it comes and goes with --p, which is right: at rest there is one rule under
   the tabs, and scrolled there are two bands with an edge each. */
@container uc (max-width: 899px) {
  .uc-head { top: var(--navh, 70px); height: 52px; padding-top: 0; }
}
.uc-head-compact {
  font-size: 17px; font-weight: 700; letter-spacing: -0.02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  opacity: var(--p);
  transform: translateY(calc((1 - var(--p)) * 7px));
  transition: opacity var(--d-1) linear, transform var(--d-2) var(--e-out);
}

/* THE REAL SPRITE, wherever she appears. rova.js mounts one of her moments as
   an <img> and moves her from moment to moment, and it writes the box's own
   width and height as inline styles off her frame table -- so nothing here may
   set them. What is here is only the pixels.

   She is in two places on a handset and each one has a job: the third tab,
   which opens her settings, and the strip walking the foot of Today, which
   opens her chat. She used to be a third time in the bar as well. */
.uc .rova, .uc-tab-rova img, .uc-brand-rova img,
.uc-theatre-rova img, .uc-rchat-rova img, .uc-trail img {
  image-rendering: crisp-edges; image-rendering: pixelated; display: block;
}
.uc-brand-rova { display: block; flex: 0 0 auto; }
.uc-theatre-rova { display: block; flex: 0 0 auto; line-height: 0; }
.uc-rchat-rova { line-height: 0; }
/* Her trail along the foot of Today: she walks the width, chases the ball and
   catches it. The strip is as tall as her widest frame's box at this size, so
   a run or a jump stays inside it and the page never moves under her.

   THE STRIP IS THE BUTTON (owner, v4 fifth pass), and the dog is not: she is
   walking, and a 28px target crossing the screen is a target you miss. It
   holds still, it is the width of the column, and the whole of it opens her
   chat. It says nothing while it rests -- a line of copy under a dog who is
   playing would be an advertisement -- and answers a hover, a focus and a
   press, which is what an affordance costs when the picture is the point. */
.uc-trailbtn {
  display: block; width: 100%; padding: 4px 8px; margin: 0;
  border: 0; border-radius: var(--r-3); background: transparent;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background var(--d-1) linear, transform var(--d-2) var(--e-spring);
}
.uc-trailbtn:hover { background: var(--ink-08); }
.uc-trailbtn:active { transform: scale(0.985); }
.uc-trail { display: block; position: relative; height: 36px; overflow: hidden; pointer-events: none; }
.uc-trail .rova-walker { position: absolute; left: 0; bottom: 0; will-change: transform; }
.uc-trail .rova-ball { position: absolute; left: 0; bottom: 0; }
.uc-trail .rova-ball.rolling { animation: uc-ball 260ms ease-out infinite alternate; }
@keyframes uc-ball { to { bottom: 6px; } }
.uc-trail img[hidden] { display: none; }

/* ── The scrollers ───────────────────────────────────────────────────────
   One per section, all mounted, only one shown. Keeping them in the DOM is
   what preserves the scroll offset of a section you come back to, which is
   the difference between an app and a set of pages. */
/* The two sections sit in here on top of each other and one of them is off the
   edge for the length of a move, so this is what clips it. */
.uc-body { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; background: var(--cream); }
.uc-view {
  position: absolute; inset: 0;
  /* Opaque, because two of these are on screen together for the length of a
     section change and the arriving one has to COVER the one it replaces. It
     is the ground .uc-body paints anyway, so nothing looks different. */
  background: var(--cream);
  overflow-y: auto; overflow-x: hidden;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  padding: calc(52px + env(safe-area-inset-top)) 16px calc(28px + env(safe-area-inset-bottom));
}
/* On a handset the chrome is the bar and then the title band under it, so the
   scroller clears both. --navh is written by movePill() off the bar's own box,
   and the bar carries the safe-area inset itself there; this was a literal
   66px against a bar that measures 79px, so the day's title sat under a bar it
   should clear -- the kind of number that is right the day it is typed and
   wrong the first time a label wraps or a font loads.

   A CONTAINER QUERY ADDS NO SPECIFICITY, so this has to come after the padding
   it overrides rather than beside the rest of the handset chrome. Left as it
   was at a desk: the rail stays on the side and there is nothing new to clear
   at the top. */
@container uc (max-width: 899px) {
  .uc-view { padding-top: calc(var(--navh, 79px) + 52px); }
}
.uc-view[hidden] { display: none !important; }
/* The one section with a (+) on it is the one section that has to leave room
   for it. Without this the last entry of the journal cannot be scrolled clear
   of the button: 58px of button and 20px of gap over 28px of padding. */
.uc-view[data-view="journal"] { padding-bottom: calc(122px + env(safe-area-inset-bottom)); }
.uc-view::-webkit-scrollbar { width: 8px; }
.uc-view::-webkit-scrollbar-thumb { background: var(--ink-12); border-radius: 8px; }
/* No bar on a handset. It is an overlay bar either way, so nothing reflows;
   a phone-shaped screen with a scrollbar drawn down the side of it is the one
   thing on here that says "web page". The rail keeps its at a desk, where a
   pointer wants somewhere to grab. */
@container uc (max-width: 899px) {
  .uc-view { scrollbar-width: none; }
  .uc-view::-webkit-scrollbar { width: 0; height: 0; }
}
.uc-page { display: flex; flex-direction: column; gap: 20px; max-width: 640px; margin: 0 auto; padding-top: 14px; }

/* The big title, IN the scroller so it scrolls away on its own. Nothing
   animates a header height: the compact title in the bar simply comes up as
   this one goes, which is one cross-fade instead of a moving box. */
.uc-title { font-size: 33px; font-weight: 700; line-height: 1.1; letter-spacing: -0.03em; }
.uc-sub { font-size: 16px; color: var(--ink-62); margin-top: 4px; }
/* The day's own fun fact, DEMOTED FROM THE HEADING (owner, Sept 8 2026: the
   title is the route now). It is a sentence and not a name -- "Wasagaming
   Townsite opens at 09:00 on Monday, the earliest of today's stops to open" --
   so it reads under the title at running weight, where a lede goes. */
.uc-lede { margin-top: 6px; font-size: 17px; line-height: 1.4; color: var(--ink-70); }
/* The day's own way out to its published page rides the END OF THE LEDE
   (owner, Sept 10 2026), as the same footnote mark the stops already wear --
   see .uc-site below, which this reuses whole rather than growing a second
   idea of what a small way-out looks like. Only the title's copy needs
   anything of its own: it sits on a 33px line and the mark would ride the
   cap height without being pulled back down. */
.uc-title .uc-site { vertical-align: 0.18em; }

/* The Edit toggle (v5, brought over on consolidation, moved to the FOOT of
   the day on the owner's fourth pass): the day is read top to bottom before
   it is changed, so the switch waits at the end of the reading rather than
   sitting beside the title before any of it has been seen. */
/* ── The day's paperwork ─────────────────────────────────────────────────
   Under the line and above Edit. Rows and not stops: a booking document is
   about something the day already names -- the bed the Tonight row holds, the
   sight already on the line -- so it must not read as another moment in the
   day. Quieter than a stop by design: no node, no rail, a thin card that says
   what it is and opens in its own tab.

   The whole row is the link (CLAUDE.md: a tile that links somewhere is the
   whole <a>), so there is no small "open" control to miss on a phone. */
.uc-docs { margin-top: 18px; }
.uc-docs-h { margin: 0 0 8px; font-size: 12.5px; font-weight: 700; letter-spacing: .05em;
             text-transform: uppercase; color: var(--ink-62); font-family: var(--display); }
.uc-doc { display: flex; align-items: center; gap: 11px; padding: 11px 13px; margin-bottom: 7px;
          border: 1px solid var(--ink-12); border-radius: var(--r-2); background: var(--cream-2);
          color: var(--ink); text-decoration: none;
          transition: border-color .15s ease, background .15s ease; }
.uc-doc:hover, .uc-doc:focus-visible { border-color: var(--coral); background: var(--cream); }
.uc-doc-em { font-size: 19px; line-height: 1; flex: 0 0 auto; }
.uc-doc-t { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.uc-doc-t b { font-size: 15.5px; font-weight: 600; }
.uc-doc-n { font-size: 13.5px; color: var(--ink-62); }

.uc-editfoot { display: flex; justify-content: center; padding-top: 2px; }
.uc-switch.is-compact { flex: 0 0 auto; gap: 8px; min-height: 0; }
.uc-switch.is-compact .uc-track { width: 40px; height: 23px; }
.uc-switch.is-compact .uc-knob { width: 17px; height: 17px; }
.uc-switch.is-compact input:checked + .uc-track .uc-knob { transform: translateX(17px); }
.uc-switch.is-compact .uc-switch-words b { font-size: 14.5px; font-weight: 600; }

/* ── Coming up for the first time ────────────────────────────────────────
   The console has one load and it should look like one: the chrome settles
   from the edge it is docked to, and the sections' own blocks rise after it
   under the same rule that carries them for the rest of the session. Runs
   once, off a class the shell drops as soon as it has played. */
.uc.is-booting .uc-nav { animation: uc-boot-bar 460ms var(--e-out) both; }
.uc.is-booting .uc-head > * { animation: uc-boot-fade 460ms 70ms var(--e-out) both; }
@keyframes uc-boot-bar { from { opacity: 0; transform: translateY(-12px); } }
@keyframes uc-boot-fade { from { opacity: 0; } }

/* ── Section changes ─────────────────────────────────────────────────────
   THE SECTION MOVE (owner, v6). Two sections in the same box, one coming in
   from the edge and one drifting out under it. The arriving one is opaque and
   sits above, so it COVERS what it is replacing and there is never a frame
   with both readable; the outgoing one moves a fraction of the distance, which
   is what makes the move read as one thing sliding over another rather than
   two things swapping places. Directional: going right along the tab bar comes
   in from the right.

   The bar is not in here at all. It is a LIVE element the whole time, which is
   the point: the pill travels, the labels light, the (+) leaves.

   The four ::view-transition rules that used to live here are gone with the API
   itself. They animated two photographs of the whole document, and the bar was
   in both of them: for the length of every move the chrome was a still image,
   so the lit pill could not travel to the tab under somebody's finger. See
   ucShow() in app.js. */
.uc-view.is-in { z-index: 2; animation: uc-slide-in 340ms var(--e-out) both; }
.uc-view.is-in-back { z-index: 2; animation: uc-slide-in-back 340ms var(--e-out) both; }
.uc-view.is-out { animation: uc-slide-out 340ms var(--e-out) both; }
.uc-view.is-out-back { animation: uc-slide-out-back 340ms var(--e-out) both; }
@keyframes uc-slide-in { from { transform: translateX(100%); } }
@keyframes uc-slide-in-back { from { transform: translateX(-100%); } }
@keyframes uc-slide-out { to { transform: translateX(-16%); } }
@keyframes uc-slide-out-back { to { transform: translateX(16%); } }

/* ── Pull to refresh ─────────────────────────────────────────────────────
   A real drag with real rubber-banding, and it only arms at the very top of
   the scroller. The arc fills with the pull rather than spinning on a timer,
   so a half pull looks like a half pull and lets go of nothing. */
.uc-ptr {
  position: absolute; top: calc(52px + env(safe-area-inset-top)); left: 0; right: 0; z-index: 15;
  display: flex; align-items: center; justify-content: center;
  height: 0; pointer-events: none;
}
.uc-ptr-ring {
  width: 30px; height: 30px; border-radius: 50%;
  border: 2.5px solid var(--ink-12);
  border-top-color: var(--coral);
  opacity: var(--pull, 0);
  transform: translateY(calc(var(--pull, 0) * 34px)) rotate(calc(var(--pull, 0) * 300deg)) scale(calc(0.6 + var(--pull, 0) * 0.4));
}
.uc-ptr.is-running .uc-ptr-ring { animation: uc-spin 700ms linear infinite; opacity: 1; transform: translateY(34px); }
/* Under the whole of the chrome, which on a handset is the bar and the band.
   After the base rule, for the same no-specificity reason as the scroller. */
@container uc (max-width: 899px) { .uc-ptr { top: calc(var(--navh, 79px) + 52px); } }
@keyframes uc-spin { to { transform: translateY(34px) rotate(360deg); } }

/* ══ The tab bar, and the same component as a rail ══════════════════════════
   One pill, moved rather than redrawn: the lit tab is a thing that TRAVELS
   between tabs, which is what says the sections are one place. */
.uc-nav {
  position: relative; z-index: 20; flex: 0 0 auto;
  /* auto-flow and not repeat(4): the bar draws whatever NAV holds, so hiding
     a section is one array in console.js and nothing here. */
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--cream) 84%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border-top: 1px solid var(--ink-12);
}
/* At the TOP on a handset (v5, brought over on consolidation): the same bar,
   moved above the content rather than below it, docked under the head. Only
   the position changes -- the pill, the tabs and the measuring in movePill()
   are untouched, since getBoundingClientRect does not care which edge a bar
   sits on. */
@container uc (max-width: 899px) {
  .uc-nav {
    position: absolute; top: 0; left: 0; right: 0;
    padding: calc(6px + env(safe-area-inset-top)) 8px 6px;
    border-top: 0; border-bottom: 1px solid var(--ink-12);
  }
}
.uc-nav-pill {
  position: absolute; z-index: 0; left: 0; top: 0;
  width: var(--pw, 0px); height: var(--ph, 0px);
  border-radius: 16px; background: var(--ink-08);
  transform: translate3d(var(--px, 0px), var(--py, 0px), 0);
  will-change: transform;
}
/* The travelling is switched on by the FIRST measurement and not before: until
   then the pill's own left is 0, and a pill that transitions into place from
   the corner of the bar on load is the one move in here nobody asked for. */
.uc-nav-pill.is-set {
  transition: transform var(--d-3) var(--e-spring), width var(--d-3) var(--e-out),
              height var(--d-3) var(--e-out), opacity var(--d-2) var(--e-out);
}
/* Standing in a section the bar does not offer -- the Calendar, reached
   straight through go(). The pill fades rather than staying lit on a tab you
   are not on, and it is still measured, so coming back lands it without a
   slide across the bar from wherever it was. */
.uc-nav-pill.is-off { opacity: 0; }
.uc-tab {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 54px; padding: 7px 4px 6px;
  border: 0; background: transparent; cursor: pointer;
  color: var(--ink-62); font-size: 11.5px; font-weight: 600; letter-spacing: 0;
  border-radius: 16px;
  transition: color var(--d-2) var(--e-out);
  -webkit-tap-highlight-color: transparent;
}
.uc-tab svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; display: block; transition: transform var(--d-3) var(--e-spring); }
/* Her tab. rova.js writes the img's own width and height off its frame table,
   so nothing here may set them: this is the 24px box the line icons sit in,
   holding her at the bottom so her feet land on the same line their strokes
   do. She is dimmed with the rest of the tab and comes up full when it is lit,
   which is the same tell the icons give, said in the one way a photograph of a
   dog can say it. */
.uc-tab-rova {
  display: flex; align-items: flex-end; justify-content: center;
  width: 24px; height: 24px;
  opacity: 0.8; filter: saturate(0.55);
  transition: opacity var(--d-2) var(--e-out), filter var(--d-2) var(--e-out), transform var(--d-3) var(--e-spring);
}
.uc-tab.is-on .uc-tab-rova { opacity: 1; filter: none; }
.uc-tab.is-popping .uc-tab-rova { animation: uc-tabpop 460ms var(--e-spring); }
.uc-tab.is-on { color: var(--ink); }
.uc-tab.is-on svg { stroke-width: 2.2; }
.uc-tab.is-popping svg { animation: uc-tabpop 460ms var(--e-spring); }
@keyframes uc-tabpop { 0% { transform: scale(1); } 34% { transform: scale(0.82) translateY(1px); } 70% { transform: scale(1.14) translateY(-2px); } 100% { transform: scale(1); } }
.uc-tab-cnt {
  position: absolute; top: 4px; left: 50%; margin-left: 6px;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
  background: var(--coral); color: #0B0D0A;
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
  font-variant-numeric: tabular-nums;
  animation: uc-cnt-in 420ms var(--e-spring) both;
}
@keyframes uc-cnt-in { from { transform: scale(0); opacity: 0; } }

/* At a desk it is a rail, and the same pill slides down it instead of across. */
@container uc (min-width: 900px) {
  .uc-shell { flex-direction: row-reverse; }
  .uc-nav {
    /* ROW and not the bar's column. grid-template-columns: 1fr on its own left
       grid-auto-flow at the bar's , so the three tabs flowed into three
       IMPLICIT COLUMNS and ran off the side of a 232px rail -- the third one
       started 15px past its right edge. The explicit column is what they stack
       IN; the flow is what makes them stack. */
    grid-auto-flow: row;
    grid-template-columns: 1fr; align-content: start; gap: 4px;
    width: 232px; flex: 0 0 232px;
    padding: 74px 12px 20px;
    border-top: 0; border-right: 1px solid var(--ink-12);
    background: var(--rail);
  }
  .uc-nav-pill { border-radius: 14px; }
  .uc-tab { flex-direction: row; justify-content: flex-start; gap: 12px; min-height: 48px; padding: 0 14px; font-size: 15.5px; border-radius: 14px; }
  .uc-tab-cnt { position: static; margin-left: auto; }
  .uc-head { left: 232px; }
  /* row-reverse and NOT an order property: the flex items in flow are the body
     and the nav, in that DOM order, so reversing the axis puts the nav on the
     left where the console's rail has always been. An `order` on the body sent
     it back to the left and took the rail with it to the right, which is what
     the first run of this looked like. */
  .uc-view { padding-left: 32px; padding-right: 32px; }
  /* THE COLUMN AT A DESK (owner, Sept 10 2026: "can we use more of the width
     of the main frame?"). 720px was a reading measure chosen when this screen
     was one line of prose on a handset, and on a 1900px monitor it left two
     thirds of the frame empty. 880px is still a measure -- the day is read
     top to bottom and a 1600px line of running text is unreadable -- but it
     is one that fills the room a desk actually has. */
  .uc-page { max-width: 880px; }
  /* ...EXCEPT THE JOURNAL, WHICH IS NOT PROSE (owner, same pass: "especially
     Journal page"). It is a masonry wall of photographs, and a wall has no
     measure to respect: every extra 260px is another column of pictures
     rather than a longer line to read. */
  .uc-view[data-view="journal"] .uc-page { max-width: 1280px; }
  /* THE FULL COLUMN (owner, Sept 8 2026: "make map bigger"). It was capped at
     560px on the argument that a desk reader already meets this drawing at
     that size on the published page. That argument was about a picture. It is
     not a picture any more: its marks are pressable and they are what the line
     below is joined to, so the thing being sized is a target and 560px was
     making eight of them share a frame two thirds the width of the words they
     answer to. The column's own 720px, and nothing wider -- the drawing is
     tuned in 680x440 units and stretching it past the text it belongs with
     would make it the page instead of part of it. It is THE COLUMN'S OWN
     WIDTH and always was, so it moved with the column when the column grew
     (owner, Sept 10 2026). */
  .uc-daymapwrap { max-width: 880px; }
  /* Above the rail's own stacking context, or the rail's background paints
     over it: both are children of the shell and the rail carries z-index 20. */
  .uc-brand {
    position: absolute; top: 0; left: 0; z-index: 21; width: 232px; height: 74px;
    display: flex; align-items: center; gap: 10px; padding: 0 20px;
    font-size: 18px; font-weight: 700; letter-spacing: -0.02em;
  }
  .uc-brand span { font-size: 22px; }
}
.uc-brand { display: none; }
@container uc (min-width: 900px) { .uc-brand { display: flex; } }

/* ══ Today ═════════════════════════════════════════════════════════════════
   The whole screen is one line and the line is the day, in the order the day
   happens. NO CLOCK IS PRINTED ON IT: the order is the time, and the only hour
   anywhere is the hour it is where you are, on the rig. v4 changes how the
   line MOVES and not what it says. */
/* ── The day's own map ────────────────────────────────────────────────────
   Drawn by the trip page's own trips/daymaps.js (one renderer since Sept 12
   2026, called with `literal: true`), falling back to
   change-day.js' abstract mapSvg only if the basemap failed to load, so what
   is here is only the frame either one sits in. Both carry their own literal
   colours (real land and water, a coral start, a crimson end), which is
   deliberate -- a drawn map keeps its key whatever surface it lands on, the
   console's dark theme included, which is why daymap.js hardcodes hex and
   never reads --ink (that custom property INVERTS in this console's dark
   theme; see the hazard in rova/CLAUDE.md) -- so it does NOT flip with the
   theme, and the frame gives it a mat under the dark one rather than letting
   it float. */
/* The card holds the DRAWING and nothing else. */
.uc-daymap { border: 1.5px solid var(--ink-12); border-radius: var(--r-3);
             overflow: hidden; background: #F3EEDF; position: relative; }
/* The trip page's pinch-zoom overlay, on the same map. Literal colours, like
   the drawing it sits on. */
.uc-daymap-big { position: absolute; right: 10px; top: 10px; border: 0;
                 border-radius: 999px; padding: 7px 13px; font: inherit; font-size: 12.5px;
                 font-weight: 700; color: #fff; background: rgba(11,13,10,0.66); cursor: zoom-in; }
.uc-daymap-big:hover { background: rgba(11,13,10,0.82); }
.uc-daymap svg { display: block; width: 100%; height: auto; }
/* ── The marks that are also rows ─────────────────────────────────────────
   daymap.js draws a transparent target over every mark the join claimed, in
   its own layer on top of the drawing, so NOT ONE DRAWN PIXEL moved to make
   this work. Everything that says a target is there is here, in CSS, on
   hover and on focus -- the map at rest is the same map it was, which is the
   point: the ring is an answer to "which of these is which", and a drawing
   wearing eight rings all the time has answered nothing.

   THE RING IS CORAL AND LITERAL, like every colour the drawing itself uses.
   var(--coral) is a site token and this console's dark theme is exactly where
   a token stops meaning what the map means (see daymap.js' header, and the
   --ink hazard in rova/CLAUDE.md). */
.uc-hit { cursor: pointer; stroke: transparent; stroke-width: 2.5; transition: stroke 140ms var(--e-out); }
.uc-hit:focus { outline: none; }
.uc-hit:hover, .uc-hit:focus-visible, .uc-hit.is-lit { stroke: #FF8852; }
.uc-hit.is-road { stroke-width: 20; }
.uc-hit.is-road:hover, .uc-hit.is-road:focus-visible, .uc-hit.is-road.is-lit {
  stroke: #FF8852; stroke-opacity: 0.28;
}
/* The other half of the same answer: the row lights when its mark does. A
   tint and the stop's own node picking up the coral, and NOTHING THAT MOVES --
   a row that grew or shifted under the pointer would push the rest of the day
   around while somebody is reading it. */
.uc-stop.is-lit { background: color-mix(in srgb, var(--coral) 9%, transparent); border-radius: 12px; }
.uc-stop.is-lit .uc-node { border-color: var(--coral); }
/* And where a press lands. Longer than the light, and it fades on its own:
   the map has just scrolled the page and this is what says where to. */
.uc-stop.is-found { animation: uc-found 1500ms var(--e-out) 1; }
@keyframes uc-found {
  0%, 55% { background: color-mix(in srgb, var(--coral) 18%, transparent); border-radius: 12px; }
  100%    { background: transparent; border-radius: 12px; }
}
.no-motion .uc-stop.is-found { animation: none; }
/* AND THE PRESS THE OTHER WAY (owner, Sept 10 2026: "make the along the way
   rows clickable to the map"). A mark has carried you to its row since Sept 8;
   this is the row carrying you back to its mark. The flash is the same
   1500ms and the same coral, drawn the only way an SVG hit target can wear
   it -- on its stroke, which is what the hover and the light already use, so
   the pressed mark and the lit one are one appearance and not two.

   ucArmMapJumps adds `is-tomap` off the map's OWN hit layer, so a sight with
   nothing on the drawing to reach gets no cursor and no focus stop: an
   affordance on a row that goes nowhere is the pin-for-the-place-you-are-not-
   going defect in another costume. */
.uc-stop.is-tomap > .uc-stop-words { cursor: pointer; border-radius: 12px; }
.uc-stop.is-tomap > .uc-stop-words:focus { outline: none; }
.uc-stop.is-tomap > .uc-stop-words:focus-visible {
  outline: 2px solid var(--coral); outline-offset: 3px;
}
.uc-hit.is-found { animation: uc-hit-found 1500ms var(--e-out) 1; }
@keyframes uc-hit-found {
  0%, 55% { stroke: #FF8852; stroke-width: 4.5; }
  100%    { stroke: transparent; stroke-width: 2.5; }
}
.uc-hit.is-road.is-found { animation: none; }
.no-motion .uc-hit.is-found { animation: none; }
/* Directions is a way OUT of the console and the map is a picture, so it is
   not in the picture's frame: it is the page's own control, on the page's own
   gap under it, sized like every other link on the day. It floated over the
   drawing until v4's fifth pass, then sat welded to the bottom edge of it;
   both were the same mistake, which is a control living in a frame that
   belongs to something it is not part of. */
/* The wrapper is what carries the map's width, so that CENTRED (owner, v6)
   means centred under the drawing and not under the column: at a desk the
   drawing is capped and the column is not, and a control centred on the column
   would sit past the end of the picture it is a way out of. The gap is the
   wrapper's own and tighter than the page's 20px, because these two are one
   thing. */
.uc-daymapwrap { display: flex; flex-direction: column; gap: 12px; }

/* The drive's gauge, on the line. THE SAME BAR the staff calendar draws, off
   the same `loadFrom` classification, so a day cannot read easy on one screen
   and long on the other -- at the size of the heading it replaces. */
.uc-gauge { display: block; width: 100%; height: 5px; border-radius: 3px; background: var(--ink-08); overflow: hidden; }
.uc-gauge i { display: block; height: 100%; border-radius: 3px; background: var(--good); }
.uc-gauge.is-limit i { background: var(--coral-2); }
.uc-gauge.is-over i { background: var(--bad); }
.uc-drivegauge { display: flex; align-items: center; gap: 12px; padding: 4px 0 2px; max-width: 300px; }
.uc-drivegauge .uc-gauge { flex: 1 1 auto; min-width: 0; height: 10px; border-radius: 5px; }
.uc-drivegauge .uc-gauge i { border-radius: 5px; }
.uc-drivegauge b { flex: 0 0 auto; font-size: 21px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }

.uc-line { position: relative; display: flex; flex-direction: column; }
/* The rail behind the stops, faded at both ends because a day carries on past
   the screen in both directions. */
.uc-line::before {
  content: ""; position: absolute; left: 21px; top: 20px; bottom: 24px; width: 2px; border-radius: 2px;
  background: linear-gradient(to bottom, transparent, var(--ink-12) 28px, var(--ink-12) calc(100% - 28px), transparent);
}
/* HOW FAR THROUGH THE DAY YOU ARE, drawn on the rail itself. It claims nothing
   the rig does not already claim -- it ends at the rig's own stop, and the rig
   is placed from the only two clocks the record holds. It grows on arrival and
   it MOVES when the rig moves, so an hour passing is something you can see
   rather than something you have to re-read. */
.uc-line::after {
  content: ""; position: absolute; left: 21px; top: 20px; width: 2px; border-radius: 2px;
  height: var(--run, 0px);
  background: linear-gradient(to bottom, color-mix(in srgb, var(--coral) 35%, transparent), var(--coral));
  transition: height var(--d-4) var(--e-out);
}
.uc-line.no-run::after { display: none; }

.uc-stop {
  position: relative; display: flex; align-items: flex-start; gap: 14px;
  padding: 11px 0; scroll-margin-top: 76px;
}
/* Each stop arrives as the line reaches it. IntersectionObserver and not a
   fixed stagger: on a day with nine stops a fixed stagger has already played
   out under the fold, and what is left is a list that was pasted in. */
.uc-stop.is-veiled, .uc-rise.is-veiled { opacity: 0; transform: translateY(14px); }
.uc-stop.is-shown, .uc-rise.is-shown { opacity: 1; transform: none; transition: opacity var(--d-3) var(--e-out), transform var(--d-3) var(--e-out); }

.uc-node {
  flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%;
  background: var(--cream); border: 2px solid var(--ink-12);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 22px; line-height: 1; position: relative; z-index: 1;
  transition: border-color var(--d-2) var(--e-out), background var(--d-2) var(--e-out), transform var(--d-3) var(--e-spring);
}
.uc-stop.is-past .uc-node { border-color: var(--coral); opacity: 0.45; filter: grayscale(0.7); }
.uc-stop.is-here .uc-node { border-color: var(--coral); background: var(--coral); transform: scale(1.08); }
/* The drive's own icon doubling as Directions (owner, Sept 8 2026): the same
   circle every stop draws, made an <a> rather than a <span>, so the only
   thing to add is that it is worth tapping -- no text, no second icon
   crowding a 44px circle that already holds one. */
.uc-node-link { text-decoration: none; cursor: pointer; }
.uc-node-link:hover, .uc-node-link:focus-visible { border-color: var(--coral); background: var(--peach); }
.uc-stop.is-here .uc-node::after {
  content: ""; position: absolute; inset: -6px; border-radius: 50%;
  border: 2px solid var(--coral); animation: uc-halo 2.6s ease-out infinite;
}
@keyframes uc-halo {
  0% { transform: scale(0.82); opacity: 0.7; }
  70%, 100% { transform: scale(1.3); opacity: 0; }
}
.uc-stop-words { flex: 1 1 auto; min-width: 0; padding-top: 3px; }
.uc-stop-head { display: flex; align-items: flex-start; gap: 12px; }
.uc-stop-say { flex: 1 1 auto; min-width: 0; }
.uc-stop-say dt { font-size: 14.5px; font-weight: 600; color: var(--ink-62); }
.uc-stop-say dd { margin: 0; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }
/* Nothing chosen yet is not the same silence as nothing to say (owner, Sept 8
   2026): the row stays on the line, in the running weight rather than the
   heading's, so Ask Rova still has a row to sit under. */
.uc-stop-say dd.uc-tochoose { font-weight: 400; color: var(--ink-62); }
/* ...and where there IS something to press, the press is the value (owner,
   Sept 8 2026): "Not chosen yet" over "Choose lunch" was one fact twice, and
   only the second could be acted on. The <dd> holds the control, so it needs
   the heading's own leading and none of its weight. */
.uc-stop-say dd.uc-tochoose.is-door { margin-top: 3px; font-weight: 400; }
/* A stop, edited in place (owner, v4 fourth pass): the same field the value
   already sat in, made typeable, so there is nothing to switch into or out
   of. Same font as the dd it replaces, so the line does not jump when Edit
   comes on. */
.uc-stopedit {
  display: block; width: 100%; margin: 0; padding: 2px 0; border: 0; border-bottom: 2px solid var(--ink-12);
  background: transparent; color: var(--ink); font-family: var(--display);
  font-size: 19px; font-weight: 700;
  transition: border-color var(--d-1) linear;
}
.uc-stopedit:hover { border-color: var(--ink-30); }
.uc-stopedit:focus { outline: none; border-color: var(--coral); }
.uc-stopstatus { margin-top: 4px; font-size: 12.5px; min-height: 1.2em; color: var(--ink-62); }
.uc-stopstatus.is-good { color: var(--good); font-weight: 600; }
/* WHAT ELSE IS KNOWN ABOUT THE STOP (owner, v4). A second line under the
   stop's own words, in the running weight rather than the heading's: the stop
   is still what the day says, and this is what is known about it. */
.uc-stop-note { margin-top: 5px; font-size: 16px; line-height: 1.45; color: var(--ink-70); }
/* The ways out. Small, quiet and side by side, because they are the least of
   what the row says and the most likely thing to be pressed by accident on the
   way past. Every one of them leaves the console, and the mark says so. */
.uc-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 9px; }
.uc-link {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 36px; padding: 0 13px 0 11px; border-radius: var(--r-pill);
  border: 1.5px solid var(--ink-12); background: var(--cream);
  color: var(--ink); text-decoration: none;
  font-size: 14.5px; font-weight: 600;
  transition: border-color var(--d-1) linear, background var(--d-1) linear, transform var(--d-2) var(--e-spring);
}
.uc-link:hover { border-color: var(--coral); background: var(--peach); }
.uc-link:active { transform: scale(0.96); }
.uc-link svg { width: 16px; height: 16px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* The one control in this row that goes INWARD -- it opens a list under the
   stop rather than leaving the console -- so it is the one that is tinted
   rather than outlined, and the one that says what it opens. */
.uc-link.is-find { cursor: pointer; background: var(--peach); border-color: transparent; }
.uc-link.is-find:hover { border-color: var(--coral); }
.uc-link.is-find.is-open { background: var(--coral); border-color: var(--coral); color: #0B0D0A; }
.uc-link .uc-out-mark { width: 13px; height: 13px; opacity: 0.5; margin-left: 1px; }
/* Icon only (owner, v4 second pass): "Map" and "Website" were read off the
   icon already, and the word doubled it. The title attribute keeps the label
   for anybody hovering or on a screen reader. */
.uc-link.is-icon { min-width: 36px; padding: 0; justify-content: center; }
.uc-link.is-icon svg { width: 18px; height: 18px; }
/* THE WAY IN TO A STOP NOBODY HAS CHOSEN (owner, Sept 8 2026). It goes INWARD
   like Ask Rova does -- it turns Edit on and puts the cursor in this row --
   so it takes the same tinted treatment rather than the outline every link
   that leaves the console wears. */
.uc-link.is-pencil { cursor: pointer; background: var(--peach); border-color: transparent; }
.uc-link.is-pencil:hover { border-color: var(--coral); }
/* ALONG THE WAY, IN EDIT (owner, Sept 12 2026). A sight's field holds a
   sentence, not a name, so it is set in the running words it replaces rather
   than the heading weight a lunch's name takes. Remove is outlined, never
   tinted: it takes something away, and its second press says so in coral. */
.uc-see.is-editing .uc-stopedit { font-family: inherit; font-size: 16.5px; font-weight: 400; line-height: 1.45; resize: none; overflow: auto; field-sizing: content; }
.uc-see-edit { margin: 2px 0 0; }
/* A sight's time and map, under its words in Edit (Sept 17 2026). Two small
   labelled fields side by side, the time narrow and the map taking the rest;
   they stack on a narrow phone. */
.uc-see-fields { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-top: 10px; }
.uc-see-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.uc-see-field > span { font-size: 12.5px; font-weight: 600; color: var(--ink-62); }
.uc-see-field.is-time { flex: 0 0 auto; }
.uc-see-field.is-map { flex: 1 1 220px; }
.uc-see.is-editing .uc-see-field .uc-stopedit { font-size: 16px; font-weight: 400; line-height: 1.4; }
.uc-see-field input[type="time"] { min-width: 7.5em; color-scheme: light dark; }
.uc-link.is-remove { cursor: pointer; min-height: 32px; font-size: 13.5px; }
.uc-link.is-remove.is-armed { border-color: var(--coral); background: var(--coral); color: #0B0D0A; }
/* Under the words column: the 44px node and the 14px gap .uc-stop sets. */
.uc-line .uc-see-more { padding: 0 0 10px 58px; }
.uc-see-more .uc-link { cursor: pointer; }
/* THE WEBSITE, AT THE END OF THE SENTENCE (owner, Sept 8 2026): a footnote's
   position and a footnote's weight. It sits inline in the last line the row
   says, so it must not break onto one of its own -- hence the non-breaking
   left margin and the baseline-nudged box. The target is still a real one:
   the padding gives it 30px of height inside a 16px line. */
.uc-sitewrap { white-space: nowrap; }
.uc-site {
  display: inline-flex; align-items: center; justify-content: center;
  margin-left: 0; padding: 5px; border-radius: var(--r-pill);
  vertical-align: -2px; color: var(--ink-62); text-decoration: none;
  transition: color var(--d-1) linear, background var(--d-1) linear;
}
.uc-site svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.uc-site:hover, .uc-site:focus-visible { color: var(--ink); background: var(--peach); }
.uc-see .uc-node { background: var(--cream-2); }
/* ── A SIGHT ANSWERS THE POINTER (owner, Sept 10 2026: "can we have a nice
   hover effect here on desktop?") ─────────────────────────────────────────
   Every one of these rows is a way out to a map on its own icon, and until
   now the only thing that said so was the cursor changing over the glyph.
   The row warms, the mark lifts and turns a little, and the name takes the
   coral -- the same three moves the journal's own cards make, so the two
   screens hover the same way.

   NOTHING REFLOWS. The lift is a transform on the icon alone, so no row ever
   changes height or pushes the day around under somebody reading it. Fine
   pointers only, and off under reduced motion: a row that tilts under a thumb
   is a row that looks broken. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .uc-see .uc-node,
  .uc-see .uc-stop-say dd,
  .uc-see { transition: background var(--d-1) linear, transform var(--d-2) var(--e-out), color var(--d-1) linear; }
  .uc-see:hover { background: color-mix(in srgb, var(--coral) 7%, transparent); border-radius: 12px; }
  .uc-see:hover .uc-node { transform: translateY(-2px) rotate(-5deg); background: var(--peach); border-color: var(--coral); }
  .uc-see:hover .uc-stop-say dd { color: var(--coral-2); }
}
.uc-see-say { margin: 4px 0 0; font-size: 16.5px; line-height: 1.45; color: var(--ink-70); }
.uc-see-say.is-lead { margin: 0; font-size: 17.5px; font-weight: 400; color: var(--ink); }
/* A stop's one practical phrase (Sept 13 2026): one play button that looks
   like one, a small line saying what it is, and the Italian folded under it.
   `uc-phr`, never `uc-say`: that is the journal's textarea. Tokens only. */
.uc-phr { display: flex; align-items: flex-start; gap: 10px; margin-top: 8px; }
.uc-phr-play { flex: 0 0 auto; width: 34px; height: 34px; margin: 0; padding: 0 0 0 2px; border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center; background: var(--coral); color: #0B0D0A; cursor: pointer; }
.uc-phr-play:hover { filter: brightness(1.06); }
.uc-phr-play:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.uc-phr-play.is-playing { box-shadow: 0 0 0 4px color-mix(in srgb, var(--coral) 35%, transparent); }
.uc-phr-more { flex: 1 1 auto; min-width: 0; padding-top: 7px; }
.uc .uc-phr-en { font-size: 14px; line-height: 1.4; color: var(--ink-70); cursor: pointer; list-style: none; }
.uc-phr-en::-webkit-details-marker { display: none; }
.uc-phr-en::after { content: ''; display: inline-block; width: 6px; height: 6px; margin: 0 0 2px 7px; border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor; transform: rotate(45deg); opacity: .7; transition: transform .15s; }
.uc-phr-more[open] > .uc-phr-en::after { transform: rotate(-135deg); margin-bottom: -1px; }
.uc .uc-phr-it { margin: 5px 0 0; font-size: 16px; font-weight: 700; line-height: 1.35; color: var(--ink); }
.uc .uc-phr-sound { margin: 2px 0 0; font-size: 13.5px; font-style: italic; color: var(--ink-62); }

/* WHAT PEOPLE SAY (Sept 17 2026): Google's words under a chosen meal, folded
   shut until tapped. Set apart by a rule and a tint (Google asks its content be
   visually distinguished) and credited "Google Maps" inside the same box. */
.uc-rev { margin-top: 8px; }
.uc .uc-rev-ask { font-size: 14px; line-height: 1.4; color: var(--ink-70); cursor: pointer; list-style: none; }
.uc-rev-ask::-webkit-details-marker { display: none; }
.uc-rev-ask::after { content: ''; display: inline-block; width: 6px; height: 6px; margin: 0 0 2px 7px; border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor; transform: rotate(45deg); opacity: .7; transition: transform .15s; }
.uc-rev[open] > .uc-rev-ask::after { transform: rotate(-135deg); margin-bottom: -1px; }
.uc-rev-body:not(:empty) { margin-top: 8px; padding: 10px 12px; border-left: 2px solid var(--ink-12);
  background: var(--ink-08); border-radius: 0 8px 8px 0; }
.uc .uc-rev-about, .uc .uc-rev-sum { margin: 0 0 8px; font-size: 15.5px; line-height: 1.45; color: var(--ink); }
.uc .uc-rev-head { margin: 0 0 2px; font-size: 13px; font-weight: 700; color: var(--ink-70); }
.uc .uc-rev-disc, .uc .uc-rev-note { margin: 0 0 8px; font-size: 13px; color: var(--ink-62); }
.uc-rev-q { margin: 0 0 10px; }
.uc .uc-rev-q p { margin: 0; font-size: 15px; line-height: 1.45; font-style: italic; color: var(--ink-70); }
.uc .uc-rev-q .uc-rev-by { margin-top: 3px; font-size: 13px; font-style: normal; color: var(--ink-62); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.uc-rev-by img { width: 20px; height: 20px; border-radius: 50%; }
.uc .uc-rev-credit { margin: 2px 0 0; font-size: 12.5px; color: var(--ink-62); }
.uc-rev a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* WHERE THE RIG IS, said ONCE (owner, v4 fifth pass). There was a second mark
   as well -- a peach badge with a wiggling rig on the stop -- beside a node
   that is already filled coral, scaled up and haloed for that same stop, and
   on the drive it repeated the stop's own icon. The node is the mark; it wears
   the words for a reader who cannot see it. */

/* The one control a stop may carry. The stop is the content; the ask is a
   control ON it, the size of a control, with the sentence as its label. */
.uc-ask {
  flex: 0 0 auto; width: 44px; height: 44px; margin: -3px -4px 0 0; padding: 0;
  border: 2px solid var(--ink-12); border-radius: 50%;
  background: var(--cream); color: var(--ink); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color var(--d-1) linear, background var(--d-1) linear, transform var(--d-2) var(--e-spring);
}
.uc-ask svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.uc-ask:hover { border-color: var(--coral); background: var(--peach); transform: rotate(-8deg) scale(1.07); }
.uc-ask:active { transform: scale(0.9); }
.uc-ask.is-open { border-color: var(--coral); background: var(--coral); }

/* ── The weather, beside the town it is about ────────────────────────────
   ONE card at both ends of the drive, and the KICKER is the only thing that
   says which fact it is: a reading says "right now" and names its town, a
   forecast says whose day it is. The tint is the sky's own and tints the
   theme's surface rather than replacing it. */
.uc-wx {
  position: relative; overflow: hidden;
  margin-top: 12px; padding: 15px 17px; border-radius: var(--r-3);
  border: 2px solid var(--ink-12); background: var(--cream-2);
  animation: uc-wx-in var(--d-3) var(--e-out) both;
}
@keyframes uc-wx-in { from { opacity: 0; transform: translateY(-6px) scale(0.99); } }
/* The sky drifts. One slow pass, behind the numbers and never over them. */
.uc-wx::before {
  content: ""; position: absolute; top: -40%; left: -20%; width: 70%; height: 180%;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--cream) 55%, transparent), transparent);
  animation: uc-drift 18s ease-in-out infinite alternate;
  pointer-events: none;
}
@keyframes uc-drift { to { transform: translateX(120%) translateY(6%); } }
.uc-wx.wx-sun   { background: linear-gradient(158deg, color-mix(in srgb, #F5A623 22%, var(--cream-2)), var(--cream-2)); }
.uc-wx.wx-mixed { background: linear-gradient(158deg, color-mix(in srgb, #F5C26B 20%, var(--cream-2)), var(--cream-2)); }
.uc-wx.wx-grey  { background: linear-gradient(158deg, color-mix(in srgb, #7E8478 20%, var(--cream-2)), var(--cream-2)); }
.uc-wx.wx-wet   { background: linear-gradient(158deg, color-mix(in srgb, #4A7FA5 22%, var(--cream-2)), var(--cream-2)); }
.uc-wx.wx-snow  { background: linear-gradient(158deg, color-mix(in srgb, #8FB6CF 20%, var(--cream-2)), var(--cream-2)); }
.uc-wx.wx-night { background: linear-gradient(158deg, color-mix(in srgb, #4C4F6B 24%, var(--cream-2)), var(--cream-2)); }
.uc-wx > * { position: relative; }
.uc-wx-top { display: flex; align-items: center; gap: 14px; }
.uc-wx-em { font-size: 44px; line-height: 1; flex: 0 0 auto; animation: uc-sky 7s ease-in-out infinite; }
@keyframes uc-sky { 0%, 100% { transform: translateY(0) rotate(0); } 50% { transform: translateY(-5px) rotate(-4deg); } }
.uc-wx-nums { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.uc-wx-hi { font-size: 40px; font-weight: 700; line-height: 1; letter-spacing: -0.035em; font-variant-numeric: tabular-nums; }
.uc-wx-lo { font-size: 17px; font-weight: 600; color: var(--ink-70); }
.uc-wx-kick { margin-top: 10px; font-family: var(--mono); font-size: 12px; letter-spacing: 0.11em; text-transform: uppercase; font-weight: 700; color: var(--ink-70); }
.uc-wx-say { margin-top: 4px; font-size: 17.5px; font-weight: 400; }
/* THE SMALL FACTS, SMALL (owner, v4). The chance of rain, the air quality and
   the fires were pill chips the size of a control, which gave three readings
   the weight of the temperature above them and made them look pressable. One
   quiet line now, under the words. Still drawn only when they have a value: a
   line that says "no fires" every morning teaches the reader to stop looking. */
.uc-wx-meta { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 9px; }
.uc-wx-meta span { display: inline-flex; align-items: center; gap: 5px; font-size: 13.5px; font-weight: 600; color: var(--ink-70); }
.uc-wx-meta i { font-style: normal; font-size: 14px; line-height: 1; }
/* A backup forecast's required credit (weather-backup.mjs): small, last, and
   only on a card whose numbers it drew. */
.uc-wx-credit { margin-top: 8px; font-size: 11.5px; color: var(--ink-70); }
.uc-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.uc-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 14.5px; font-weight: 600; color: var(--ink);
  background: color-mix(in srgb, var(--cream) 72%, transparent);
  border: 1.5px solid var(--ink-12); border-radius: var(--r-pill); padding: 6px 12px;
}
/* The alert lives INSIDE the card and the loudest of them sets the card's own
   edge, off Environment Canada's own three words. Every bulletin is quoted;
   only how loud the card is comes from reading them. */
/* NO BOX, NO BORDER, NO BULLETS (owner, v6). The loudness is said once, on the
   weather card's own left edge below, and a red frame with a crimson heading
   around the same words said it a second time and a third -- which is what made
   a special air quality statement read like an evacuation. Running text, the
   bulletin still quoted word for word. */
.uc-alert { margin-top: 11px; padding: 0; border: 0; background: none; }
.uc-alert p { font-size: 16px; line-height: 1.45; color: var(--ink); }
.uc-alert p + p { margin-top: 7px; }
/* The yield is the only mark, and it is the same one whatever the tier: it says
   "read this", which is true of all three, and never how bad it is. */
.uc-alert-yield { margin-right: 7px; }
.uc-alert-lead { color: var(--ink-70); }
.uc-wx.has-alert { border-left-width: 6px; }
.uc-wx.has-alert.is-warning { border-left-color: var(--crimson); }
.uc-wx.has-alert.is-watch { border-left-color: var(--coral); }
.uc-wx.is-warning { animation: uc-wx-in var(--d-3) var(--e-out) both, uc-alarm 3.4s ease-in-out 1.2s 2; }
@keyframes uc-alarm { 0%, 100% { border-left-color: var(--crimson); } 50% { border-left-color: color-mix(in srgb, var(--crimson) 40%, var(--cream-2)); } }
/* The road carries no forecast, only its own worst warning, so the alert IS
   the card there. Inventing a picture to hang it under would be inventing
   weather. */
.uc-wx.wx-road { padding: 0; border: 0; background: none; }
.uc-wx.wx-road::before { display: none; }
.uc-wx.wx-road .uc-alert { margin-top: 11px; }

/* Where an answer lands, under the stop that was asked and nowhere else. */
.uc-out:empty { display: none; }
.uc-out { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--d-3) var(--e-out); }
.uc-out.is-open { grid-template-rows: 1fr; }
.uc-out > div { overflow: hidden; min-height: 0; }
.uc-foundhead { margin-top: 14px; font-size: 12.5px; font-weight: 700; letter-spacing: 0.1em;
                text-transform: uppercase; color: var(--ink-62); }
.uc-found { list-style: none; margin: 6px 0 2px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
/* THE CARD IS THE ROW, and the row now holds two things: the press that takes
   this place, and the two ways out that let you look it up first (owner, v6).
   The frame moved off the button and onto the <li> so that a place and its
   links read as ONE object; the button is still the only thing on it that
   changes the day. A button cannot hold a link, which is what forced it. */
.uc-found li { animation: uc-found-in var(--d-3) var(--e-out) both;
               border: 1.5px solid var(--ink-12); border-radius: var(--r-3);
               background: var(--cream);
               transition: border-color var(--d-1) linear, background var(--d-1) linear; }
.uc-found li:hover { border-color: var(--coral); background: var(--peach); }
.uc-found li:nth-child(2) { animation-delay: 70ms; }
.uc-found li:nth-child(n+3) { animation-delay: 140ms; }
@keyframes uc-found-in { from { opacity: 0; transform: translateY(-6px); } }
/* THE PRESS, which carries everything about the place and is still the one
   target on the row that changes the day. The frame is the <li>'s now, so this
   carries none of it. */
.uc-use {
  display: block; position: relative; width: 100%; margin: 0;
  padding: 11px 78px 11px 13px; text-align: left;
  border: 0; border-radius: var(--r-3);
  background: transparent; color: var(--ink); cursor: pointer;
  transition: transform var(--d-2) var(--e-spring);
}
.uc-use:active { transform: scale(0.985); }
/* The ride's rows, which are read rather than pressed because `bike` has no
   ledger shape. It is not a control, so it does not offer a control's cursor. */
.uc-use.is-read { cursor: default; }
/* The two ways out, in the card and under the words they are about. Tucked to
   the card's own left edge so they line up with the name above them, and with
   no gap of their own above: they belong to this place and not to the next
   one. */
.uc-uselinks { margin-top: 0; padding: 0 13px 11px; }
/* Level with the NAME, not with the middle of however many lines the place
   happens to take: a four-line card floated it into the prose. */
.uc-use-mark {
  position: absolute; top: 10px; right: 11px;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: var(--ink-08); font-size: 13.5px; font-weight: 700;
}
.uc-found li:hover .uc-use-mark { background: var(--coral); color: #0B0D0A; }
.uc-found.is-fresh li { border-color: var(--coral); }
.uc-found b { display: block; font-size: 18px; }
.uc-found .town { display: block; font-size: 15px; color: var(--ink-62); }
.uc-found p { margin-top: 4px; font-size: 16.5px; }
/* A VISITOR'S LINE, NOT ROVA'S. It used to be drawn in the reason's own type,
   because the endpoint sent no reason and the card fell through to it -- so
   "trail impassable about half way up the hill after heavy rain" read as a
   recommendation. Quoted in the markup and set apart here: italic, one step
   down, and pulled quiet so the reason above it stays the loudest thing on the
   card after the name. --ink-62 is derived, never a literal cream alpha; the
   console inverts --ink in its dark theme. */
.uc-found .uc-said { margin-top: 4px; font-size: 15px; font-style: italic; color: var(--ink-62); }
.uc-found small { display: block; margin-top: 4px; font-size: 14px; color: var(--ink-62); }
/* The search, which is the one thing here that spends. Asked for once opens
   this; asked for twice runs it. The block is tinted rather than outlined so
   that it does not read as one more row in the list above it. */
.uc-arm { margin-top: 14px; padding: 14px; border-radius: var(--r-3);
          background: var(--peach); border: 1.5px solid var(--coral); }
.uc-arm p { font-size: 16px; line-height: 1.45; color: var(--ink); }
/* THE NAME OF THE THING (owner, Sept 10 2026). What stood here was three
   sentences of internal accounting -- what a search is, what it costs, why it
   waits -- between somebody and a supper. The second press still guards the
   spend; the words are just the name now. */
.uc-arm-head { display: flex; align-items: center; gap: 8px; font-size: 17px; font-weight: 700; color: var(--ink); }
.uc-arm-head svg { width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* ...AND WHAT TO LOOK FOR. `query` has always been a free-text field on
   POST /api/admin/me/find; this screen was hardcoding the town into it, so
   every search of a stop asked one question. Capped at 80 characters in the
   markup against the route's own 120, which the town is joined into. */
.uc-arm-q {
  display: block; width: 100%; margin-top: 10px; padding: 11px 13px;
  border: 2px solid var(--ink-12); border-radius: var(--r-3);
  background: var(--cream); color: var(--ink);
  font-family: var(--display); font-size: 16.5px; min-height: 46px;
  transition: border-color var(--d-1) linear;
}
.uc-arm-q::placeholder { color: var(--ink-62); }
.uc-arm-q:hover { border-color: var(--ink-30); }
.uc-arm-q:focus { outline: none; border-color: var(--coral); }
.uc-arm-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.uc-arm-row .uc-btn { flex: 1 1 auto; min-height: 46px; }
.uc-btn.is-go { background: var(--coral); border-color: var(--coral); color: #0B0D0A; }
.uc-btn.is-quiet { font-size: 15.5px; }
.uc-searchnote { margin-top: 14px; font-size: 14.5px; color: var(--ink-62); }
[data-arm] { margin-top: 14px; }

/* Rova going through what the trip already knows. Her sprite, and one line at
   a time -- and not one word of it says she is searching the web, because the
   route this stands in for spends nothing. */
.uc-theatre { display: flex; align-items: center; gap: 13px; padding: 12px 0 4px; }
.uc-theatre-em { font-size: 30px; line-height: 1; animation: uc-sniff 1.5s ease-in-out infinite; }
@keyframes uc-sniff { 0%, 100% { transform: translateX(0) rotate(0); } 25% { transform: translateX(-3px) rotate(-6deg); } 75% { transform: translateX(3px) rotate(6deg); } }
.uc-theatre-line { font-size: 16.5px; font-weight: 400; }
.uc-theatre-line.is-swapping { animation: uc-beat 300ms var(--e-out); }
@keyframes uc-beat { from { opacity: 0; transform: translateY(6px); } }

/* Empty states are states, not blank pages: each says the one thing the reader
   can act on. */
.uc-none { font-size: 17px; border: 2px dashed var(--ink-12); border-radius: var(--r-3); padding: 18px; color: var(--ink-70); }
.uc-none span { font-size: 21px; }

/* ══ Buttons ══════════════════════════════════════════════════════════════ */
.uc-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  min-height: 52px; padding: 12px 20px; border-radius: var(--r-pill);
  border: 2px solid var(--ink-12); background: var(--cream); color: var(--ink);
  font-family: var(--display); font-size: 17px; font-weight: 700; cursor: pointer;
  transition: transform var(--d-2) var(--e-spring), background var(--d-1) linear, border-color var(--d-1) linear;
  -webkit-tap-highlight-color: transparent;
}
.uc-btn:hover { border-color: var(--coral); background: var(--cream-2); }
.uc-btn:active { transform: scale(0.97); }
.uc-btn.is-wide { width: 100%; }
.uc-btn.is-coral { background: var(--coral); border-color: var(--coral); color: #0B0D0A; }
.uc-btn.is-coral:hover { background: var(--coral-2); border-color: var(--coral-2); }
.uc-btn.is-on { background: var(--coral); border-color: var(--coral); color: #0B0D0A; }
.uc-btn.is-left { justify-content: flex-start; text-align: left; }
.uc-btn svg { width: 21px; height: 21px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }

/* ══ Calendar: what is waiting, and the bank ═══════════════════════════════ */
.uc-seg { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 4px; background: var(--ink-08); border-radius: var(--r-pill); position: relative; }
.uc-seg-pill { position: absolute; top: 4px; left: 4px; height: calc(100% - 8px); width: var(--sw, 50%); border-radius: var(--r-pill); background: var(--cream); box-shadow: var(--lift-1); transform: translateX(var(--sx, 0px)); transition: transform var(--d-3) var(--e-spring); }
.uc-seg button { position: relative; z-index: 1; min-height: 46px; border: 0; background: transparent; border-radius: var(--r-pill); font-family: var(--display); font-size: 16px; font-weight: 700; color: var(--ink-62); cursor: pointer; transition: color var(--d-2) var(--e-out); display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.uc-seg button.is-on { color: var(--ink); }
.uc-seg .cnt { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; background: var(--coral); color: #0B0D0A; font-size: 11.5px; line-height: 20px; font-weight: 700; }
.uc-panel.is-swapping { animation: uc-panel-in var(--d-3) var(--e-out) both; }
@keyframes uc-panel-in { from { opacity: 0; transform: translateY(10px); } }

.uc-cards { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.uc-card { border: 2px solid var(--ink-12); border-radius: var(--r-3); padding: 16px 18px; background: var(--cream); transition: transform var(--d-2) var(--e-out), border-color var(--d-2) var(--e-out); }
.uc-card.is-waiting { border-color: var(--coral); }
.uc-card-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.uc-card-top b { font-size: 18.5px; }
.uc-card p { margin-top: 6px; font-size: 16.5px; color: var(--ink-70); }
.uc-days { display: inline-flex; gap: 6px; }
.uc-day { display: inline-flex; align-items: center; justify-content: center; min-width: 32px; height: 32px; padding: 0 9px; border-radius: var(--r-pill); background: var(--peach); font-size: 15.5px; font-weight: 700; font-variant-numeric: tabular-nums; animation: uc-day-in 420ms var(--e-spring) both; }
.uc-day:nth-child(2) { animation-delay: 60ms; }
.uc-day:nth-child(3) { animation-delay: 120ms; }
@keyframes uc-day-in { from { transform: scale(0.4); opacity: 0; } }
.uc-note { margin-top: 10px; font-size: 15.5px; display: flex; align-items: center; gap: 8px; color: var(--ink-70); }
.uc-marks { display: grid; grid-template-columns: 1fr; gap: 9px; margin-top: 13px; }
@container uc (min-width: 460px) { .uc-marks { grid-template-columns: repeat(3, 1fr); } }
.uc-marks .uc-btn { min-height: 50px; font-size: 16px; padding: 10px 12px; }
/* No resize grip. It is the one control on the sheet drawn by the browser
   rather than by this file, it lands on the sheet's own bottom-right corner
   next to the drag handle, and the field grows itself as you type anyway. */
.uc-say { width: 100%; margin-top: 10px; min-height: 84px; max-height: 44vh; padding: 12px 14px; border-radius: var(--r-3); border: 2px solid var(--ink-12); background: var(--cream); font-family: var(--display); font-size: 17px; resize: none; overflow-y: auto; }
.uc-status { margin-top: 8px; font-size: 15.5px; min-height: 1.4em; color: var(--ink-70); }
.uc-status.is-good { color: var(--good); font-weight: 600; }

/* ══ Journal: Gallery wall ═══════════════════════════════════════════════════
   Journal entry redesign, Sept 9 2026 (docs/2026-09-09-journal-entry-redesign-
   decisions.md), porting the design lab's .ent-wallgrid/figure. A natural-
   ratio masonry column, not the old single flex column of fixed 4:3 boxes:
   the CARD is still the ENTRY and nothing else, and everything that changes
   the entry rather than its words is still behind one door -- this changes
   the LAYOUT and the photo's fit, not the entry's own behaviour.

   .uc-wallgrid is a second class alongside the shared .uc-cards, never a
   change to .uc-cards itself: that class is also the Calendar screen's
   Waiting/Ideas list (.uc-card, singular -- a different component), which
   has to stay a single flex column. */
/* ── A GRID, AND NOT A MASONRY COLUMN (owner, Sept 10 2026: "are the journal
   entries chronological? looks pretty random on journal page with new
   design") ────────────────────────────────────────────────────────────────
   They were, and they are: the payload is newest-first (wall-api.mjs' own
   order) and ucItems keeps it. What scrambled them was CSS `columns`, which
   fills COLUMN BY COLUMN -- so the top row of a four-column wall read 1, 3,
   5, 7 and the eye found no order in it at all. Two columns hid the problem
   rather than fixing it; widening the page for the owner's Journal ask is
   what made it plain.

   So the wall is a grid, which fills ROW BY ROW: left to right, top to
   bottom, strictly the order the entries were written. `align-items: start`
   is what keeps the Gallery wall's real promise (Sept 9 2026) -- every card
   is its own natural height and no photograph is cropped or stretched to fit
   a row. What is given up is the tight vertical packing: cards in a row now
   bottom out raggedly. That is the trade the owner took, and it is the right
   way round -- a wall you can read in order, with some air in it, beats a
   perfectly packed wall in an order nobody can follow.

   It went through two column layouts on the way to one track (auto-fill at
   220px and 260px, then those tracks capped to the day's own card count), and
   both are gone rather than left standing under an override: a rule that
   declares four columns and is beaten by a later `1fr` is a rule the next
   reader has to work out is dead. */
.uc-wallgrid {
  display: grid; grid-template-columns: 1fr;
  align-items: start; gap: 12px;
}
/* ── ONE CARD PER ROW (owner, Sept 10 2026: "maybe back to 1 row for journal
   for now") ────────────────────────────────────────────────────────────────
   Three columns fitted to the day's own card count fixed the lopsidedness and
   still left a day reading as a mosaic. A single column is the calmest thing
   this page can be: the entries run down in the order they were written, each
   one the width of the reading column, photographs big enough to actually
   look at. It is also the layout the row-major grid was already reaching for
   -- with one track, drawn order and read order cannot come apart at all.

   `--uc-n` is still written on the list by ucWallListHtml and is deliberately
   left unread here: the count is a fact about the day, and the next layout
   that wants it will not have to go and re-derive it.

   The wall keeps a measure of its own rather than filling the journal's wide
   column: a photograph 1280px across is a poster, and the words under it
   would run to a line nobody can read. */
@container uc (min-width: 900px) {
  .uc-wallgrid { max-width: 620px; margin-inline: auto; }
}
/* ══ The journal's head, and its voice notes ═══════════════════════════════
   Ported from the Gallery/Journal concept pass (Sept 10 2026) at the owner's
   ask. Colours are this console's tokens throughout, never the concept's own
   literals: it was authored against a standalone palette and this file has a
   dark theme those literals would fight (rova/CLAUDE.md's --ink hazard).
   `--kraft` is the one new pair, and it is stated for both themes below. */
:root, :host { --kraft: #D8B98C; --kraft-ink: #5B3F23; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --kraft: #4A3B28; --kraft-ink: #E7D2AE; }
}
:root[data-theme="dark"] { --kraft: #4A3B28; --kraft-ink: #E7D2AE; }

.uc-jhead { display: flex; align-items: center; gap: 14px; }
.uc-jhead-words { min-width: 0; flex: 1 1 auto; }
/* Share this day (Sept 10 2026), only drawn when ucDayIsShareable says the
   public day permalink can actually render one -- same gate as the site's
   own control, so the two can never offer a link the other would refuse. */
.uc-jshare {
  flex: 0 0 auto; width: 34px; height: 34px; padding: 0; border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  background: var(--ink-8, rgba(11,13,10,.08)); color: var(--ink-70, rgba(11,13,10,.7));
  transition: background .15s ease, color .15s ease;
}
.uc-jshare svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
.uc-jshare:hover { background: var(--coral); color: #0B0D0A; }
.uc-jshare--done { background: var(--coral); color: #0B0D0A; }
/* THE POSTMARK. A kraft stamp reads like something glued into a passport
   rather than a coloured numeral, which is what the concept settled on after
   three passes that did not. It is decorative and aria-hidden: the day and
   the town are said in words beside it for anybody who cannot see it. */
.uc-stamp {
  flex: 0 0 auto; width: 70px; height: 70px; border-radius: 50%;
  border: 2px solid var(--kraft-ink); background: var(--kraft);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; position: relative; transform: rotate(-6deg); padding: 0 6px;
}
.uc-stamp::before {
  content: ""; position: absolute; inset: 5px; border-radius: 50%;
  border: 1px dashed var(--kraft-ink); opacity: 0.6;
}
.uc-stamp b { font-size: 13px; font-weight: 800; color: var(--kraft-ink); line-height: 1.15; }
.uc-stamp span {
  font-size: 8px; font-weight: 600; letter-spacing: 0.03em; color: var(--kraft-ink);
  text-transform: uppercase; margin-top: 1px; line-height: 1.15;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
/* The day you are actually on wears the coral ring -- the console's accent,
   and NOT crimson, which is this console's alarm (see the note on
   .uc-lede.uc-hand). Since Sept 10 2026 the ring is the ONLY mark on this
   heading saying so: the "here now" pill beside it said the same thing in
   words the strip under the page was already saying. */
.uc-stamp.is-now { border-color: var(--coral-2); }
.uc-stamp.is-now b, .uc-stamp.is-now span { color: var(--coral-2); }
/* THE LEDE IN A HAND (owner). It is the one line on this screen that is a
   voice rather than a label, so it gets the handwriting accent and the
   crimson; the route above it stays in the display face, because a heading in
   a hand is a heading you cannot scan. */
/* CALMER THAN THE CONCEPT'S, AND CORAL RATHER THAN RED (owner, Sept 10 2026:
   "a little more on brand... a little less tilt", then "did you look at brand
   colours? red isn't on there").

   The colour was the real fault. `--crimson` IS in the site's palette, but in
   THIS console it is the alarm: it is `--bad`'s own value, and it paints the
   failure toast, the staff banner, an over-budget gauge, a held thread and a
   weather warning's edge. A handwritten line in it reads as something wrong
   with the day rather than as the day's own voice. Coral is the console's
   accent and is already what the chosen pill, the (+) and every hover wear.

   The tilt: the concept used -2deg, which is right for a masthead seen once
   and wrong for a line that changes every time you flick a day, where a long
   lede visibly climbs away from the heading above it. */
.uc-lede.uc-hand {
  font-family: var(--script); font-size: 24px; line-height: 1.3; font-weight: 600;
  color: var(--coral-2); transform: rotate(-0.5deg); transform-origin: left center;
  margin-top: 7px;
}
@media (prefers-reduced-motion: reduce) { .uc-lede.uc-hand { transform: none; } }

/* ── The voice note's own player ──────────────────────────────────────────
   The bars are DECORATION, deterministic on the entry's id (ucVoiceBars), and
   are never a reading of the sound: real peaks mean fetching and decoding the
   whole file, which is not a thing to do on a traveller's phone for a
   picture. What is real is the FILL -- `--uc-played` is currentTime/duration,
   written by ucWireVoice off the element's own events. */
.uc-voice { display: flex; align-items: center; gap: 12px; padding: 14px 14px 4px; }
.uc-voice audio { display: none; }
.uc-voice-play {
  flex: none; width: 38px; height: 38px; border-radius: 50%; border: 0; padding: 0;
  background: var(--coral); color: #0B0D0A; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform var(--d-2) var(--e-spring), background var(--d-1) linear;
}
.uc-voice-play:hover { background: var(--coral-2); }
.uc-voice-play:active { transform: scale(0.92); }
/* A triangle in CSS rather than an SVG: it is two states and one shape, and
   the pause bars are the same box with a different border. */
.uc-voice-tri {
  width: 0; height: 0; margin-left: 3px;
  border-left: 12px solid currentColor; border-top: 7px solid transparent; border-bottom: 7px solid transparent;
}
.uc-voice.is-playing .uc-voice-tri {
  width: 11px; height: 13px; margin-left: 0; border: 0;
  border-left: 4px solid currentColor; border-right: 4px solid currentColor;
}
.uc-voice-wave {
  position: relative; flex: 1 1 auto; display: flex; align-items: center;
  gap: 2px; height: 22px; min-width: 0;
}
.uc-voice-wave i { flex: 1 1 0; min-width: 2px; border-radius: 1px; background: var(--coral); opacity: 0.45; }
/* HOW FAR IN IT IS, and this is the half that is REAL. One line under the
   bars rather than lighting them one at a time: eleven bars is eleven steps,
   so a bar-by-bar fill would read as a progress bar with a resolution of nine
   seconds on a forty-seven second note. A line moves continuously and cannot
   imply the bars mean anything. `--uc-played` is currentTime/duration,
   written by ucWireVoice; it is absent until the first event, which is what
   the 0 fallback is for. */
.uc-voice-wave::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -5px; height: 2px;
  border-radius: 2px; background: var(--coral); transform-origin: left center;
  transform: scaleX(var(--uc-played, 0));
  transition: transform 120ms linear;
}
.no-motion .uc-voice-wave::after { transition: none; }
.uc-voice-clock {
  flex: none; font-size: 11.5px; color: var(--ink-62); font-variant-numeric: tabular-nums;
}
/* The transcript under the player is the entry's own words, unchanged. */
.uc-entry .uc-voice + .uc-entry-body { padding-top: 6px; }

/* A LITTLE MOVEMENT ON HOVER (owner: "some movement on hover too like with
   'audio' example"), lifted from the concept's own tile rule. It is a pointer
   affordance and nothing more, so it is off entirely under reduced motion and
   on any device without a fine pointer.

   A LIFT AND A SHADOW, NO TILT (owner, Sept 10 2026). The card used to turn
   -0.6deg with the lift; the shadow is what reads as "this is a thing you can
   pick up", and the rotation only put the words on it off true. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .uc-entry { transition: transform var(--d-2) var(--e-out), box-shadow var(--d-2) var(--e-out); }
  .uc-entry:hover { transform: translateY(-2px); box-shadow: 0 16px 30px -18px var(--ink-30); }
}

.uc-entry {
  /* The gap is the grid's now; the bottom margin and `break-inside: avoid`
     were both there to stop a CSS column splitting a card across its own
     break, which a grid cell cannot do. */
  margin: 0;
  border: 1.5px solid var(--ink-12); border-radius: var(--r-3); background: var(--paper); overflow: hidden;
  display: flex; flex-direction: column;
}
/* NATURAL RATIO, NO FORCED CROP (owner). The old fixed 4:3 box had no
   object-fit set at all, so a photo was actually stretched to fill it, not
   cropped -- and a second or third photo on one entry, block-level inside a
   box with a fixed height and overflow:hidden, was invisible past the
   first's own edge. The box now takes the photo's own shape, so every photo
   on a multi-photo entry stacks and shows in full. Video and audio keep
   their own real fixed frame (:not(.is-clip):not(.is-audio)) -- a media
   player earns a plain box; only a photograph earns the wall treatment. The
   door sheet's OWN turn preview (.uc-shotbox.is-square, ucOpenEntrySheet) is
   a different tree entirely and is untouched by any of this. */
.uc-entry .uc-shotbox:not(.is-clip):not(.is-audio) { aspect-ratio: auto; }
.uc-entry .uc-shot { display: block; width: 100%; height: auto; }
.uc-entry .uc-shot + .uc-shot { margin-top: 2px; }
/* The turn, kept (owner): a saved rotation still shows on the wall card, it
   just cannot lean on a fixed frame's ratio for its fit any more.
   `--uc-rot`/`--uc-rot-scale` are written by ucEntryHtml/ucFitTurnedShots
   (app.js) as custom properties rather than a literal transform, so this
   rule and the parallax keyframes below can both read the same rotation
   without one silently overriding the other -- an animated `transform` wins
   over a plain style attribute's, always, so the rotation has to live
   somewhere an animation can see it too. */
.uc-entry .uc-shot { transform: rotate(var(--uc-rot, 0deg)) scale(var(--uc-rot-scale, 1)); }
/* Real scroll-driven parallax, no JS, no library: each photo is oversized
   16% inside its own clipped figure and drifts against the scroll,
   alternating direction so the wall reads with real depth. Progressive
   enhancement -- a browser without animation-timeline just gets the plain
   masonry above, nothing breaks. */
@supports (animation-timeline: scroll()) {
  .uc-entry .uc-shotbox:not(.is-clip):not(.is-audio) .uc-shot {
    animation: uc-wall-parallax linear both;
    animation-timeline: scroll(nearest block);
    animation-range: cover 0% cover 100%;
  }
  /* Alternating on nth-of-type read as depth when the cards ran DOWN a
     masonry column. Row-major, the same rule alternates across a row, where
     two neighbours drifting opposite ways reads as a wobble rather than as
     depth -- so the whole wall drifts one way now and the alternation is
     gone with the columns it was drawn for. */
  .uc-entry .uc-shot { --uc-pd: 6%; }
  @keyframes uc-wall-parallax {
    from { transform: scale(calc(1.12 * var(--uc-rot-scale, 1))) rotate(var(--uc-rot, 0deg)) translateY(calc(var(--uc-pd, 0%) * -1)); }
    to   { transform: scale(calc(1.12 * var(--uc-rot-scale, 1))) rotate(var(--uc-rot, 0deg)) translateY(var(--uc-pd, 0%)); }
  }
}
.uc-shotbox.is-square { aspect-ratio: 1 / 1; border-radius: var(--r-2); }
.uc-shot { display: block; width: 100%; height: 100%; transition: transform var(--d-3) var(--e-out); }
.uc-shot.sky { background: linear-gradient(178deg, #7FB2D6, #DCEAF2 62%, #E7D7B4); }
.uc-shot.dusk { background: linear-gradient(184deg, #2C3050, #8E5A6C 48%, #E8A05C); }
.uc-entry-body { position: relative; padding: 12px 14px 26px; display: flex; flex-direction: column; gap: 8px; }
/* A words-only entry (no photo, no clip): the same big decorative quote mark
   the design lab used, standing in for the photo panel it does not have,
   rather than a blank paper tile among photographs. Coloured by
   .uc-textcard--peach/--dark below. */
.uc-textcard-mark {
  display: block; padding: 18px 16px 0; font-family: Georgia, 'Times New Roman', serif;
  line-height: 1; font-size: 84px; color: var(--coral); opacity: 0.35; pointer-events: none; user-select: none;
}
/* THE HEAD IS THE DOOR AND NOTHING ELSE (owner, Sept 10 2026). The coloured
   initial and the name have both gone: the initial was a picture of the first
   letter of a name printed two centimetres from it, and the name now signs
   the foot. `justify-content: flex-end` rather than the door's old
   `margin-left: auto`, because there is no longer anything for it to be
   pushed past. */
.uc-entry-top { display: flex; align-items: center; justify-content: flex-end; }
.uc-doorbtn { flex: 0 0 auto; width: 40px; height: 40px; margin-right: -8px; padding: 0;
              border: 0; border-radius: 50%; background: transparent; color: var(--ink-62); cursor: pointer;
              display: inline-flex; align-items: center; justify-content: center;
              transition: background var(--d-1) linear, color var(--d-1) linear, transform var(--d-2) var(--e-spring); }
.uc-doorbtn svg { width: 22px; height: 22px; fill: currentColor; stroke: none; }
.uc-doorbtn:hover { background: var(--ink-08); color: var(--ink); }
.uc-doorbtn:active { transform: scale(0.9); }
.uc-entry-words { font-size: 17px; line-height: 1.5; }
/* THE SIGNATURE (owner, Sept 10 2026: "move name to after 'the words' and do
   -Name @ time", in a script font). It replaces two things that used to sit
   apart -- a byline over the words, which read as a heading for them, and a
   bare clock pinned in the corner. Under the words and in a hand, the same
   two facts read as what they are: somebody putting their name to what they
   wrote. Right-aligned, because that is where a signature goes.

   The card's own bottom padding drops with it: the stamp was ABSOLUTE and
   needed 26px of clearance held open underneath the flow; this is in the
   flow and asks for nothing. */
.uc-sign {
  align-self: flex-end; margin-top: 2px;
  font-family: var(--script); font-size: 19px; line-height: 1.2;
  color: var(--ink-62); font-variant-numeric: tabular-nums;
}
.uc-entry-machine { font-size: 14px; font-style: italic; color: var(--ink-62); }
/* The words ARE the field. It is drawn as writing and not as a form control --
   no box until you are in it -- because a page of bordered textareas reads as
   something to fill in rather than as a journal. */
.uc-words { width: 100%; min-height: 1.5em; padding: 6px 8px; margin: -6px -8px;
            border: 1.5px solid transparent; border-radius: var(--r-2);
            background: transparent; color: var(--ink); resize: none; overflow: hidden;
            font-family: var(--display); font-size: 17px; line-height: 1.5;
            transition: border-color var(--d-1) linear, background var(--d-1) linear; }
.uc-words:hover { border-color: var(--ink-12); }
.uc-words:focus { outline: none; border-color: var(--coral); background: var(--cream); }
.uc-wstatus { font-size: 13px; color: var(--ink-62); min-height: 1.2em; }
.uc-wstatus.is-good { color: var(--good); font-weight: 600; }


/* ── ONE TEXT CARD, AND IT IS THE INVERSE OF THE PAGE ────────────────────
   ───────────────────────────────────────────────────────────────────────
   It was two, alternating on a hash of the entry's own id (Sept 9 2026:
   "black and beige"). The owner reversed that on Sept 10 2026 -- "I thought
   we made a decision here to make this black/cream based on light/dark
   theme... this is unreadable" -- and the reversal fixes something the
   alternation could not: a card whose colour is decided by an ID cannot know
   which theme it is drawn in, so one of the two was always fighting the page
   under it.

   ONE RULE, NO LITERALS. `--ink` and `--cream` re-point TOGETHER when the
   theme flips (the :root blocks at the head of this file), so the card is
   black-on-cream in the light theme, cream-on-black in the dark one, and the
   pale-on-pale failure this console has hit before (the --ink hazard in
   rova/CLAUDE.md) is not expressible here at all.

   Still written `.uc-entry.uc-textcard` (compound, two classes), never a
   lone `.uc-textcard`: a lone class has the same specificity as the base
   `.uc-entry` rule above and can lose to it on source order alone.

   `color` is inherited by nearly everything for free (admin.css's own
   `button,input,select,textarea{color:inherit}` reset); what needs saying
   here is every element that sets an EXPLICIT colour of its own. */
.uc-entry.uc-textcard { background: var(--ink); color: var(--cream); }
.uc-entry.uc-textcard .uc-doorbtn { color: color-mix(in srgb, var(--cream) 62%, transparent); }
.uc-entry.uc-textcard .uc-doorbtn:hover { background: color-mix(in srgb, var(--cream) 14%, transparent); color: var(--cream); }
.uc-entry.uc-textcard .uc-sign,
.uc-entry.uc-textcard .uc-entry-machine { color: color-mix(in srgb, var(--cream) 62%, transparent); }
/* NO ROOM HELD OPEN OVER THE WORDS (owner, Sept 10 2026: "lots of blank space
   above words, not needed"). The mark stood in an 84px line box with 18px of
   padding over it, IN THE FLOW, so the card opened with most of a thumb of
   nothing and then a 40px row holding one door button, and only then the
   first sentence. Shrinking the glyph was not enough, because the flow was
   the problem and not the size.

   It is a MARK, not a panel, so it is taken out of the flow entirely: pinned
   to the card's top-left corner, costing no height at all. The door keeps the
   top band on the right, where it already was, and the two now share one row
   instead of stacking. The words start directly under them.

   `pointer-events: none` was already on it, so the mark cannot eat a press
   meant for the card underneath. */
.uc-entry.uc-textcard { position: relative; }
.uc-entry.uc-textcard .uc-textcard-mark {
  position: absolute; top: 2px; left: 14px; padding: 0; font-size: 52px;
}
.uc-entry.uc-textcard .uc-entry-body { padding-top: 6px; padding-bottom: 12px; }
/* ...and where there is no door (somebody else's entry) the top band has
   nothing in it at all, so the words would ride up under the mark. */
.uc-entry.uc-textcard .uc-entry-body:not(:has(.uc-entry-top)) { padding-top: 40px; }

/* ONE DOOR INTO THE JOURNAL, AND IT IS A CIRCLE IN THE MIDDLE (owner, v4). It
   was a labelled pill in the corner of the scroller, so it moved with the list
   and had to be caught. It rides the bar now -- in the shell, over the middle
   of the tabs, half on and half off -- so it is in one place on every screen it
   appears on, it is the biggest target on the screen, and the overlap is what
   says it belongs to the bar rather than to the page behind it.
   No word on it: a plus in a coral circle over a tab bar is the one control on
   a phone nobody has needed a label for in a decade. The label is still there
   for anybody who cannot see it, as aria-label and as the tooltip. */
/* A plain floating circle at every width. Centered at the foot on a handset
   (v4 fourth pass, owner: the nav moved off the bottom entirely, so there is
   no bar left for the plus to ride, and a centred circle over nothing reads
   as deliberate the way a corner one does not); a rail has a natural corner
   for it at a desk, unchanged from before. */
.uc-fab {
  position: absolute; z-index: 22; overflow: hidden;
  /* WAY BIGGER (owner): the one door into the journal, on a screen whose
     whole point is to get you posting, is worth more thumb than a 58px
     circle gave it. 82px is roughly the trailbtn's own reach, not a round
     number picked to look big. */
  left: 50%; margin-left: -41px; bottom: calc(20px + env(safe-area-inset-bottom));
  width: 82px; height: 82px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 3px solid var(--cream); border-radius: 50%;
  background: var(--coral); color: #0B0D0A; cursor: pointer;
  box-shadow: var(--lift-2);
  transition: transform var(--d-2) var(--e-spring), background var(--d-1) linear;
  /* LAST, AND ON PURPOSE (owner, v4 fifth pass). It came up over the same
     560ms the first cards were rising through, so the second card appeared to
     land on top of a button that was still fading in and the button then
     surfaced through it -- two things arriving in one place, reading as one
     of them going wrong. Delayed past the cards' own arrival (380ms + their
     stagger), it is the last thing on the screen to appear, which is what it
     is: the thing you reach for once you have read what is there. `both`
     holds it at nothing until then, so it is absent rather than half-drawn. */
  animation: uc-fab-in 460ms 420ms var(--e-spring) both;
  -webkit-tap-highlight-color: transparent;
}
@keyframes uc-fab-in { from { transform: scale(0.4) translateY(14px); opacity: 0; } }
/* Arriving and leaving WITH a section, rather than blinking. It could only
   appear or not while the move was a pair of document snapshots; it is a live
   element for the whole of the move now, so it can leave. The delay the boot
   rule carries is deliberately not here: on a section change the (+) is not
   waiting for cards to finish rising, it is following the reader. */
.uc-fab.is-coming { animation: uc-fab-in 380ms var(--e-spring) both; }
.uc-fab.is-going { animation: uc-fab-out 200ms var(--e-exit) both; }
@keyframes uc-fab-out { to { transform: scale(0.4) translateY(14px); opacity: 0; } }
.uc-fab:hover { background: var(--coral-2); }
.uc-fab:active { transform: scale(0.92); }
.uc-fab svg { width: 34px; height: 34px; fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; transition: transform var(--d-2) var(--e-spring); }
@container uc (min-width: 900px) {
  /* CENTRED AT A DESK AS WELL (owner, Sept 10 2026). It sat in the bottom
     right corner on the argument that a rail gives a desk a natural corner
     for it. It does -- but the journal is a CENTRED wall of cards now, and a
     control in the corner of a wide window is a control a long way from
     what it is about.

     Centred over the BODY and not over the shell: the rail is 232px of the
     shell's width and takes no part in this, so half of what is left is
     116px right of the shell's own middle. `margin-left` is the circle's
     own half-width, the same way the handset rule centres it. */
  .uc-fab { left: calc(50% + 116px); margin-left: -41px; right: auto; bottom: 28px; }
}

/* THE PLUS OPENS (borrowed from v5.1's mt-post/mj-post bloom, owner's ask on
   consolidation). The button blooms coral-on-cream from its own centre, the
   plus turns, and the journal is already rising by the time the bloom has
   finished covering the screen -- the button BECOMES the sheet rather than
   pointing at it. The literal cream (not var(--cream)) is deliberate, same
   reasoning as v5.1: it paints on coral, which is the same colour in both
   themes, so a token would follow the theme away from the surface it has to
   read against. Timing lives in JS (openPost's bloomThen), not here: the
   bloom runs 640ms and the sheet opens partway through it on purpose. */
.uc-fab.is-opening { animation: uc-fab-press 640ms cubic-bezier(0.2, 0.7, 0.3, 1) both; }
.uc-fab.is-opening::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: 26px; height: 26px;
  margin: -13px 0 0 -13px; border-radius: 50%; pointer-events: none;
  background: rgba(255, 250, 240, 0.55);
  animation: uc-fab-bloom 640ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
.uc-fab.is-opening svg { animation: uc-fab-plus-turn 640ms cubic-bezier(0.2, 0.7, 0.3, 1) both; }
@keyframes uc-fab-press {
  0%   { transform: none; }
  18%  { transform: scale(0.9); }
  55%  { transform: scale(1.06); }
  100% { transform: none; }
}
@keyframes uc-fab-bloom {
  from { transform: scale(1); opacity: 0.55; }
  to   { transform: scale(18); opacity: 0; }
}
@keyframes uc-fab-plus-turn {
  0%   { transform: rotate(0) scale(1); }
  45%  { transform: rotate(90deg) scale(1.3); }
  100% { transform: rotate(90deg) scale(1); }
}

/* ══ Add to today: the (+) on Route fans out Eat, See, Do ══════════════════
   Ported from the design sandbox (rova/design/console-v4, Sept 12 2026, seven
   passes with the owner). On Route the (+) does not bloom: what it opens is
   three words, not a screen, so it only turns and three rays fly out of it.
   The journal's (+) keeps its bloom. */
.uc-fab.is-menu-open svg { transform: rotate(45deg); }
/* A transparent catcher for "tap anywhere else", under the fab (22) so the
   fab's own second tap still reaches it and closes the menu. */
.uc-addscrim { position: absolute; inset: 0; z-index: 21; -webkit-tap-highlight-color: transparent; }
/* RADIAL, LIKE THE SUN (owner). The menu is the fab's own box, and every ray
   starts at its centre and flies to a fixed --dx/--dy. The fab is centred at
   every width in this console, so the fan is symmetric at every width too. */
.uc-addmenu {
  position: absolute; z-index: 23; pointer-events: none;
  left: 50%; margin-left: -41px; bottom: calc(20px + env(safe-area-inset-bottom));
  width: 82px; height: 82px;
}
@container uc (min-width: 900px) {
  .uc-addmenu { left: calc(50% + 116px); bottom: 28px; }
}
.uc-addopt {
  position: absolute; left: 50%; top: 50%; pointer-events: auto;
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 48px; padding: 0 18px 0 14px; border-radius: var(--r-pill);
  border: 2px solid var(--ink-12); background: var(--cream); color: var(--ink);
  font-family: var(--display); font-size: 16px; font-weight: 700; white-space: nowrap;
  cursor: pointer; box-shadow: var(--lift-2);
  opacity: 0; transform: translate(-50%, -50%) scale(0.3);
  transition: transform var(--d-3) var(--e-spring), opacity var(--d-2) var(--e-out),
              background var(--d-1) linear, border-color var(--d-1) linear;
}
.uc-addopt:nth-child(1) { --dx: -118px; --dy: -74px; transition-delay: 0ms; }
.uc-addopt:nth-child(2) { --dx: 0px;    --dy: -136px; transition-delay: 45ms; }
.uc-addopt:nth-child(3) { --dx: 118px;  --dy: -74px; transition-delay: 90ms; }
.uc-addmenu[data-n="2"] .uc-addopt:nth-child(1) { --dx: -74px; --dy: -112px; }
.uc-addmenu[data-n="2"] .uc-addopt:nth-child(2) { --dx: 74px;  --dy: -112px; }
.uc-addmenu[data-n="1"] .uc-addopt:nth-child(1) { --dx: 0px;   --dy: -120px; }
.uc-addmenu.is-up .uc-addopt {
  opacity: 1;
  transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(1);
}
.uc-addopt:hover { border-color: var(--coral); background: var(--peach); }
.uc-addopt:active { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.92); }
.uc-addopt:focus-visible { outline: 3px solid var(--coral); outline-offset: 2px; }
.uc-addopt-ic { font-size: 19px; line-height: 1; }
.no-motion .uc-addopt { transition: none; }

/* FULL SCREEN (owner: "there's space to use full screen"). Opted into with
   `full: true`, never the base .uc-sheet: it holds the field AND the trip's
   own record on one scroll. It fades rather than sliding, because it has no
   edge to slide from, and the X is the one way out (no drag-to-dismiss on a
   screen whose long list is scrolled with the same thumb). */
.uc-sheet.uc-sheet--full {
  left: 0; right: 0; top: 0; bottom: 0; max-height: none;
  border-radius: 0; transform: none; opacity: 0;
  transition: opacity var(--d-2) var(--e-out);
}
.uc-sheet.uc-sheet--full.is-up { opacity: 1; }
.uc-sheet--full .uc-grab { display: none; }
.uc-sheet--full .uc-sheet-head { padding-top: calc(14px + env(safe-area-inset-top)); }
.uc-sheet--full .uc-sheet-body { width: 100%; max-width: 640px; margin: 0 auto; }

/* THE WORDS ARE THE PANEL (owner: "make text entry the MAJOR thing"). */
.uc-addsheet { display: flex; flex-direction: column; gap: 18px; }
.uc-addfield {
  display: block; width: 100%; min-height: 108px; padding: 16px 18px; margin-bottom: 4px;
  border: 2px solid var(--ink-12); border-radius: var(--r-3);
  background: var(--cream); color: var(--ink);
  font-family: var(--display); font-size: 21px; font-weight: 700; line-height: 1.35;
  resize: vertical; transition: border-color var(--d-1) linear;
}
.uc-addfield::placeholder { color: var(--ink-62); font-weight: 400; }
.uc-addfield:hover { border-color: var(--ink-30); }
.uc-addfield:focus { outline: none; border-color: var(--coral); }
.uc-addtoggle .uc-switch-words b { transition: opacity var(--d-1) linear; }
.uc-addtoggle-rova { display: block; flex: 0 0 auto; width: 28px; height: 28px; line-height: 0; }
.uc-addtoggle-rova img { image-rendering: crisp-edges; image-rendering: pixelated; display: block; }
.uc-addstatus:empty { display: none; }
.uc-addstatus { font-size: 15px; color: var(--ink-70); }
.uc-addstatus.is-bad { color: var(--crimson); }
/* The seam between the asking and the trip's own memory: a label, not a tab. */
.uc-addrule {
  display: flex; align-items: center; gap: 10px;
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-62);
}
.uc-addrule::before, .uc-addrule::after { content: ''; flex: 1 1 auto; height: 1px; background: var(--ink-12); }
/* Smaller HERE only (owner): the stop row's own Edit-mode list keeps its size. */
.uc-addsheet .uc-found { gap: 6px; }
.uc-addsheet .uc-use { padding: 9px 66px 9px 12px; }
.uc-addsheet .uc-found b { font-size: 15px; }
.uc-addsheet .uc-found .town { font-size: 12.5px; }
.uc-addsheet .uc-found p { font-size: 13.5px; margin-top: 2px; }
.uc-addsheet .uc-found .uc-said { font-size: 13px; }
.uc-addsheet .uc-use-mark { top: 8px; right: 9px; padding: 4px 9px; font-size: 12px; }
.uc-addsheet .uc-foundhead { margin-top: 4px; }
/* THE ROW A SAVE JUST LANDED ON. No fill mode: the resting state is the
   row's own, so a paused animation in a background tab leaves nothing behind. */
.uc-stop.is-justadded { animation: uc-justadded 1300ms var(--e-out); border-radius: 14px; }
@keyframes uc-justadded {
  0%, 70% { background: color-mix(in srgb, var(--coral) 18%, transparent); }
  100%    { background: transparent; }
}

/* ══ You: the morning message ══════════════════════════════════════════════ */
.uc-switch { display: flex; align-items: center; gap: 14px; min-height: 56px; cursor: pointer; }
.uc-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.uc-track { flex: 0 0 auto; width: 60px; height: 34px; border-radius: var(--r-pill); background: var(--ink-12); position: relative; transition: background var(--d-2) var(--e-out); }
.uc-knob { position: absolute; top: 3px; left: 3px; width: 28px; height: 28px; border-radius: 50%; background: var(--cream); box-shadow: 0 1px 3px #0B0D0A59; transition: transform var(--d-3) var(--e-spring), width var(--d-2) var(--e-out); }
.uc-switch input:checked + .uc-track { background: var(--coral); }
.uc-switch input:checked + .uc-track .uc-knob { transform: translateX(26px); }
.uc-switch:active .uc-knob { width: 34px; }
.uc-switch input:checked + .uc-track:active .uc-knob { transform: translateX(20px); }
.uc-switch input:focus-visible + .uc-track { outline: 3px solid var(--coral); outline-offset: 3px; }
.uc-switch-words b { display: block; font-size: 18px; }
.uc-switch-words span { font-size: 15.5px; color: var(--ink-70); }
.uc-times { display: flex; flex-wrap: wrap; gap: 8px; }
.uc-times .uc-btn { flex: 1 1 auto; min-width: 96px; min-height: 48px; font-variant-numeric: tabular-nums; }
.uc-preview { border-left: 4px solid var(--coral); padding: 4px 0 4px 14px; }
.uc-preview b { display: block; font-size: 15px; color: var(--ink-62); }
.uc-preview p { margin-top: 5px; font-size: 17.5px; }
.uc-fold { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--d-3) var(--e-out), opacity var(--d-2) var(--e-out); opacity: 0; }
.uc-fold.is-open { grid-template-rows: 1fr; opacity: 1; }
.uc-fold > div { overflow: hidden; min-height: 0; display: flex; flex-direction: column; gap: 14px; }
.uc-fold.is-open > div { padding-top: 14px; }

/* ══ Sheets ════════════════════════════════════════════════════════════════
   One component: the ask, the post, and Rova all come up as this. It is
   DRAGGABLE, and a drag that is let go of decides by velocity rather than by
   distance, because a fast short flick is a dismissal and a slow long pull is
   somebody reading. */
.uc-scrim {
  position: absolute; inset: 0; z-index: 30;
  background: #0B0D0A; opacity: 0;
  transition: opacity var(--d-3) var(--e-out);
  -webkit-tap-highlight-color: transparent;
}
.uc-scrim.is-on { opacity: 0.42; }
.uc-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 31;
  max-height: 86%; display: flex; flex-direction: column;
  background: var(--cream); color: var(--ink);
  border-radius: 26px 26px 0 0;
  box-shadow: var(--lift-3);
  transform: translateY(100%);
  transition: transform var(--d-3) var(--e-out);
  will-change: transform;
  padding-bottom: env(safe-area-inset-bottom);
}
.uc-sheet.is-up { transform: translateY(0); }
.uc-sheet.is-dragging { transition: none; }
.uc-grab { flex: 0 0 auto; padding: 10px 0 4px; display: flex; justify-content: center; cursor: grab; touch-action: none; }
.uc-grab::before { content: ""; width: 40px; height: 5px; border-radius: 3px; background: var(--ink-30); }
.uc-sheet-head { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; padding: 4px 18px 10px; }
.uc-sheet-head h2 { font-size: 20px; font-weight: 700; flex: 1 1 auto; }
.uc-x { flex: 0 0 auto; width: 40px; height: 40px; border: 0; border-radius: 50%; background: var(--ink-08); color: var(--ink); font-size: 21px; line-height: 1; cursor: pointer; transition: background var(--d-1) linear, transform var(--d-2) var(--e-spring); }
.uc-x:hover { background: var(--ink-12); }
.uc-x:active { transform: scale(0.9); }

/* ── Centered (journal entry redesign, Sept 9 2026) ──────────────────────────
   docs/2026-09-09-journal-entry-redesign-decisions.md. ucOpenPost's own sheet
   only, opted in with `centered: true`, never the base .uc-sheet rule above:
   .uc-sheet is shared with the entry door's "This entry" sheet
   (ucOpenEntrySheet), which is a short utility sheet that stays a bottom
   sheet. Always a centered card, phone included -- there is only one
   position to get right, at every width, so no breakpoint is added here
   (the console's own desktop one is `@container uc (min-width: 900px)`,
   used elsewhere in this file, and centering does not need it). Every rule
   is written `.uc-sheet.uc-sheet--centered` (compound, not a lone modifier
   class) so it always beats the base rule above regardless of source order. */
.uc-sheet.uc-sheet--centered {
  left: 50%; right: auto; top: 50%; bottom: auto;
  width: min(460px, 92%); max-height: 84%;
  border-radius: 22px;
  transform: translate(-50%, -46%) scale(0.94);
  opacity: 0;
  transition: transform var(--d-3) var(--e-spring), opacity var(--d-2) var(--e-out);
}
.uc-sheet.uc-sheet--centered.is-up { transform: translate(-50%, -50%) scale(1); opacity: 1; }
.uc-sheet.uc-sheet--centered.is-dragging { transition: none; }
/* No grab handle on a dialog that isn't anchored to an edge: closing it is
   the X, same as every other overlay in this console. Hiding it is enough --
   ucArmSheetDrag's pointerdown listener is still attached but a hidden
   element takes no pointer events, so the drag-to-dismiss logic is inert
   here without touching that shared function. */
.uc-sheet--centered .uc-grab { display: none; }
.uc-sheet--centered .uc-sheet-head { padding-top: 20px; }

/* A photo, at full view (owner). The same door as the sheet's own -- a scrim
   and an X, X DISMISSABLE -- but the whole screen and object-fit:contain
   rather than a card of text: a photograph is the content here, not
   something written about one. */
.uc-shot[role="button"] { cursor: zoom-in; }
.uc-photoview {
  position: absolute; inset: 0; z-index: 45;
  display: flex; align-items: center; justify-content: center;
  background: #0B0D0AE8;
  opacity: 0; transition: opacity var(--d-2) var(--e-out);
}
.uc-photoview.is-on { opacity: 1; }
.uc-photoview.is-instant { transition: none; }
.uc-photoview-img {
  max-width: min(94vw, 900px); max-height: 88vh; object-fit: contain;
  border-radius: 4px;
  transform: scale(0.96); transition: transform var(--d-2) var(--e-spring);
}
.uc-photoview.is-on .uc-photoview-img { transform: scale(1); }
.uc-photoview-x {
  position: absolute; z-index: 1;
  top: calc(14px + env(safe-area-inset-top)); right: 14px;
  background: color-mix(in srgb, #0B0D0A 55%, transparent); color: var(--cream);
}
.uc-photoview-x:hover { background: color-mix(in srgb, #0B0D0A 70%, transparent); }
.uc-sheet-body { flex: 1 1 auto; overflow-y: auto; padding: 4px 18px 20px; display: flex; flex-direction: column; gap: 14px; }
/* Inside the entry's sheet: the turn, the pair, the strip of days, the delete.
   Each one laid out as itself, which is the room the inline disclosure did not
   have. */
.uc-turn { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.uc-turn .uc-shotbox { max-width: 190px; align-self: center; }
/* Icon only (owner, v4 second pass): the picture is right there, so "Turn the
   photo" as a full-width sentence repeated what the thumb was about to do. */
.uc-iconbtn {
  width: 46px; height: 46px; padding: 0; border-radius: 50%;
  border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color var(--d-1) linear, background var(--d-1) linear, transform var(--d-2) var(--e-spring);
}
.uc-iconbtn svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.uc-iconbtn:hover { border-color: var(--coral); background: var(--peach); }
.uc-iconbtn:active { transform: rotate(90deg); }
.uc-field { display: flex; flex-direction: column; gap: 9px; }
.uc-flabel { font-size: 12.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-62); }
/* THE DAY AS A STRIP YOU SCRUB, not a select. Thirty days in a dropdown on a
   phone is a wheel somebody has to land on; thirty days in a row you push along
   is the same choice with a target you can hit. Scroll-snapped so a flick stops
   on a day rather than between two. */
.uc-dayscrub { display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 8px;
               scroll-snap-type: x proximity; overscroll-behavior-x: contain; }
.uc-dayscrub::-webkit-scrollbar { height: 5px; }
.uc-dayscrub::-webkit-scrollbar-thumb { background: var(--ink-12); border-radius: 5px; }
.uc-dayb { flex: 0 0 auto; scroll-snap-align: center;
           min-width: 48px; height: 48px; padding: 0 10px; border-radius: var(--r-2);
           border: 2px solid var(--ink-12); background: var(--cream); color: var(--ink);
           font-family: var(--display); font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums;
           cursor: pointer; transition: background var(--d-1) linear, border-color var(--d-1) linear, transform var(--d-2) var(--e-spring); }
.uc-dayb:hover { border-color: var(--coral); }
.uc-dayb:active { transform: scale(0.92); }
.uc-dayb.is-on { background: var(--coral); border-color: var(--coral); color: #0B0D0A; }
/* THE SHEET'S FOOT (owner, v6). Save is the wide control, because it is what
   somebody opened the sheet to press; the delete is a control-sized trash at
   the far LEFT, which on a handset is the corner a right thumb reaches last. */
.uc-sheetfoot { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
.uc-sheetfoot .uc-btn { flex: 1 1 auto; }
/* IT TRANSFORMS RATHER THAN SWAPPING ITS WORDS. Armed, the same button grows
   its own question out of itself and turns the failure colour: one control
   changing what it is asking, which the eye follows, instead of a new label in
   an old box, which it does not. max-width and not width, so the growth is
   driven by the words and nothing is measured in JS. */
.uc-trash {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0;
  min-height: 50px; padding: 0 13px; border-radius: var(--r-pill);
  border: 1.5px solid color-mix(in srgb, var(--bad) 40%, transparent);
  background: var(--cream); color: var(--bad); cursor: pointer;
  font-family: var(--display); font-size: 15.5px; font-weight: 700;
  transition: background var(--d-2) var(--e-out), border-color var(--d-2) var(--e-out),
              color var(--d-2) var(--e-out), gap var(--d-2) var(--e-out), transform var(--d-2) var(--e-spring);
}
.uc-trash svg { width: 21px; height: 21px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.uc-trash-say { max-width: 0; overflow: hidden; white-space: nowrap; opacity: 0;
                transition: max-width var(--d-3) var(--e-out), opacity var(--d-2) var(--e-out); }
.uc-trash:hover { border-color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }
.uc-trash:active { transform: scale(0.94); }
.uc-trash.is-armed { gap: 8px; background: var(--bad); border-color: var(--bad); color: var(--cream); }
.uc-trash.is-armed .uc-trash-say { max-width: 130px; opacity: 1; }
/* Armed, then done, then undoable: three chances not to delete a photograph of
   a day nobody gets back. */
.uc-btn.is-danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
.uc-btn.is-danger:hover { border-color: var(--bad); background: color-mix(in srgb, var(--bad) 10%, transparent); }
.uc-btn.is-danger.is-armed { background: var(--bad); border-color: var(--bad); color: var(--cream); }
/* ── Attach pickers (journal entry redesign, Sept 9 2026) ────────────────────
   Two small, quiet, real pickers -- .ent-attachrow/.ent-quiet/.ent-attachpreview
   in the design lab, restated -- replacing the single dashed drop tile a
   photo or a clip used to compete with the words in. */
.uc-attachrow { display: flex; gap: 8px; }
.uc-quiet {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1.5px solid var(--ink-12); background: transparent; color: var(--ink-70);
  font-family: var(--display); font-size: 14px; font-weight: 600; border-radius: var(--r-pill);
  padding: 9px 15px; cursor: pointer;
  transition: border-color var(--d-1) linear, color var(--d-1) linear, transform var(--d-1) linear;
}
.uc-quiet:hover { border-color: var(--ink); color: var(--ink); }
.uc-quiet:active { transform: translateY(1px); }
.uc-quiet svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.uc-attachpreview { position: relative; border-radius: var(--r-2); overflow: hidden; background: var(--ink-08); }
.uc-attachpreview img, .uc-attachpreview video { display: block; width: 100%; max-height: 260px; object-fit: contain; background: var(--ink-08); }
.uc-previewx {
  position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: color-mix(in srgb, #0B0D0A 55%, transparent); color: var(--cream);
  display: inline-flex; align-items: center; justify-content: center;
}
.uc-previewx:hover { background: color-mix(in srgb, #0B0D0A 70%, transparent); }
.uc-previewx svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }

/* THE DAY STRIP, shared by Today's own nav and Journal's end-of-scroll
   one -- a wider, two-line sibling of .uc-dayscrub above (weekday over date,
   since these ARE navigation and not a field inside a sheet). `--dn-stretch`
   is written by ucArmDayStrip's own rubber-band and eases back through
   --e-spring, the same overshoot every pressed control in this file answers
   back with; `is-grabbed` kills the transition while a finger is actually
   holding it, so the strip tracks the drag with no lag and only springs on
   release. */
/* POSITIONED SO THE PILLS ARE MEASURED AGAINST THE SCROLLER ITSELF. Every
   sum in this strip -- which pill is nearest the middle, where a tap should
   ease to, where the boot centres -- reads `pill.offsetLeft`, and offsetLeft
   is relative to the nearest POSITIONED ancestor and shifts with any scroll
   between the two. With the fade wrapper positioned and the scroller not, the
   pills were measured from outside their own scroller and every reading moved
   as the strip did: a tap on day 21 landed on day 3. */
.uc-dnstrip {
  position: relative;
  display: flex; gap: 8px; padding: 4px 0 10px; overflow-x: auto;
  overscroll-behavior-x: contain; touch-action: pan-y;
  -webkit-user-select: none; user-select: none;
  transform: translateX(var(--dn-stretch, 0px));
  transition: transform var(--d-2) var(--e-spring);
}
/* THE FADE IS ON A WRAPPER, NOT ON THE SCROLLER (the mock). A mask on the
   scroller itself is painted in the scroller's own coordinate space, so the
   soft edges travel with the content instead of staying at the window's two
   sides. The wrapper also bleeds the strip past the page's gutter and pads it
   back, so a pill can sit under the fade rather than stopping short of it. */
.uc-dnfade {
  position: relative; margin: 0 -16px; padding: 0 16px;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
}
.uc-dnstrip.is-grabbed { transition: none; cursor: grabbing; }
.uc-dnstrip::-webkit-scrollbar { display: none; }
/* NO CSS SCROLL SNAP. `scroll-snap-type: x proximity` was the one thing here
   the mock does not have, and it is not a spare belt: the browser's own snap
   runs DURING the JS glide and the JS ease, so a flick settled twice -- once
   where the momentum ran out and again where the engine decided, which is the
   stutter at the end of every flick. ucSnapDayStrip is the only settle. */
.uc-dnpill {
  flex: 0 0 auto; touch-action: pan-y;
  width: 52px; padding: 9px 0 8px; border-radius: var(--r-2);
  border: 1px solid var(--ink-12); background: var(--ink-08); color: var(--ink);
  cursor: pointer;
  transition: background var(--d-1) linear, border-color var(--d-1) linear, transform var(--d-2) var(--e-spring);
}
.uc-dnpill .wd { display: block; font-family: var(--mono); font-size: 10px; font-weight: 400; letter-spacing: 0.06em; color: var(--ink-62); }
.uc-dnpill .dn { display: block; font-family: var(--display); font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 3px; }
.uc-dnpill:hover { border-color: var(--coral); }
.uc-dnpill:active { transform: scale(0.93); }
.uc-dnpill.is-today:not(.is-on) { border-color: var(--coral); }
.uc-dnpill.is-on { background: var(--coral); border-color: var(--coral); }
.uc-dnpill.is-on .wd, .uc-dnpill.is-on .dn { color: #0B0D0A; }

/* Today's own strip, AT THE FOOT OF THE PAGE, IN THE FLOW (owner: "don't do
   floating bottom any more, just put it at the bottom of the page") --
   drawn by ucViewToday itself now, the last thing on the page apart from
   Rova's own trail button, and it scrolls away with the reading like every
   other row instead of sitting pinned over it. Shares .uc-daynav-end's
   spacing with Journal's own strip below rather than a second rule saying
   the same thing. */
.uc-daynav-foot { margin: 18px 0 10px; }

/* Journal's own day nav, AT THE END OF THE SCROLL (owner): Journal already
   has the FAB living at the foot of the screen, and a second thing pinned
   there would be two controls contesting one corner. In the flow, after the
   last card, so it comes into view once the reader has reached the end of
   it. */
.uc-daynav-end { margin: 4px 0 10px; }

/* THE LIVE SWIPE PREVIEW (owner: "previewed x to x on calendar swipe"),
   shared by both strips: "Day 9 · Wed, Sep 9", set once on paint and
   overwritten live while a finger is on the strip (ucPaintDayPreview).
   aria-hidden -- the pill it is naming already carries the same words for a
   screen reader, so this is a sighted-only convenience and never a second
   announcement of the same choice. */
.uc-dnpreview {
  font-family: var(--display); font-size: 15px; font-weight: 700;
  color: var(--ink); text-align: center; margin: 0 0 6px;
  min-height: 1.3em; /* holds its own row so pills below it never jump */
}

/* Rova's console, full screen and growing out of the button that opened it.
   --ox/--oy are that button's centre. A browser with no clip-path support gets
   the fade on the same rule, which is the whole fallback. */
/* Rova's console is INK-on-cream, and under the dark theme --ink is the LIGHT
   value, so this panel inverts with the rest of the console rather than staying
   dark. That is admin.css's own reading and it is kept: what the panel is for is
   being the opposite of the screen it came out of, and it still is. */
.uc-rchat {
  position: absolute; inset: 0; z-index: 40;
  display: flex; flex-direction: column;
  background: var(--ink); color: var(--cream);
  --ox: 50%; --oy: 8%;
  animation: uc-rchat-in 420ms var(--e-out) both;
}
@keyframes uc-rchat-in {
  from { opacity: 0.25; clip-path: circle(3% at var(--ox) var(--oy)); }
  to { opacity: 1; clip-path: circle(155% at var(--ox) var(--oy)); }
}
.uc-rchat.is-going { animation: uc-rchat-out 260ms var(--e-exit) both; }
@keyframes uc-rchat-out {
  from { opacity: 1; clip-path: circle(155% at var(--ox) var(--oy)); }
  to { opacity: 0; clip-path: circle(3% at var(--ox) var(--oy)); }
}
.uc-rchat-top { flex: 0 0 auto; display: flex; align-items: center; gap: 9px; padding: calc(14px + env(safe-area-inset-top)) 16px 14px; border-bottom: 1px solid color-mix(in srgb, var(--cream) 17%, transparent); font-size: 16px; }
.uc-rchat-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--coral); animation: uc-pulse 2.4s ease-in-out infinite; }
@keyframes uc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.uc-rchat-sub { font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: color-mix(in srgb, var(--cream) 64%, transparent); }
.uc-rchat-top .uc-x { margin-left: auto; background: color-mix(in srgb, var(--cream) 12%, transparent); color: var(--cream); }
.uc-rchat-body { flex: 1 1 auto; overflow-y: auto; padding: 26px 20px calc(26px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; align-items: center; gap: 14px; justify-content: safe center; }
.uc-rchat-body > * { flex: 0 0 auto; }
.uc-rchat-em { font-size: 62px; line-height: 1; animation: uc-wag 3.2s ease-in-out infinite; }
@keyframes uc-wag { 0%, 100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
.uc-term { font-family: var(--mono); font-size: 14px; letter-spacing: 0.06em; color: color-mix(in srgb, var(--cream) 82%, transparent); min-height: 1.5em; text-align: center; }
.uc-term .cur { display: inline-block; width: 8px; height: 1em; background: var(--cream); vertical-align: -2px; animation: uc-cur 1s steps(2) infinite; }
@keyframes uc-cur { 50% { opacity: 0; } }
.uc-soon { font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: color-mix(in srgb, var(--cream) 64%, transparent); }
.uc-eg { list-style: none; margin: 0; padding: 0; width: 100%; max-width: 34em; display: flex; flex-direction: column; gap: 9px; }
.uc-eg li { display: flex; align-items: center; gap: 12px; font-size: 16.5px; background: color-mix(in srgb, var(--cream) 8%, transparent); border: 1px solid color-mix(in srgb, var(--cream) 17%, transparent); border-radius: var(--r-2); padding: 12px 14px; animation: uc-eg-in var(--d-3) var(--e-out) both; }
.uc-eg li:nth-child(1) { animation-delay: 160ms; }
.uc-eg li:nth-child(2) { animation-delay: 220ms; }
.uc-eg li:nth-child(3) { animation-delay: 280ms; }
.uc-eg li:nth-child(4) { animation-delay: 340ms; }
@keyframes uc-eg-in { from { opacity: 0; transform: translateY(12px); } }
.uc-eg li span { font-size: 21px; }

/* ══ Toast ════════════════════════════════════════════════════════════════ */
.uc-toast {
  position: absolute; left: 50%; bottom: calc(88px + env(safe-area-inset-bottom)); z-index: 45;
  transform: translate(-50%, 20px); opacity: 0; pointer-events: none;
  background: var(--ink); color: var(--cream);
  border-radius: var(--r-pill); padding: 9px 10px 9px 20px; font-size: 15.5px; font-weight: 600;
  box-shadow: var(--lift-2); max-width: 88%;
  display: flex; align-items: center; gap: 12px;
  transition: transform var(--d-3) var(--e-spring), opacity var(--d-2) var(--e-out);
}
.uc-toast.is-up { pointer-events: auto; }
.uc-toast:not(:has(button)) { padding-right: 20px; }
.uc-toast button {
  flex: 0 0 auto; border: 0; border-radius: var(--r-pill); cursor: pointer;
  background: var(--coral); color: #0B0D0A;
  font-family: var(--display); font-size: 15px; font-weight: 700;
  min-height: 36px; padding: 0 16px;
  transition: transform var(--d-2) var(--e-spring);
}
.uc-toast button:active { transform: scale(0.94); }
.uc-toast.is-up { transform: translate(-50%, 0); opacity: 1; }

/* Her bark, over whichever Rova was tapped. One word, out again on its own. */
.uc-bark {
  position: absolute; z-index: 50; transform: translate(-50%, -118%); pointer-events: none;
  background: var(--cream); color: var(--ink); border: 2px solid var(--ink);
  border-radius: var(--r-pill); padding: 4px 12px;
  font-size: 14px; font-weight: 700; white-space: nowrap;
  animation: uc-bark 900ms var(--e-out) forwards;
}
.uc-bark::after { content: ""; position: absolute; left: 50%; bottom: -7px; margin-left: -5px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 7px solid var(--ink); }
@keyframes uc-bark {
  0% { opacity: 0; transform: translate(-50%, -90%) scale(0.6); }
  22% { opacity: 1; transform: translate(-50%, -128%) scale(1.08); }
  40% { transform: translate(-50%, -118%) scale(1); }
  78% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -142%) scale(0.94); }
}

/* ══ A shape while a screen loads ══════════════════════════════════════════
   Slabs the size of what is coming, so nothing jumps when it lands. */
.uc-skel { display: flex; flex-direction: column; gap: 14px; }
.uc-skel i { display: block; border-radius: var(--r-3); background: linear-gradient(100deg, var(--ink-08) 30%, var(--ink-12) 50%, var(--ink-08) 70%); background-size: 300% 100%; animation: uc-skel 1.3s linear infinite; }
.uc-skel i.t { height: 40px; width: 78%; }
.uc-skel i.c { height: 180px; }
.uc-skel i.b { height: 72px; }
.uc-skel i.s { height: 52px; width: 64%; }
@keyframes uc-skel { to { background-position: -300% 0; } }

/* ══ Motion off ════════════════════════════════════════════════════════════
   Every animation above moves content that is already drawn and already
   interactive, so all of it can go and nothing is lost but the movement. Kept
   in ONE block so the answer to "is this screen usable with motion off" is one
   place. .no-motion is the harness's switch and says exactly what the media
   query says, so a demo can show both without a system setting. */
@media (prefers-reduced-motion: reduce) {
  .uc *, .uc *::before, .uc *::after {
    animation-duration: 1ms !important; animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
  .uc-stop.is-veiled, .uc-rise.is-veiled { opacity: 1; transform: none; }
}
.uc.no-motion *, .uc.no-motion *::before, .uc.no-motion *::after {
  animation-duration: 1ms !important; animation-iteration-count: 1 !important;
  transition-duration: 1ms !important;
}
.uc.no-motion .uc-stop.is-veiled, .uc.no-motion .uc-rise.is-veiled { opacity: 1; transform: none; }

/* ── Three screens the sandbox had no fixture for ──────────────────────────
   Every one of them is a state a real console reaches and a page of fixtures
   never does: a section still loading, a member on more than one trip, and a
   member on none. They are carried across from the screens they replace rather
   than rewritten, and re-set under .uc so they read as the same product as the
   four sections around them. */

/* A SHAPE WHILE A SECTION LOADS, and not the word "Loading". Slabs the size of
   what is coming, so nothing jumps when it lands. Decoration -- aria-hidden,
   with a live region beside it that says Loading for anybody listening rather
   than looking. It rides inside the scroller, so the bar and the tab bar are
   already there and already the right size while it runs. */
.uc-skel { display: flex; flex-direction: column; gap: 14px; padding: 4px 0 0; }
.uc-skel-card, .uc-skel-bar {
  border-radius: 18px;
  background: linear-gradient(100deg, var(--ink-08) 30%, var(--ink-12) 50%, var(--ink-08) 70%);
  background-size: 300% 100%; animation: uc-skel 1.3s linear infinite;
}
.uc-skel-card { height: 190px; }
.uc-skel-bar { height: 84px; }
.uc-skel-bar.short { height: 56px; width: 70%; }
@keyframes uc-skel { to { background-position: -300% 0; } }

/* WHICH TRIP, for a member on more than one. The whole card is the <a> and
   never a div with a link inside it, so the tap target is the card and a
   screen reader reads one link; the emoji is a picture of the trip and the
   name beside it is what the trip is called, so the emoji is aria-hidden. */
.uc-triplist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.uc-tripcard {
  display: flex; align-items: center; gap: 16px;
  padding: 18px; min-height: 84px;
  background: var(--paper); border: 1px solid var(--ink-12); border-radius: 18px;
  color: inherit; text-decoration: none;
  box-shadow: var(--lift-1);
  transition: border-color var(--d-1) var(--e-out), transform var(--d-1) var(--e-out);
}
.uc-tripcard:hover, .uc-tripcard:focus-visible { border-color: var(--coral); }
.uc-tripcard:active { transform: scale(0.985); }
.uc-tripemoji { font-size: 40px; line-height: 1; flex: none; }
.uc-tripwords { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.uc-tripwords b { font-size: 21px; font-weight: 700; letter-spacing: -0.02em; }
.uc-tripwords span { font-size: 16px; color: var(--ink-70); }

/* NOBODY BEHIND THE TOKEN: removed from the roster since it was minted, or
   never on a roster trip at all. One sentence, and the one thing the reader
   can actually do about it -- the rule every empty state on these screens
   obeys. */
.uc-notrip { font-size: 21px; line-height: 1.5; max-width: 22em; color: var(--ink); }

/* THE AGENT'S TWO SCREENS (Oct 22 2026): the client list and a trip's Flights.
   Lone screens like the two above, so they borrow the trip cards and add only
   the agency's sub-line, the way back, the watch note and the sign-out. */
.ag-sub { margin: -10px 0 0; font-size: 17px; color: var(--ink-70); }
.ag-h2 { margin: 8px 0 0; font-size: 19px; font-weight: 700; letter-spacing: -0.02em; }
.ag-back { align-self: flex-start; font-size: 15px; font-weight: 600; color: var(--ink-70); text-decoration: none; }
.ag-back:hover, .ag-back:focus-visible { color: var(--coral); }
.ag-note { margin: 0; font-size: 15px; line-height: 1.5; color: var(--ink-70); max-width: 34em; }
.ag-out { align-self: flex-start; margin-top: 8px; }

@media (prefers-reduced-motion: reduce) {
  .uc-skel-card, .uc-skel-bar { animation: none; }
  .uc-tripcard { transition: none; }
}

/* ── The morning message, carried across whole ─────────────────────────────
   THE ONE SCREEN THAT WAS NOT REBUILT. v4 sketched this room from a fixture:
   a switch, four hours and a note. The real one is the trip owner's
   daily-message editor as well as the traveller's own switch -- two scopes,
   one at a time, the unsaved draft stashed across the switch between them,
   and the {flight} refusal and the cap over the COMPOSED text behind it.
   Replacing that with the sketch would have been a regression dressed as a
   redesign, so the screen keeps its markup and its rules, and only its HOST
   changed: these were `.uc .mo-*` and are `.uc .mo-*` now, because
   the big-print layer they hung on went with the screens it was written for.

   One changes one person's copy; the other changes what everybody wakes up
   to. A paragraph between them was not enough separation, so they are on
   their own tabs and only a trip owner ever sees the second. */
.uc .mo-h { display: flex; align-items: center; gap: 10px; font-size: 26px; margin: 0; }
.uc .mo-h2 { margin: 0; font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 9px; }
.uc .mo-lede { margin: 0; font-size: 17px; }
.uc .mo-scope { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 6px;
                        background: var(--ink-08); border-radius: var(--r-pill); }
.uc .mo-tab { display: inline-flex; align-items: center; justify-content: center; gap: 8px;
                      min-height: 52px; border: 0; border-radius: var(--r-pill); background: transparent;
                      font-family: var(--display); font-size: 17px; font-weight: 700; color: var(--ink);
                      cursor: pointer; transition: background .15s ease; }
.uc .mo-tab.on { background: var(--cream); box-shadow: 0 1px 0 var(--ink-12); }
.uc .mo-card { border: 2px solid var(--ink-12); border-radius: var(--r-3); padding: 16px 18px;
                       display: flex; flex-direction: column; gap: 12px; }
/* The switch says which way it is set in words as well as in the control: a
   tick box on its own is the one thing on this screen somebody could read
   backwards. The real checkbox is still there, off screen, so the keyboard and
   the screen reader get a checkbox and not a puzzle. */
.uc .mo-switch.is-on { border-color: var(--coral); }
.uc .mo-sw { display: flex; align-items: center; gap: 14px; cursor: pointer; min-height: 56px; }
.uc .mo-sw input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.uc .mo-sw-track { flex: 0 0 auto; width: 62px; height: 34px; border-radius: var(--r-pill);
                           background: var(--ink-12); position: relative; transition: background .18s ease; }
.uc .mo-sw-knob { position: absolute; top: 3px; left: 3px; width: 28px; height: 28px; border-radius: 50%;
                          background: var(--cream); box-shadow: 0 1px 3px rgba(11, 13, 10, 0.35);
                          transition: transform .18s cubic-bezier(0.2, 0.7, 0.3, 1); }
.uc .mo-sw input:checked + .mo-sw-track { background: var(--coral); }
.uc .mo-sw input:checked + .mo-sw-track .mo-sw-knob { transform: translateX(28px); }
.uc .mo-sw input:focus-visible + .mo-sw-track { outline: 3px solid var(--coral); outline-offset: 3px; }
.uc .mo-sw-words { display: flex; flex-direction: column; gap: 2px; }
.uc .mo-sw-words b { font-size: 19px; }
.uc .mo-sw-words span { font-size: 16px; }
/* The time wheel: three snap-scrolling columns, which is how every phone asks
   for a time and the one gesture a thumb already knows. Five rows tall, the
   middle one held by a band -- the column's padding is what lets the first and
   last row reach the middle of the window, so it is (220 - 44) / 2 and changes
   with neither number alone. */
.uc .mo-timeset { display: flex; flex-direction: column; gap: 12px; }
.uc .mo-wheel { position: relative; display: grid; grid-template-columns: 1fr 1fr 1fr;
                gap: 4px; height: 220px; border: 2px solid var(--ink-12);
                border-radius: var(--r-2); background: var(--cream); padding: 0 6px;
                overflow: hidden; }
.uc .mo-wband { position: absolute; left: 6px; right: 6px; top: 88px; height: 44px;
                border-radius: var(--r-2); background: var(--peach); pointer-events: none; }
.uc .mo-wcol { overflow-y: auto; scroll-snap-type: y mandatory; padding: 88px 0;
               scrollbar-width: none; -ms-overflow-style: none;
               text-align: center; position: relative; z-index: 1; }
.uc .mo-wcol::-webkit-scrollbar { display: none; }
.uc .mo-wcol:focus-visible { outline: 3px solid var(--coral); outline-offset: -3px;
                             border-radius: var(--r-2); }
.uc .mo-wopt { height: 44px; line-height: 44px; scroll-snap-align: center; cursor: pointer;
               font-family: var(--display); font-size: 26px; font-weight: 700;
               font-variant-numeric: tabular-nums; color: var(--ink-62);
               transition: color .12s ease; }
/* --ink-62 and not --ink-50 on the rows either side: they are large numerals a
   reader is aiming at, so they are text, and tokens.css says --ink-50 fails AA
   for it. */
.uc .mo-wopt.on { color: var(--ink); }
.uc .mo-timego { min-height: 56px; justify-content: center; font-size: 18px; }
.uc .mo-timeclear { align-self: flex-start; }
@media (prefers-reduced-motion: reduce) {
  .uc .mo-wcol { scroll-behavior: auto; }
  .uc .mo-wopt { transition: none; }
}
/* The trip's own message, for the person whose trip it is. */
.uc .mo-owner { border: 2px solid var(--coral); border-radius: var(--r-3); padding: 16px 18px;
                        display: flex; flex-direction: column; gap: 12px; }
.uc .mo-warn { display: flex; align-items: center; gap: 9px; align-self: flex-start;
                       background: var(--peach); border-radius: var(--r-pill); padding: 7px 14px;
                       font-size: 16px; font-weight: 700; color: var(--ink); }
.uc .mo-label { display: block; font-size: 17px; font-weight: 600; margin: 0; }
.uc .mo-owner textarea { width: 100%; }
.uc .mo-blocks { display: grid; grid-template-columns: 1fr; gap: 8px; }
@media (min-width: 480px) { .uc .mo-blocks { grid-template-columns: 1fr 1fr; } }
.uc .mo-block { display: flex; align-items: center; gap: 10px; font-size: 17px;
                        min-height: 48px; cursor: pointer; border: 1.5px solid var(--ink-12);
                        border-radius: var(--r-2); padding: 6px 12px; }
.uc .mo-block:has(input:checked) { border-color: var(--coral); background: var(--cream-2); }
.uc .mo-block input { width: 26px; height: 26px; min-height: 26px; flex: 0 0 auto; }
.uc .mo-preview { border-left: 4px solid var(--coral); padding: 4px 0 4px 14px; }
.uc .mo-preview b { display: block; font-size: 16px; }
.uc .mo-preview p { margin: 4px 0 0; font-size: 18px; }
.uc .mo-next, .uc .mo-hint, .uc .mo-note { margin: 0; font-size: 16px; }
.uc .mo-note { display: flex; align-items: flex-start; gap: 8px; }

/* Learn the local language (063): the first section of Rova's screen. Built on
   the mo-* cards above so it reads as the same screen, and on tokens only so
   the dark theme inverts it with everything else. */
.uc .lp { margin-bottom: 28px; }
.uc .lp-count, .uc .lp-sit { font-size: 14px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-62); }
.uc .lp-en { font-family: var(--display); font-size: 21px; font-weight: 700; line-height: 1.3; }
.uc .lp-answer { display: flex; flex-direction: column; gap: 6px; border-top: 2px solid var(--ink-08); padding-top: 10px; }
.uc .lp-tr { font-family: var(--display); font-size: 23px; font-weight: 800; line-height: 1.3; color: var(--ink); }
.uc .lp-say { font-size: 16px; color: var(--ink-62); }
.uc .lp-tip { font-size: 16px; }
.uc .lp-play { align-self: flex-start; }
.uc .lp-acts { display: flex; gap: 10px; flex-wrap: wrap; }
.uc .lp-acts .btn { min-height: 52px; font-size: 18px; justify-content: center; flex: 1 1 140px; }
.uc .lp-stop { align-self: flex-start; }
.uc .lp-field { min-height: 72px; }
.uc .lp-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.uc .lp-item { display: flex; flex-direction: column; gap: 6px; padding-bottom: 14px; border-bottom: 2px solid var(--ink-08); }
.uc .lp-item:last-child { border-bottom: 0; padding-bottom: 0; }
.uc .lp-remove { align-self: flex-start; }
.uc .lp-seeded summary { cursor: pointer; list-style: none; }
.uc .lp-seeded summary::-webkit-details-marker { display: none; }

/* A card for this trip (card-fit, Sept 16 2026): the same mo-* cards, one row
   of answers each. The picked answer is said by aria-pressed and drawn on
   tokens only, so the dark theme inverts it with everything else. */
.uc .cf { margin-bottom: 28px; }
.uc .cf-known { display: flex; flex-direction: column; gap: 10px; margin: 0; }
.uc .cf-known-row { display: flex; flex-direction: column; gap: 2px; }
.uc .cf-known-row dd { margin: 0; }
.uc .cf-known .uc-chips { margin-top: 4px; }
.uc .cf-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.uc .cf-opt { min-height: 48px; font-size: 16px; padding: 10px 16px; white-space: normal; }
.uc .cf-opt.is-on { background: var(--peach); border-color: var(--coral); }
.uc .cf-opt.is-on:hover { background: var(--peach); border-color: var(--coral); }
.uc .cf-verdict { border-color: var(--coral); }

/* ── Rova's console, full screen, and the bark on the way in ──────────────
   Not rebuilt either: the sandbox drew its own, and this one already has the
   typewriter, the examples, the focus return and the rows that pin all three.
   Her bark goes over whichever Rova was tapped: one word, taken back out on its
   own after nine hundred milliseconds; nothing waits on it. */
.rbark { position: fixed; z-index: 60; transform: translate(-50%, -118%); pointer-events: none;
         background: var(--cream); color: var(--ink); border: 2px solid var(--ink);
         border-radius: var(--r-pill); padding: 5px 13px; font-family: var(--display);
         font-size: 15px; font-weight: 700; white-space: nowrap;
         animation: rbark-pop 900ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards; }
.rbark::after { content: ""; position: absolute; left: 50%; bottom: -7px; margin-left: -5px;
                width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent;
                border-top: 7px solid var(--ink); }
@keyframes rbark-pop {
  0%   { opacity: 0; transform: translate(-50%, -90%) scale(0.6); }
  22%  { opacity: 1; transform: translate(-50%, -128%) scale(1.08); }
  40%  { transform: translate(-50%, -118%) scale(1); }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -140%) scale(0.94); }
}

/* ── Rova's console, full screen ───────────────────────────────────────────
   It was a card at the foot of the screen. A traveller tapping Rova is not
   glancing at a footnote, they are going to her, so she takes the whole screen
   and the shell behind it stops scrolling. It still takes nothing: there is no
   input, because a box that looks like it sends and does not is worse than no
   box, and no route behind it exists yet.

   --ox/--oy are the tapped Rova's centre, written on the element by
   openRovaChat. The console grows out of her; a browser with no clip-path
   support simply gets the fade, which is why the opacity is on the same rule. */
body.chatting { overflow: hidden; }
.rchat { position: fixed; inset: 0; z-index: 50; display: flex; align-items: stretch;
         justify-content: center; background: var(--ink);
         --ox: 50vw; --oy: 50vh;
         /* Derived, never literal: the console has a DARK THEME (CLAUDE.md). Under the dark
            theme --ink is the LIGHT value and this panel inverts with it. */
         --on-ink-soft: color-mix(in srgb, var(--cream) 82%, transparent);
         --on-ink-mute: color-mix(in srgb, var(--cream) 64%, transparent);
         --on-ink-line: color-mix(in srgb, var(--cream) 17%, transparent);
         --on-ink-fill: color-mix(in srgb, var(--cream) 8%, transparent); }
.rchat-box { width: 100%; max-width: 720px; background: var(--ink); color: var(--cream);
             display: flex; flex-direction: column; overflow: hidden;
             animation: rchat-in 340ms cubic-bezier(0.2, 0.7, 0.3, 1); }
@keyframes rchat-in {
  from { opacity: 0.2; clip-path: circle(3% at var(--ox) var(--oy)); }
  to   { opacity: 1;   clip-path: circle(150% at var(--ox) var(--oy)); }
}
@media (prefers-reduced-motion: reduce) { .rchat-box { animation: none; } }
.rchat-top { display: flex; align-items: center; gap: 9px; padding: 14px 16px;
             border-bottom: 1px solid var(--on-ink-line); font-size: 17px; flex: 0 0 auto; }
.rchat-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--coral);
             animation: rchat-pulse 2.4s ease-in-out infinite; }
@keyframes rchat-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.rchat-sub { font-family: var(--mono); font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase;
             color: var(--on-ink-mute); }
.rchat-x { margin-left: auto; border: 0; background: transparent; color: inherit;
           font-size: 30px; line-height: 1; cursor: pointer; min-height: 48px; min-width: 48px; }
.rchat-x:focus-visible { outline: 2px solid var(--coral); outline-offset: -4px; }
/* `safe center` and not `center`: on a tall screen the column sits in the
   middle, and on a short one it falls back to the top rather than overflowing
   above the scroll origin where nothing can reach it. A browser that does not
   know the keyword drops the declaration and gets the top, which is the same
   answer. */
.rchat-body { flex: 1 1 auto; overflow-y: auto; padding: 28px 20px calc(28px + env(safe-area-inset-bottom));
              display: flex; flex-direction: column; align-items: center; gap: 14px;
              justify-content: safe center; }
.rchat-body > * { flex: 0 0 auto; }
.rchat-rova { line-height: 0; }
.rchat-rova img { image-rendering: crisp-edges; image-rendering: pixelated; }
.rchat .term.rchat-term { margin-top: 0; color: var(--on-ink-soft); max-width: 100%; }
.rchat .term.rchat-term .term-name { color: var(--cream); }
.rchat .term.rchat-term .cur { background: var(--cream); }
.rchat-say { margin: 0; text-align: center; font-size: 17px; line-height: 1.5;
             color: var(--on-ink-soft); max-width: 38ch; }
.rchat-soon { margin: 12px 0 0; font-family: var(--mono); font-size: 13px; letter-spacing: 0.14em;
              text-transform: uppercase; color: var(--on-ink-mute); }
/* Examples of what it will answer. A list, never a row of buttons: something
   shaped like a control that does nothing is the same defect as the box. */
.rchat-eg { list-style: none; margin: 0; padding: 0; width: 100%; max-width: 34em;
            display: flex; flex-direction: column; gap: 9px; }
.rchat-eg li { display: flex; align-items: center; gap: 12px; font-size: 17px;
               color: var(--cream); background: var(--on-ink-fill);
               border: 1px solid var(--on-ink-line); border-radius: var(--r-2); padding: 12px 14px; }
.rchat-eg li span[aria-hidden] { font-size: 21px; line-height: 1; }
.rchat-meanwhile { margin: 4px 0 0; text-align: center; font-size: 16px;
                   color: var(--on-ink-mute); max-width: 34em; }

/* ── The thread, once chat is live for a member (Sept 9 2026) ────────────────
   Every colour here is DERIVED from --cream through color-mix, using the four
   custom properties .rchat already declares. A cream alpha written as a
   literal is pale-on-pale under this theme, because --ink is the LIGHT value
   in it and this panel inverts with it (CLAUDE.md).

   The layout is one line of real change: .rchat-box is already a column flex
   and .rchat-body already scrolls, so the composer is a flex: 0 0 auto sibling
   and nothing else moves. */

/* Once there are messages, the body stops centring its opening state and
   stacks from the top -- otherwise the first exchange sits in the middle of
   the screen and walks downward as it grows. */
.rchat-body:has(.rchat-msg) { justify-content: flex-start; align-items: stretch; }
/* The composer owns the safe-area inset now; leaving it on the body put the
   padding above the keyboard tray instead of under the send button. */
.rchat-body:has(~ .rchat-foot) { padding-bottom: 20px; }

.rchat-msg { max-width: 44ch; font-size: 17px; line-height: 1.5; padding: 11px 14px;
             border-radius: var(--r-2); white-space: pre-wrap; overflow-wrap: anywhere; }
.rchat-msg--you  { align-self: flex-end; background: var(--coral); color: var(--ink);
                   border-bottom-right-radius: 5px; }
.rchat-msg--rova { align-self: flex-start; background: var(--on-ink-fill); color: var(--cream);
                   border: 1px solid var(--on-ink-line); border-bottom-left-radius: 5px; }
.rchat-note { align-self: center; margin: 2px 0; text-align: center; max-width: 36em;
              font-size: 15px; line-height: 1.45; color: var(--on-ink-mute); }

/* Three dots while a turn is in flight. Research can hold a turn for a long
   time, so this has to keep moving; with motion off it holds still and the
   aria-label is what says it is working. */
.rchat-dots { display: flex; align-items: center; gap: 5px; padding: 15px 14px; }
.rchat-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--on-ink-soft);
                animation: rchat-blink 1.25s infinite ease-in-out both; }
.rchat-dots i:nth-child(2) { animation-delay: 0.16s; }
.rchat-dots i:nth-child(3) { animation-delay: 0.32s; }
@keyframes rchat-blink { 0%, 72%, 100% { opacity: 0.28; } 36% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .rchat-dots i { animation: none; opacity: 0.6; } }

/* The examples, as controls. They are buttons here because here they work. */
.rchat-eg--live li { padding: 0; background: none; border: 0; }
.rchat-chip { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
              font: inherit; font-size: 17px; color: var(--cream); background: var(--on-ink-fill);
              border: 1px solid var(--on-ink-line); border-radius: var(--r-2); padding: 12px 14px;
              cursor: pointer; }
.rchat-chip span[aria-hidden] { font-size: 21px; line-height: 1; }
.rchat-chip:hover { background: color-mix(in srgb, var(--cream) 14%, transparent); }
.rchat-chip:focus-visible { outline: 2px solid var(--coral); outline-offset: 2px; }

.rchat-foot { flex: 0 0 auto; border-top: 1px solid var(--on-ink-line);
              padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); background: var(--ink); }
.rchat-form { display: flex; align-items: flex-end; gap: 10px; max-width: 720px; margin: 0 auto; }
.rchat-in { flex: 1 1 auto; resize: none; font: inherit; font-size: 17px; line-height: 1.4;
            color: var(--cream); background: var(--on-ink-fill);
            border: 1px solid var(--on-ink-line); border-radius: var(--r-2);
            padding: 11px 13px; max-height: 132px; overflow-y: auto; }
.rchat-in::placeholder { color: var(--on-ink-mute); }
.rchat-in:focus-visible { outline: 2px solid var(--coral); outline-offset: -1px; }
/* Its OWN class and never .btn: body.member .btn would shadow an accent class
   here and the send button would come out of the member shell's palette. */
.rchat-send { flex: 0 0 auto; width: 42px; height: 42px; border: 0; border-radius: 50%;
              background: var(--coral); color: var(--ink); font-size: 19px; line-height: 1;
              cursor: pointer; display: flex; align-items: center; justify-content: center; }
.rchat-send:disabled { opacity: 0.42; cursor: default; }
.rchat-send:focus-visible { outline: 2px solid var(--cream); outline-offset: 2px; }

/* A transfer arrives as a say carrying the number. A browser cannot dial, but
   the handset reading this console can, so it becomes something to tap. */
.rchat-tel { display: inline-block; margin-top: 9px; font-weight: 600; color: var(--coral);
             text-decoration: none; border-bottom: 1px solid currentColor; }
.rchat-tel:focus-visible { outline: 2px solid var(--coral); outline-offset: 3px; }
/* A goodbye in a console is not a hang-up: the thread stops taking input and
   offers to start again rather than tearing the socket down under them. */
.rchat-again { align-self: center; margin-top: 6px; font: inherit; font-size: 16px;
               color: var(--cream); background: var(--on-ink-fill);
               border: 1px solid var(--on-ink-line); border-radius: var(--r-2);
               padding: 10px 18px; cursor: pointer; }
.rchat-again:hover { background: color-mix(in srgb, var(--cream) 14%, transparent); }
.rchat-again:focus-visible { outline: 2px solid var(--coral); outline-offset: 2px; }

/* Her console and her bark, with motion off. Stated here rather than beside
   each rule so the answer to "is this screen still usable with motion off" is
   in one place -- the same reason the block it came from gave. */
@media (prefers-reduced-motion: reduce) {
  .uc .mo-sw-track, .uc .mo-sw-knob, .uc .mo-tab { transition: none; }
  .rchat-dot { animation: none; }
  .rbark { animation: none; }
}



/* ── What the live console needs that a page of fixtures did not ───────────
   Six shapes the sandbox never had to draw: the two screens that are not a
   room, the morning screen's own column, the retry a refused save offers, the
   attachment and its upload bar, and the sentinel the wall pages on. */

/* The trip list and the no-trip screen. Neither has a bar, a tab or a section
   to keep, so the shell is not mounted for them at all -- but they are still
   this console and still read as it. */
.uc.is-lone { overflow: auto; }
.uc-lonewrap { padding: 28px 20px 40px; display: flex; flex-direction: column; gap: 20px;
               max-width: 620px; margin: 0 auto; width: 100%; }
.uc-lone { display: flex; flex-direction: column; gap: 18px; }

/* Rova's room, which is viewMyMorning inside the shell's page. The column and
   its gap were `body.member .mo-page` and are stated here because the layer
   that carried them went with the screens it was written for. */
.uc .mo-page { display: flex; flex-direction: column; gap: 20px; }
.uc-page-settings { gap: 20px; }

/* A retry, offered ONLY for a refusal a second attempt could clear -- never for
   an empty field, where the fix is to type and a second attempt would fail
   identically. Hidden by the attribute, so it is out of the tab order too. */
.uc-retry { align-self: flex-start; border: 1px solid var(--ink-12); background: transparent;
            color: var(--ink-70); border-radius: 999px; padding: 5px 12px;
            font-size: 13px; font-weight: 600; cursor: pointer; }
.uc-retry:hover { border-color: var(--coral); color: var(--ink); }

/* How far the upload has got. The bar is the browser's own progress on a
   direct PUT, so it moves because bytes moved. The preview thumbnail itself
   is .uc-attachpreview above (journal entry redesign, Sept 9 2026). */
.uc-progress { height: 6px; border-radius: 999px; background: var(--ink-08); overflow: hidden; }
.uc-progress-fill { height: 100%; width: 0; background: var(--coral);
                    transition: width var(--d-2) var(--e-out); }

/* The sentinel the journal pages on. Given a height on purpose: a zero-height
   box below the fold is a box some browsers never report as intersecting. It
   gets NO margin -- the reach ahead is the observer's rootMargin, which is
   measured against the scroller and stays right whatever the viewport is,
   where a margin in vh would push the sentinel further from the last entry on
   a tall screen than on a short one and page LATER exactly where there is more
   room to page early. */
.uc-wallmore { height: 1px; }

/* Where a forecast lands. It is written by paintMyWeather rather than by the
   stop it hangs under, so the stop's own markup leaves it an empty box. */
.uc-wxslot:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
  .uc-progress-fill { transition: none; }
}

/* ── Standing in for a member (Sept 7 2026) ─────────────────────────────────
   docs/2026-09-07-standing-in-for-a-member.md.

   The bar sits above the member topbar and is drawn in renderMember's markup,
   never by this file: styling that goes missing must not take a warning with
   it. What is here is only loudness. Crimson rather than coral, because coral
   is the console's ordinary accent and this has to read as "you are not
   yourself right now" at a glance across a desk.

   It is sticky for the same reason it is not dismissible: somebody scrolled
   into a traveller's journal, typing, is exactly the person who most needs to
   be reminded whose name the words will carry. */
.sib {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 16px;
  background: var(--crimson); color: var(--cream);
  font-size: 13.5px; line-height: 1.45;
}
.sib-say { flex: 1 1 22ch; }
.sib-say b { font-weight: 700; }
.sib-dot {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%;
  background: var(--cream); opacity: .9;
}
.sib .btn {
  flex: 0 0 auto;
  background: transparent; color: var(--cream);
  border: 1.5px solid var(--cream); border-radius: 9px;
  padding: 5px 12px; font-size: 12.5px; cursor: pointer;
}
.sib .btn:hover { background: var(--cream); color: var(--crimson); }

/* The whole shell carries the mark, so a screenshot of any part of a stand-in
   session shows it was one. body.standin is set beside body.member in
   loadMe(); losing it costs the frame and never the bar. */
body.standin .app { box-shadow: inset 0 0 0 3px var(--crimson); }

@media (max-width: 480px) {
  .sib { padding: 8px 12px; font-size: 13px; }
}

/* ── The flight card (Sept 9 2026, drawn Sept 11) ──────────────────────────
   Sized as a sibling of .uc-wx and deliberately quieter than it. A weather
   bulletin stops shouting (owner, Sept 8 2026) and a DELAYED flight has no more
   right to shout than a wind warning: it keeps the quiet card and one coloured
   rule down its left edge.

   NARROWED, NOT REVERSED, Sept 11 2026 (owner). Two states now flip the whole
   card: cancelled and diverted. Those are the states where the day as planned
   is over, and reserving the loud treatment for them is what keeps it meaning
   something when it arrives. A delay still never gets it.

   THEME. Every colour below is a token or is mixed off one, because --ink is
   ALREADY the pale value in the console's dark theme and a literal cream would
   be pale-on-pale there. The three deliberate exceptions declare their own
   local variable and say why: the sky, the plane on it and the money chip are
   their own fixed surfaces in both themes, so their ink is fixed too. */
.uc-flight {
  position: relative;
  margin-top: 12px; padding: 14px 16px 12px; border-radius: var(--r-3);
  border: 2px solid var(--ink-12); border-left-width: 6px; border-left-color: var(--ink-12);
  background: var(--cream-2); color: var(--ink);
  animation: uc-wx-in var(--d-3) var(--e-out) both;
  display: flex; flex-direction: column; gap: 10px;
}
.uc-flight + .uc-flight { margin-top: 8px; }
.uc-flight.is-delayed   { border-left-color: var(--coral); }
.uc-flight.is-landed,
.uc-flight.is-ontime    { border-left-color: color-mix(in srgb, var(--ink) 30%, transparent); }

/* The flip. --flip-ink is a LITERAL on purpose: the crimson ground is the same
   colour in both themes, so the text on it must be too. Everything inside the
   card reads `currentColor`, which is how one ladder and one money line work on
   both the quiet card and this one. */
.uc-flight.is-flip {
  --flip-ink: #FFFAF0;
  color: var(--flip-ink);
  border-color: var(--crimson-2); border-left-color: var(--crimson-2);
  background: var(--crimson);
}
.uc-flight.is-cancelled.is-flip {
  background: linear-gradient(135deg, var(--crimson) 0%, var(--crimson) 38%, var(--coral) 100%);
}
.uc-flight.is-flip .uc-flight-air,
.uc-flight.is-flip .uc-flight-statlabel,
.uc-flight.is-flip .uc-flight-compsub,
.uc-flight.is-flip .uc-flight-codes,
.uc-flight.is-flip .uc-flight-appr,
.uc-flight.is-flip .uc-flight-updated,
.uc-flight.is-flip .uc-flight-help > summary,
.uc-flight.is-flip .uc-flight-help p,
.uc-flight.is-flip .uc-wx-meta { color: color-mix(in srgb, var(--flip-ink) 76%, transparent); }
.uc-flight.is-flip .uc-flight-appr,
.uc-flight.is-flip .uc-flight-updated { border-top-color: color-mix(in srgb, var(--flip-ink) 24%, transparent); }
.uc-flight.is-flip .uc-flight-banner { background: color-mix(in srgb, var(--flip-ink) 16%, transparent); }

.uc-flight-defs { position: absolute; width: 0; height: 0; }

.uc-flight-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.uc-flight-ident { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.uc-flight-no {
  font-size: 26px; font-weight: 700; line-height: 1;
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
}
.uc-flight-air { font-size: 14px; font-weight: 600; color: var(--ink-70); }

/* The pill. One reading of the state, and it is the server's. */
.uc-flight-pill {
  font-size: 12px; font-weight: 800; padding: 5px 12px; border-radius: var(--r-pill);
  white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;
}
.uc-flight-pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.uc-flight-pill.is-ok    { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink-70); }
.uc-flight-pill.is-soon  { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink-70); }
.uc-flight-pill.is-watch { background: color-mix(in srgb, var(--coral) 22%, transparent); color: var(--coral-2); }
.uc-flight-pill.is-warn  { background: var(--flip-ink, var(--cream)); color: var(--crimson); }

/* ── The scene ─────────────────────────────────────────────────────────────
   Sky, drifting cloud, one plane. NO LANDMARKS by owner's call: this card draws
   for every trip and every leg, and mountains would be it claiming to know a
   place it was never told about.

   --sky-ink is a literal for the same reason --flip-ink is: the sky gradients
   below are fixed in both themes, so the silhouette on them is fixed too.
   var(--ink) here would go pale in the dark theme and vanish into a bright
   sky. */
.uc-flight-scene {
  --sky-ink: #0B0D0AE0;
  position: relative; height: 84px; border-radius: var(--r-2); overflow: hidden;
}
.uc-flight-sky { position: absolute; inset: 0; }
.mood-dawn .uc-flight-sky { background: linear-gradient(165deg, #FFD9A8 0%, #FFF3DE 55%, var(--cream-2) 100%); }
.mood-day  .uc-flight-sky { background: linear-gradient(165deg, #FFC08A 0%, #FFE3C2 55%, var(--cream-2) 100%); }
.mood-warm .uc-flight-sky { background: linear-gradient(165deg, var(--coral) 0%, #FFC08A 60%, var(--cream-2) 100%); }
.mood-hot  .uc-flight-sky { background: linear-gradient(165deg, var(--crimson-2) 0%, var(--coral) 65%, #FFE3C2 100%); }
.mood-dusk .uc-flight-sky { background: linear-gradient(165deg, #E8935B 0%, #FFD9A8 55%, var(--cream-2) 100%); }

.uc-flight-clouds {
  position: absolute; top: 8%; left: 0; height: 46%; width: 200%;
  display: flex; opacity: 0.8; animation: uc-flight-drift 34s linear infinite;
}
.uc-flight-cloudset { width: 50%; height: 100%; flex-shrink: 0; fill: #FFFAF0; opacity: 0.85; }
@keyframes uc-flight-drift { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* The horizon, drawn only on a leg whose record named an airport there is a
   drawing for (app.js' UC_FLIGHT_LAND). Sized by HEIGHT and anchored to its own
   corner of the baseline: the box is deliberately wider than the art needs so
   `meet` fits to the height and the drawing keeps its proportions at every card
   width. A ridge stretched sideways is a wider range, but a stretched tower is
   a different building.

   Both tones are FIXED and neither is a theme token, the same call the plane
   above already carries and for the same reason: var(--ink) and var(--cream)
   both flip in the dark theme, the five sky gradients do not, and a silhouette
   that inverted would come back pale against a bright sky. The snow caps and
   the tower's bands are the cream the clouds are already painted in. */
.uc-flight-land { position: absolute; pointer-events: none; fill: var(--sky-ink); }
.uc-flight-land.mtn { left: 0; bottom: -4%; width: 70%; height: 64%; }
.uc-flight-land.twr { right: 4%; bottom: -2%; width: 36%; height: 92%; }
.uc-flight-land .snow { fill: #FFFAF0; opacity: 0.92; }
.uc-flight-land .band { fill: #FFFAF0; opacity: 0.55; }

.uc-flight-plane { position: absolute; width: 22px; height: 22px; color: var(--sky-ink); }
.uc-flight-plane.depart   { left: 24%; bottom: 30%; transform: rotate(-32deg); animation: uc-flight-bob 2.8s ease-in-out infinite; }
.uc-flight-plane.arrive   { right: 24%; bottom: 34%; transform: rotate(24deg); animation: uc-flight-bob 2.8s ease-in-out infinite; }
/* Centred on a margin and not on translateX(-50%): the westbound scene below
   mirrors this element, and a centring translate inside the same matrix gets
   mirrored with it and lands the plane half a width off. Half of the 22px
   width, which is the one number these two rules share. */
.uc-flight-plane.mid      { left: 50%; margin-left: -11px; top: 26%; transform: rotate(-6deg); animation: uc-flight-bob 2.8s ease-in-out infinite; }
.uc-flight-plane.jittery  { animation: uc-flight-jitter 0.8s ease-in-out infinite; }
/* CANCELLED IS PARKED, NOT FALLING (owner, Sept 12 2026: "look it's crashing,
   maybe keep on ground"). The Tabler glyph's nose points right, so the old
   rotate(88deg) stood it nose-down over the horizon -- read as a plane going
   in, on the one card already carrying the worst news of the trip. Level, low
   in the frame and dimmed is a plane that is not going anywhere, which is what
   a cancellation actually is. */
.uc-flight-plane.grounded { left: 44%; bottom: 8%; opacity: 0.5; transform: rotate(0deg); }
.uc-flight-plane.astray   { left: 56%; bottom: 42%; animation: uc-flight-wobble 2.4s ease-in-out infinite; }

/* ── WESTBOUND: THE SAME SCENE, MIRRORED ───────────────────────────────────
   The silhouettes never move. Vancouver is west and draws on the left, Italy is
   east and draws on the right, on every card, because a map whose ends swap is
   not a map -- so the plane is the only thing that says which way this leg
   runs, and on the way home it runs the other way (owner, Sept 12 2026).

   Each rule is the eastbound pose mirrored, which is why scaleX(-1) comes
   FIRST: a transform list applies right to left, so the pose is built as it
   always was and the mirror is taken of the finished thing. The left/right
   offsets are mirrored the same way, by hand, since `left` is not part of the
   transform.

   `astray` is deliberately absent. Its wobble animates `transform` itself, so a
   base transform here would be thrown away the moment the animation starts --
   and a diverted flight is the one card where "which way is it going" is the
   question rather than the answer. */
.uc-flight-scene.is-west .uc-flight-plane.depart   { left: auto; right: 24%; transform: scaleX(-1) rotate(-32deg); }
.uc-flight-scene.is-west .uc-flight-plane.arrive   { right: auto; left: 24%; transform: scaleX(-1) rotate(24deg); }
.uc-flight-scene.is-west .uc-flight-plane.mid      { transform: scaleX(-1) rotate(-6deg); }
.uc-flight-scene.is-west .uc-flight-plane.grounded { transform: scaleX(-1); }
@keyframes uc-flight-bob    { 0%, 100% { margin-top: 0; } 50% { margin-top: -6px; } }
@keyframes uc-flight-jitter { 0%, 100% { margin-top: 0; margin-left: 0; } 50% { margin-top: 1px; margin-left: -2px; } }
@keyframes uc-flight-wobble { 0%, 100% { transform: rotate(8deg) translateX(0); } 50% { transform: rotate(-14deg) translateX(9px); } }

/* The ping, drawn only while a flight is actually in the air. */
.uc-flight-ping {
  position: absolute; left: 50%; top: 26%; width: 18px; height: 18px; margin: -9px 0 0 -9px;
  border-radius: 50%; border: 2px solid var(--sky-ink); opacity: 0;
  animation: uc-flight-ping 2.6s ease-out infinite;
}
.uc-flight-ping.p2 { animation-delay: 1.1s; }
@keyframes uc-flight-ping { 0% { transform: scale(0.4); opacity: 0.5; } 100% { transform: scale(3); opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .uc-flight, .uc-flight-clouds, .uc-flight-plane, .uc-flight-ping, .uc-flight-gap { animation: none !important; }
  .uc-flight-ping { opacity: 0; }
}

/* ── The numbers this leg is about ────────────────────────────────────────── */
.uc-flight-hero { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; }
.uc-flight-stat { display: flex; flex-direction: column; gap: 2px; }
/* --ink-62 and never fainter, anywhere in this card that is TEXT. The alpha
   ladder fails AA below it and these are the screens built for older eyes
   (admin-tiers-check pins it). The design's 50%% labels are 62%% here. */
.uc-flight-statlabel { font-size: 12.5px; font-weight: 700; color: var(--ink-62); }
.uc-flight-statval {
  font-weight: 800; font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em; line-height: 1; font-size: clamp(24px, 6vw, 32px);
}
.uc-flight-stat.is-big .uc-flight-statval  { font-size: clamp(28px, 7vw, 38px); }
.uc-flight-stat.is-huge .uc-flight-statval { font-size: clamp(34px, 9vw, 46px); }
.uc-flight-stat.is-was .uc-flight-statval {
  font-size: 19px; font-weight: 700; color: var(--ink-62);
  text-decoration: line-through; text-decoration-thickness: 2px;
}
.uc-flight-arrow { align-self: center; font-size: 20px; color: var(--ink-62); }
.uc-flight.is-flip .uc-flight-stat.is-was .uc-flight-statval,
.uc-flight.is-flip .uc-flight-arrow { color: color-mix(in srgb, var(--flip-ink) 50%, transparent); }

.uc-flight-say { font-size: 17.5px; font-weight: 400; }
.uc-flight-banner {
  font-size: 13.5px; font-weight: 700; line-height: 1.4; border-radius: var(--r-2);
  padding: 9px 12px; background: color-mix(in srgb, var(--coral) 18%, transparent);
}

/* ── The gap between two legs ──────────────────────────────────────────────
   Drawn BETWEEN the two cards it joins. It belongs to neither flight. */
.uc-flight-gap {
  margin: 8px auto; padding: 8px 16px; width: fit-content; border-radius: var(--r-2);
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border: 2px dashed var(--ink-12);
}
.uc-flight-gap .eyebrow {
  font-size: 10.5px; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-62);
}
.uc-flight-gap .dur { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; }
.uc-flight-gap .where { font-size: 13px; font-weight: 600; color: var(--ink-70); }
.uc-flight-gap.is-tight  { background: color-mix(in srgb, var(--coral) 20%, transparent); border-color: var(--coral); }
.uc-flight-gap.is-missed { background: color-mix(in srgb, var(--crimson) 14%, transparent); border-color: var(--crimson); }
.uc-flight-gap.is-tight .eyebrow,
.uc-flight-gap.is-missed .eyebrow { color: var(--ink-70); }

/* ── The money ─────────────────────────────────────────────────────────────
   Everything here was priced on the server. The chip's own ink is a literal
   pair for the reason the sky's is: it is a fixed dark surface in both themes,
   and an orange on it that flipped with the palette would be unreadable on one
   of them. */
.uc-flight-comp { display: flex; flex-direction: column; gap: 6px; }
.uc-flight-compsub { font-size: 12.5px; font-weight: 700; color: var(--ink-62); }
.uc-flight-money {
  align-self: flex-start; display: inline-flex; align-items: baseline; gap: 5px;
  background: #0B0D0A; border-radius: var(--r-2); padding: 6px 15px 7px;
  font-family: var(--mono); font-weight: 800; font-variant-numeric: tabular-nums;
}
.uc-flight-money .amt { font-size: clamp(28px, 7vw, 40px); color: #FF9152; letter-spacing: -0.01em; }
.uc-flight-money .ccy { font-size: 12px; font-weight: 700; color: #FF915299; align-self: flex-end; margin-bottom: 3px; }

/* The ladder reads currentColor throughout, which is the whole reason one set
   of rules draws it on the quiet delayed card AND on the flipped cancelled
   one without either knowing which it is in. */
.uc-flight-tiers { display: flex; flex-direction: column; gap: 6px; margin-top: 2px; }
.uc-flight-tier {
  display: grid; grid-template-columns: 52px 1fr 52px; align-items: center; gap: 10px;
  font-size: 11.5px; color: color-mix(in srgb, currentColor 62%, transparent);
}
.uc-flight-tier .lab { font-weight: 700; }
.uc-flight-tier .track {
  height: 8px; border-radius: var(--r-pill); overflow: hidden;
  background: color-mix(in srgb, currentColor 16%, transparent);
}
.uc-flight-tier .fill { display: block; height: 100%; border-radius: var(--r-pill); background: color-mix(in srgb, currentColor 40%, transparent); }
.uc-flight-tier .amt { font-family: var(--mono); font-weight: 800; text-align: right; }
.uc-flight-tier.is-on { color: currentColor; }
.uc-flight-tier.is-on .lab, .uc-flight-tier.is-on .amt { color: var(--coral-2); }
.uc-flight.is-flip .uc-flight-tier.is-on .lab,
.uc-flight.is-flip .uc-flight-tier.is-on .amt { color: #FF9152; }
.uc-flight-tier.is-on .fill { background: currentColor; }

/* The entitlement sentence. Plain text at reading size, not a callout: it is
   one thing that may be true, and dressing it as a banner would make it read as
   money already owed. */
.uc-flight-appr {
  padding-top: 10px; border-top: 1px solid var(--ink-12);
  font-size: 14px; line-height: 1.5; color: var(--ink-70);
}
/* The page the SMS watcher already sends about this same flight. */
.uc-flight-link {
  align-self: flex-start; font-size: 13.5px; font-weight: 700;
  text-decoration: underline; text-underline-offset: 3px;
}
.uc-flight-foot {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  flex-wrap: wrap; font-size: 12.5px;
}
.uc-flight-codes {
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.06em; color: var(--ink-62);
}
.uc-flight-updated {
  padding-top: 8px; border-top: 1px solid var(--ink-08);
  font-size: 12px; font-weight: 600; color: var(--ink-62);
  display: flex; align-items: center; justify-content: space-between; gap: 6px; flex-wrap: wrap;
}
.uc-flight-updated .left { display: flex; align-items: center; gap: 6px; }
.uc-flight-updated .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
/* A card nothing is watching. The dot is hollow rather than lit, because a
   filled dot is what every live strip on this screen uses for "there is a
   reading behind this" and the one state with no reading must not borrow it. */
.uc-flight-updated.is-off .dot { background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }

/* Closed by default, and it costs one line of the card when it is. */
.uc-flight-help > summary {
  cursor: pointer; font-size: 13.5px; font-weight: 700; color: var(--ink-70);
  list-style: none;
}
.uc-flight-help > summary::-webkit-details-marker { display: none; }
.uc-flight-help > summary::before { content: "\203A  "; display: inline-block; transition: transform var(--d-2) var(--e-out); }
.uc-flight-help[open] > summary::before { transform: rotate(90deg); }
.uc-flight-help p { margin-top: 8px; font-size: 14px; line-height: 1.55; color: var(--ink-70); }

/* ── The clock leads (owner, Sept 12 2026: "TIME IS IMPORTANT") ─────────────
   A booked tour's start, a train's departure and its arrival, set at the
   size of the place beside it, in coral and tabular figures so a column of
   them lines up. Colours come from tokens, so the dark theme inverts them. */
.uc-clock { display: inline-block; margin-right: 6px; font-variant-numeric: tabular-nums;
            font-weight: 800; color: var(--coral-2, var(--coral)); letter-spacing: -.01em; }
.uc-stop-say dd .uc-clock { font-size: 1.12em; }

/* A document as the last words of a row's sentence, not a card under it. */
.uc-doclink { display: inline-flex; align-items: center; gap: 4px; margin-left: 2px; white-space: nowrap;
              font-weight: 600; color: var(--ink); text-decoration: underline;
              text-decoration-color: var(--coral); text-underline-offset: 3px; }
.uc-doclink:hover, .uc-doclink:focus-visible { color: var(--coral-2, var(--coral)); }

/* ── Engine (/engine, Sept 16 2026) ───────────────────────────
   Run a recommendation by hand and read how it was made. src/admin/engine.js,
   docs/2026-09-16-the-engine-shows-its-work.md. Tokens only: no literal colour
   below, so the dark theme needs nothing of its own. */
.eng { min-width: 0; max-width: 100%; }
.eng > * { min-width: 0; max-width: 100%; }
.eng .panel .ph { flex-wrap: wrap; }
.eng .panel .ph h2 { margin: 0; }
.eng .f { grid-template-columns: 110px minmax(0, 1fr); }
.eng .f > * { min-width: 0; }
.eng select, .eng input:not([type="checkbox"]) { max-width: 100%; min-width: 0; text-overflow: ellipsis; }
.eng-explain > summary { cursor: pointer; list-style: none; padding-bottom: 14px; }
.eng-explain > summary::-webkit-details-marker { display: none; }
.eng-explain > summary h2::after { content: " +"; color: var(--ink-62); font-weight: 500; }
.eng-explain[open] > summary h2::after { content: " \2212"; }
.eng-steps { margin: 0; padding-left: 22px; display: grid; gap: 8px; font-size: 14px; color: var(--ink-70); max-width: 78ch; }
.eng-steps b { color: var(--ink); }
.eng-chips { justify-content: flex-end; }
.eng-seg { display: flex; flex-wrap: wrap; gap: 6px; }
.eng-pad { padding-top: 9px; font-size: 14px; }
.eng-onday { display: flex; flex-wrap: wrap; gap: 6px 16px; padding-top: 9px; font-size: 13.5px; color: var(--ink-70); }
.eng-onday b { color: var(--ink-62); font-weight: 500; margin-right: 4px; }
.eng-quick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.eng-quick .btn { color: var(--ink-70); }
.eng-check { display: flex; gap: 8px; align-items: center; font-size: 14px; color: var(--ink-70); padding-top: 0 !important; }
.f .eng-check input { width: auto; padding: 0; margin: 0; accent-color: var(--coral); }
.eng-running { font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.08em; color: var(--ink-62); }
.eng-running::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 10px; border-radius: 50%; background: var(--coral); vertical-align: middle; animation: eng-pulse 1s ease-in-out infinite alternate; }
@keyframes eng-pulse { from { opacity: 0.25; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .eng-running::before { animation: none; } }
.eng-run .ph { align-items: flex-start; flex-wrap: wrap; }
.eng-run .ph h2 { overflow-wrap: anywhere; }
.eng-stats .v { font-size: 22px; }

.eng-funnel { display: flex; flex-direction: column; gap: 2px; }
.eng-frow { display: grid; grid-template-columns: minmax(160px, 1.2fr) 2fr 48px; gap: 12px; align-items: center; padding: 6px 0; }
.eng-flabel { font-size: 14px; }
.eng-flabel small { display: block; color: var(--ink-62); font-size: 12px; }
.eng-fbar { height: 10px; border-radius: 5px; background: var(--ink-08); overflow: hidden; }
.eng-fbar i { display: block; height: 100%; border-radius: 5px; background: var(--ink-50); }
.eng-frow.added .eng-fbar i { background: var(--coral); }
.eng-frow:last-of-type .eng-fbar i { background: var(--good); }
.eng-fcount { text-align: right; font-weight: 700; }
.eng-removed { margin: -2px 0 6px; font-size: 13px; }
.eng-removed summary { cursor: pointer; color: var(--ink-62); }
.eng-removed ul { margin: 6px 0 4px; padding-left: 18px; display: grid; gap: 4px; }

.eng-place .who { font-size: 12px; }
.eng-place .note { margin-top: 6px; }
.eng-links { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 4px; font-size: 12.5px; }
.eng-links a { color: var(--ink-62); }
.eng-add { flex-direction: column; align-items: flex-end; }
.eng-add select { padding: 6px 8px; border-radius: 8px; border: 1.5px solid var(--ink-12); background: var(--cream); color: var(--ink); font-size: 13px; }

.eng-tl { display: flex; flex-direction: column; gap: 4px; }
.eng-tlrow { display: grid; grid-template-columns: minmax(180px, 1.3fr) 2fr 56px; gap: 12px; align-items: center; font-size: 13px; padding: 3px 0; }
.eng-tllabel { min-width: 0; }
.eng-tllabel b { font-weight: 600; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eng-tllabel small { color: var(--ink-62); font-size: 12px; overflow-wrap: anywhere; }
.eng-tlbar { position: relative; height: 12px; border-radius: 3px; background: var(--ink-08); }
.eng-tlbar i { position: absolute; top: 0; bottom: 0; border-radius: 3px; background: var(--ink-50); }
.eng-tlrow.k-google .eng-tlbar i { background: var(--good); }
.eng-tlrow.k-meta .eng-tlbar i, .eng-tlrow.k-openai .eng-tlbar i, .eng-tlrow.k-anthropic .eng-tlbar i { background: var(--coral); }
.eng-tlrow.late { opacity: 0.55; }
.eng-tlms { text-align: right; color: var(--ink-62); }

.eng-proc { margin: 0; padding-left: 26px; display: flex; flex-direction: column; gap: 2px; }
.eng-proc > li { padding: 4px 0; border-bottom: 1px solid var(--ink-08); }
.eng-proc > li:last-child { border-bottom: 0; }
.eng-proc > li.late { opacity: 0.7; }
.eng-proc summary { cursor: pointer; display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; padding: 4px 0; font-size: 14px; }
.eng-proc summary .muted { font-size: 13px; }
.eng-at { margin-left: auto; font-size: 12px; color: var(--ink-62); font-family: var(--mono); }
.eng-body { padding: 6px 0 12px; font-size: 13.5px; display: flex; flex-direction: column; gap: 10px; }
.eng-body ol, .eng-body ul { margin: 4px 0 0; padding-left: 20px; }
.eng-kv { grid-template-columns: 120px 1fr; font-size: 13.5px; }
.eng-quote { margin: 0; padding-left: 12px; border-left: 2px solid var(--ink-12); color: var(--ink-70); }
.eng-model { padding: 8px 0; border-bottom: 1px dashed var(--ink-12); }
.eng-model:last-of-type { border-bottom: 0; }
.eng-prompt summary { cursor: pointer; color: var(--ink-62); }
.eng-prompt pre, .eng-body > pre { white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; background: var(--cream-2); color: var(--ink); padding: 10px 12px; border-radius: 10px; margin: 6px 0 0; }
.eng-tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.eng-table { border-collapse: collapse; width: 100%; font-size: 13px; }
.eng-table th { text-align: left; font-weight: 600; color: var(--ink-62); font-size: 12px; padding: 6px 10px 6px 0; border-bottom: 1px solid var(--ink-12); white-space: nowrap; }
.eng-table td { padding: 6px 10px 6px 0; border-bottom: 1px solid var(--ink-08); vertical-align: top; }
.eng-table tr:last-child td { border-bottom: 0; }

@media (max-width: 760px) {
  .eng-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .eng-frow { grid-template-columns: minmax(0, 1fr) 64px 36px; }
  .eng-tlrow { grid-template-columns: minmax(0, 1fr) 72px 44px; }
  .eng-kv { grid-template-columns: 1fr; gap: 2px; }
  .eng-kv dd { margin-bottom: 6px; }
  .eng .f { grid-template-columns: minmax(0, 1fr); }
  .eng .page, .eng { padding-left: 16px; padding-right: 16px; }
  .eng-chips { justify-content: flex-start; }
  .eng-place .act { width: 100%; justify-content: flex-start; padding-left: 48px; }
  .eng-place .t { flex-basis: calc(100% - 48px); }
  .eng .f > label { padding-top: 0; }
  .eng .f .hint { grid-column: 1; }
  .eng-add { flex-direction: row; flex-wrap: wrap; }
  .eng-place { flex-wrap: wrap; }
  .eng-at { margin-left: 0; }
}
