/* ===========================================================================
   table-first.css — F10 v3 Phase A: the app shell (nav | table window | chat
   rail), behind table-first.js's ?tf=1 flag.

   Every selector in this file is scoped under `.tf-shell` (or an id that only
   exists inside it) — `.tf-shell` is added to `.app-shell` exclusively by
   table-first.js's activate(), which only runs when the flag is on AND the
   viewport is >=1024px. So this file is a complete no-op (zero matched
   selectors, zero computed-style effect) on every flag-off page load, no
   matter how it renders.

   Adapted from the reviewer-approved mockup stylesheet
   (docs/product/mockups/_tf-new.css, table-first-mockups round-0 §1/§2) —
   visual recipe copied near-verbatim (colors/spacing are literals lifted from
   there, not re-derived), minus the mockup-only page chrome
   (.tf-mockup-frame/.tf-eyebrow-bar/.tf-section-label and the
   `.chat-container{height:auto!important}` override, which exists ONLY so
   multiple mockup states could stack on one long screenshot page — the real
   app's `.chat-container` app-height sizing, chat.css:3558+, must NOT be
   overridden here).
   =========================================================================== */

/* ---- the 3-column grid + reserved dock row (Phase D, brief's explicit ask
   for round 0 §4: reserve the row now so a later phase is additive). No
   element ever carries `grid-area: dock` in Phase A, so the `auto` row
   simply collapses to 0px height until something is placed there. ---------- */
.app-shell.tf-shell {
    /* Round 2 (owner: "the Chat rail is too narrow") — ONE variable drives
       the rail's non-collapsed width everywhere it's consumed (this rule,
       .rail-collapsed below, and the resize-drag JS in index.html, which
       sets it as an inline style override on .app-shell — highest
       specificity, so it always wins over this default regardless of which
       of the rules below is also matched). 390px is the new default (was a
       literal 336px); a persisted custom width (aw_chat_rail_width,
       initChatRailWidth() in index.html) overrides this default the same
       way initChatRailCollapsed() overrides .chat-rail-collapsed's default
       state — applied as an inline custom property, not a second CSS rule.
       Collapsed states (chat-rail-collapsed below) intentionally do NOT
       reference this variable — collapse always reclaims to a fixed 60px
       regardless of whatever custom width was set, per the round-2 brief's
       "collapse overrides custom width" rule; expand later restores
       whatever this variable currently holds (never reset by collapse),
       which is exactly "restores the CUSTOM width, not the default". */
    --chat-rail-width: 390px;
    display: grid;
    grid-template-columns: 210px minmax(0, 1fr) var(--chat-rail-width);
    grid-template-rows: 1fr auto;
    grid-template-areas:
        "nav   table  chat"
        "dock  dock   dock";
    gap: 14px;
    align-items: start;
}
/* Collapse reclaims the rail's width (item 13, home-polish PR 3). toggleRail()
   (index.html) only ever toggles `.rail-collapsed` on `.app-shell`; it has no
   idea `.tf-shell` is also present. chat.css's own `.app-shell.rail-collapsed`
   override (~line 2115, `60px minmax(0, 1fr)`) is equal specificity (0,2,0) to
   `.app-shell.tf-shell` above, and this file loads after chat.css, so without
   this rule `.tf-shell`'s 210px rail track always won the cascade tie: the
   rail's content collapsed to icons but the grid track never did (the
   "wide-pill button" symptom, since `.tab-bar-item{width:100%}` still filled a
   210px column). Three classes (0,3,0) beats `.tf-shell` (0,2,0) outright, so
   this is safe regardless of source order.
   60px matches the codebase's existing collapsed-rail convention (chat.css
   ~2115 plus its "60px icon strip" comment ~3773) rather than inventing a new
   value (round-0-review.md decision 1). Override ONLY `grid-template-columns`:
   `grid-template-areas`/`grid-template-rows`/`gap` inherit from `.tf-shell`
   above unchanged (round-0-review.md decision 2; a partial restatement of
   this grid is exactly how it bit us before, per table-first.css's own
   history). The 3rd (chat) track stays `var(--chat-rail-width)` (round 2 —
   was a literal 336px) so the chat rail survives at whatever width it
   currently has (default or a persisted custom one), and the middle
   `minmax(0, 1fr)` track absorbs the reclaimed ~150px automatically, so no
   separate rule is needed for "table absorbs the width". No transition: the
   base `.app-shell` comment (chat.css ~147) already warns that animating a
   grid-template-columns fr track pins the computed value in Chrome and the
   collapse silently no-ops. */
.app-shell.tf-shell.rail-collapsed {
    grid-template-columns: 60px minmax(0, 1fr) var(--chat-rail-width);
}
/* Chat-rail collapse (feat/chat-rail-collapse) — the same reclaim, mirrored to
   the right edge. toggleChatRail() (index.html) only ever toggles
   `.chat-rail-collapsed` on `.app-shell`, independent of `.rail-collapsed`
   above, so both need their own override plus one for when both are
   present at once (both collapsed = two 60px strips, center maximal). Each
   selector below is equal-or-higher specificity than the ones it must beat:
   `.chat-rail-collapsed` alone (0,3,0, same weight as `.rail-collapsed`
   above) only ever competes with the base `.tf-shell` (0,2,0) rule, and the
   4-class combined selector (0,4,0) beats every 3-class rule outright
   regardless of source order — same technique already in use here. See
   table-first.css's chat-rail section (below) for the collapsed-strip
   layout this track width enables; full rationale for the "no transition"
   choice is on `.rail-collapsed` above (still applies — animating a
   grid-template-columns fr track pins the computed value in Chrome). */
.app-shell.tf-shell.chat-rail-collapsed {
    grid-template-columns: 210px minmax(0, 1fr) 60px;
}
.app-shell.tf-shell.rail-collapsed.chat-rail-collapsed {
    grid-template-columns: 60px minmax(0, 1fr) 60px;
}
.app-shell.tf-shell > .tab-bar      { grid-area: nav; }
.app-shell.tf-shell > .table-window { grid-area: table; }
.app-shell.tf-shell > .chat-rail    { grid-area: chat; }
/* .app-main shares the `table` track with .table-window — never a real
   overlap: table-first.js's switchTab hook keeps exactly one of the two
   un-hidden at a time (round-0-design.md §3.4 — the tf-shell chrome is
   chat-view-scoped; .app-main still renders Home/Explore/Saved/Wallet/Trips
   on every other tab, at the width it has today). */
.app-shell.tf-shell > .app-main     { grid-area: table; }
/* .app-shell's base align-items:start (chat.css:145) sizes every grid item
   to its own content height, top-aligned — wrong for .table-window/.chat-rail,
   which must fill row 1's full `1fr` track so their internal scroll regions
   (.tw-body, .chat-area-wrapper) work the same way .app-main's own desktop
   stretch override (chat.css:3584-3589, unconditional on .tf-shell so it
   still applies whichever of .app-main/.table-window is un-hidden) already
   does.
   .tab-bar used to be the one deliberate exception here ("a sticky nav,
   unaffected") back when it only ever held nav buttons sized to their own
   content. TF-shell chrome (round-0-design.md, approved 2026-08-15) moves
   the account cluster into the rail's bottom slot on desktop — pinning it
   there needs the SAME full-row-height box .table-window/.chat-rail already
   get (margin-top:auto has nowhere to push into inside a content-sized box),
   so .tab-bar joins this group too. position:sticky and align-self:stretch
   aren't in tension: sticky governs where the box sits during scroll,
   stretch governs how tall the box's own layout box is — a sticky box can
   still be viewport-tall. See chat.css's own `.tab-bar > #userControls`
   rules (appended at the end of that file) for the account-pinning half of
   this change; this file only owns making the row tall enough. */
.app-shell.tf-shell > .table-window,
.app-shell.tf-shell > .chat-rail,
.app-shell.tf-shell > .tab-bar {
    align-self: stretch;
}

/* CRITICAL: the HTML `hidden` attribute and an author `display` rule have
   EQUAL CSS specificity (0,1,0 each — an attribute selector and a class
   selector count the same). Without this rule, `.table-window { display:
   flex; ... }` below would WIN the cascade tie against the UA stylesheet's
   `[hidden]{display:none}` purely by loading later, silently un-hiding the
   shell even though table-first.js never activated and `.tf-shell` was
   never added — exactly the "flag-off byte-identical" hazard this file's
   own header comment warns about. Pin it down explicitly with `!important`
   rather than relying on selector-scoping alone (the `.tf-shell`-scoped
   rules throughout this file are correct too, but THIS is the rule that
   actually matters if any of them are ever mis-scoped in a future edit). */
.table-window[hidden],
.chat-rail[hidden] {
    display: none !important;
}
/* Round 3 live bug (owner's ?tf=1 test — table-window's mode tabs were dead
   clicks): .app-main is the SAME hazard, on a pre-existing element this
   time. chat.css's own desktop rule (`.app-main { display: flex; ...}`,
   chat.css:3584-3589, unconditional on .tf-shell) ties [hidden]'s
   specificity and wins by cascade order — table-first.js sets
   .app-main.hidden=true while the shell owns the chat tab (setShellVisibleForTab),
   but without this rule that attribute was cosmetically true and functionally
   inert: .app-main stayed display:flex, stayed painted OVER .table-window
   (later in DOM order, same grid-area "table"), and silently absorbed
   every click meant for the tab buttons underneath (confirmed with
   `document.elementFromPoint` on a mounted tab button: resolved to
   `.app-main`, not the button). Same fix shape as .table-window/.chat-rail
   above — pin display:none for real when the attribute says so. */
.app-main[hidden] {
    display: none !important;
}

/* ---- table window (replaces "table renders inline in a chat bubble") ---- */
.table-window {
    background: var(--aw-surface);
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius);
    box-shadow: var(--aw-shadow);
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    /* Positioned containing block for the tf=1 center empty-state overlay:
       in table-first mode #chatEmptyState (the search form's home) relocates
       here and paints as an absolute inset:0 overlay over the window, so the
       search form lives on the large center canvas — not the narrow chat rail
       (owner ruling 2026-08-18 / frame-16 "primary canvas per layout"). */
    position: relative;
}
/* When the center hosts the relocated empty state, the placeholder "Search or
   ask in chat…" line is redundant (the form IS the empty state) — hidden via
   the [hidden] attribute set in table-first.js placeEmptyState(). */
/* The center empty state now mounts INSIDE .tw-body (below the control bar),
   so the body is the positioned containing block its inset:0 overlay anchors
   to — keeping the form off the .tw-header row (owner-caught occlusion:
   "No filters applied" was eaten by the card boundary, 2026-08-19). */
.table-window.tf-has-center-emptystate .tw-body {
    position: relative;
}
/* The overlay itself — found MISSING while proving the "+ New search"
   doorknob (brief NEW_SEARCH_DOOR_BRIEF.md) in a real browser: every comment
   above (and in table-first.js's mountEmptyStateInCenter) describes
   #chatEmptyState as "an absolute inset:0 overlay" once it's relocated
   here, but no rule anywhere ever gave `.chat-empty-state` a `position`.
   This went unnoticed because #chatEmptyState and a mounted query card
   never coexisted in `.tw-body` before now (mountQueryCard's
   `bodyMount.textContent = ''` always cleared one before the other could
   render) — the "+ New search" doorknob is the first path where
   showEmptyState() re-adds the form onto a canvas that ALREADY has a
   mounted card sitting in it, which is exactly the scenario frame-16 draws
   ("the form never coexists with a table," frame-16-notes.md). Scoped
   strictly to the tf=1 center-mount context: off tf, and while
   #chatEmptyState still rides in the chat rail, it must stay in normal
   flow (it's the ENTIRE view there, not an overlay over anything). */
.table-window.tf-has-center-emptystate .chat-empty-state {
    position: absolute;
    inset: 0;
    /* round 2 (owner try-pr #332 field failure): z-index:5 tied the mounted
       table's OWN sticky header row (flight-table.css's `.flight-table thead
       th { position: sticky; z-index: 5; }` — the frozen-header-while-
       scrolling pin), and ties resolve by paint/stacking order, not simply
       "later in the DOM wins," once a sticky element is involved — the
       header row kept bleeding through the overlay's top edge. A decisive
       margin over every sticky cell z-index inside a flight table (the
       highest observed there is 6, for a sticky-row/sticky-column corner
       cell) removes any ambiguity. 50 is also a CEILING, not just a floor:
       the avatar/session dropdown menus (chat.css, z-index 60) must keep
       painting over this overlay, so it cannot be raised to out-stack the
       floating .aw-scroll-btn chevrons (flight-table.css, also 60) — those
       instead hide themselves via JS while this overlay is visible (see
       attachScrollIndicator's position() in flight-table.js). */
    z-index: 50;
    overflow: auto;
    padding: 14px 16px 18px;
    background: var(--aw-surface);
}
/* Blocked/failed-search notice overlay (table-first.js renderBlockedNotice) —
   the SAME absolute inset:0 recipe as the center empty state above, including
   the z-index:50 that clears a mounted flight table's sticky header (max
   z-index 6 there) yet stays under the avatar/session menus (z-index 60). It
   paints OVER a mounted card without destroying it (a keyless user's prior cash
   table survives behind an award block); mountQueryCard / mountEmptyStateInCenter
   remove it before the next view. .tw-body is the positioned ancestor, matching
   the empty-state overlay. */
.table-window.tf-has-blocked-notice .tw-body {
    position: relative;
}
.tf-blocked-overlay {
    position: absolute;
    inset: 0;
    z-index: 50;
    overflow: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px 16px;
    background: var(--aw-surface);
}
.tw-header {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 12px 16px;
    border-bottom: 1px solid var(--aw-hairline);
    background: var(--aw-ground-2);
}
.tw-navgroup {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: none;
}
/* Back/Forward — mirrors the real .export-btn/.tab-bar-item button recipe
   (border+radius+color from tokens) rather than inventing a new style.
   Phase A: always disabled (round-0 §3.1 — Phase B wires real history). */
.tw-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: var(--aw-radius-sm);
    border: 1px solid var(--aw-hairline-strong);
    background: var(--aw-elevated);
    color: var(--aw-ink-dim);
    font-size: 0.9rem;
    cursor: pointer;
}
.tw-nav-btn:hover:not(:disabled) {
    border-color: var(--aw-accent);
    color: var(--aw-accent);
}
.tw-nav-btn:disabled {
    opacity: 0.35;
    cursor: default;
}
.tw-history-indicator {
    font-family: var(--aw-font-mono);
    font-size: 0.68rem;
    color: var(--aw-muted);
    letter-spacing: 0.04em;
    white-space: nowrap;
    padding: 0 2px;
}
.tw-chips {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    flex: 1;
    min-width: 0;
}
.tw-chips-label {
    font-family: var(--aw-font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--aw-muted);
    margin-right: 2px;
}
/* Applied-state chip (Phase B fills this row; Phase A ships the empty
   state only — .tw-chips-empty below). Kept here so Phase B is additive. */
.tw-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--aw-font-mono);
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--aw-accent-tint);
    background: color-mix(in srgb, var(--aw-accent) 12%, var(--aw-surface));
    border: 1px solid color-mix(in srgb, var(--aw-accent) 40%, transparent);
    border-radius: var(--aw-radius-pill);
    padding: 4px 6px 4px 11px;
    white-space: nowrap;
}
.tw-chip button {
    all: unset;
    cursor: pointer;
    color: var(--aw-muted);
    font-size: 0.8rem;
    line-height: 1;
    padding: 2px 5px;
    border-radius: 999px;
}
.tw-chip button:hover {
    color: var(--aw-bad);
    background: var(--aw-bad-dim);
}
.tw-chips-empty {
    color: var(--aw-muted);
    font-size: 0.76rem;
    font-style: italic;
}
/* round 1 (dualview-b1-compose, B1 fix, chip-grammar spec §1) — the
   persistent per-mode view-definition strip ("Your Award view · Business ·
   Nonstop"): plain readable text, deliberately NOT styled like .tw-chip
   (no pill background/border, no ✕ — informational only, not yet an editor
   in phase 1). Sits first in .tw-chips, ahead of the existing per-value
   chips — additive, "replacing nothing" per the spec. */
.tw-view-strip {
    font-family: var(--aw-font-mono);
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--aw-ink);
    white-space: nowrap;
}

/* dual-view Phase 2 — the compare tab's two composed strips
   ("Composed from [Your Award view · …] and [Your Cash view · …]"), each
   bracket a real <button> link back to its own tab (spec §4: navigation,
   not editing). .tw-view-strip-link layers a link affordance (underline on
   hover/focus, pointer cursor) onto the SAME plain-text .tw-view-strip look
   — button-reset so it never picks up default button chrome. */
.tw-composed-strips {
    font-family: var(--aw-font-mono);
    font-size: 0.74rem;
    color: var(--aw-muted);
    white-space: normal;
}
.tw-view-strip-link {
    font: inherit;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    color: var(--aw-ink);
    cursor: pointer;
    text-decoration: underline;
    text-decoration-color: transparent;
    text-underline-offset: 2px;
}
.tw-view-strip-link:hover,
.tw-view-strip-link:focus-visible {
    text-decoration-color: currentColor;
}

/* dual-view Phase 2 (F5 fix) — the empty-join "share no flights" copy's
   optional one-line reason (buildModePanel, index.html), gated on both
   sides being independently non-empty. Muted, matching the app's other
   secondary-explainer text (chat.css's own --aw-muted usage). */
.view-toggle-empty-reason {
    color: var(--aw-muted);
    font-size: 0.85rem;
}

/* Mode tabs mount point. The mount itself carries no visual chrome — the
   REAL .view-toggle/.view-toggle-tab nodes buildResultCard already builds
   (flight-table.js/index.html, unchanged) are re-parented in here by
   table-first.js and restyled below to match the mockup's pill recipe,
   without touching buildResultCard/selectCardTab/activateCardTab at all. */
.tw-tabs {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: none;
    margin-left: auto;
}
#tfTabsMount:empty { display: none; }

/* Round 5 (owner try-pr #332 finding): while the center empty-state
   overlay is showing, the tab group / pager / filter-status line all
   describe the previous, now-covered hunt (syncComposerHelpVisibility,
   index.html, toggles their `hidden` property the same way it already
   does for #tfNewSearchBtn). An explicit `[hidden]` override is required
   here — round 3's own lesson relearned correctly this time: `.tw-navgroup`
   /`.tw-chips`/`.tw-tabs` all declare their own `display` above, and any
   author-origin `display` beats the UA's bare `[hidden] { display: none }`
   regardless of specificity, so setting the `hidden` property alone would
   silently do nothing without this. A compound `[hidden]` selector has
   higher specificity than the bare class it overrides, so this correctly
   wins the cascade (round 3's bug was adding `display` to a selector with
   no `[hidden]` counterpart at all — different fix, same failure mode). */
.tw-navgroup[hidden],
.tw-chips[hidden],
.tw-tabs[hidden] {
    display: none;
}
#tfTabsMount .view-toggle {
    background: var(--aw-rail);
    border: 1px solid var(--aw-line);
    border-radius: 10px;
    padding: 3px;
    margin: 0;
}
#tfTabsMount .view-toggle-tab {
    appearance: none;
    border: 1px solid transparent;
    background: none;
    color: var(--aw-muted);
    font-family: var(--aw-font-sans);
    font-size: 0.82rem;
    font-weight: 600;
    border-radius: 7px;
    padding: 6px 13px;
}
#tfTabsMount .view-toggle-tab:hover { background: var(--aw-surface); }
#tfTabsMount .view-toggle-tab.is-active {
    color: var(--aw-accent);
    background: var(--aw-accent-wash-solid);
    border-color: var(--aw-accent-line);
}

/* "+ New search" doorknob (frame-18b panel 2, owner-ruled 2026-08-22,
   round-3-brief.md — the FILLED treatment, recommended over the outlined
   alternative frame-18b also drew): a second sibling of .tw-tabs in
   .tw-header, so .tw-tabs' own margin-left:auto pushes this along with it
   to the header's far right. Ships `hidden` in the markup; index.html's
   syncComposerHelpVisibility() toggles it opposite #chatEmptyState's own
   visibility. Solid accent + white label + a hairline divider (::before)
   separate it from the Award/Cash/Award vs Cash view-tab pills by a
   deliberate margin-left gap — round 1/2 drew this as a quiet dashed chip
   that read as just another tab; frame-18b's argument: it's the one
   control in the header that ACTS rather than toggles, so it should be the
   one that doesn't look like a toggle ("tabs toggle views of THIS search;
   the button starts the NEXT one — different jobs, different clothes"). */
.tw-new-search-btn {
    /* No explicit `display` here on purpose: the button ships `hidden` in
       the markup and relies on the UA's `[hidden] { display: none }` rule
       to actually hide it — any author-origin `display` declaration on
       this selector wins over that UA rule regardless of specificity (author
       origin always beats UA origin), which would leave the button visible
       even while #chatEmptyState is showing (a door on an open door — the
       exact bug rounds 1-2 fixed for the overlay itself). The button's
       content is plain text, so no flex layout is needed for it anyway. */
    flex: none;
    position: relative;
    font-family: inherit;
    font-size: 0.82rem;
    font-weight: 700;
    padding: 11px 16px;
    margin-left: 16px;
    color: var(--aw-on-accent);
    background: var(--aw-accent-btn);
    /* 1px TRANSPARENT border (not border:0) so the filled primary and the
       .tw-new-search-btn--secondary outline door share the exact box model —
       otherwise the secondary's 1px accent border makes it 2px taller/wider
       and the "matched pair" renders ~2px misaligned (no global box-sizing
       reset; Copilot #373 round 2). Invisible on the filled button. */
    border: 1px solid transparent;
    border-radius: var(--aw-radius-sm);
    cursor: pointer;
    white-space: nowrap;
}
.tw-new-search-btn::before {
    content: "";
    position: absolute;
    left: -9px;
    top: 50%;
    transform: translateY(-50%);
    width: 1px;
    height: 26px;
    background: var(--aw-hairline-strong);
}
.tw-new-search-btn:hover {
    background: var(--aw-accent-btn-hover);
}

/* PR2 two-door pair (docs/product/mockups/plan-tab/notes.md §2): the
   SECONDARY treatment for #tfNewSearchBtn now that #tfNewTripBtn (same
   .tw-new-search-btn base, unmodified) is the filled primary beside it.
   Transparent bg + accent outline + accent text — the "quiet" half of the
   filled-vs-outline pair that teaches search ⊂ trip. The one genuinely NEW
   class this PR adds (real class, not inline styles, per the brief); every
   other token/selector here is reused verbatim. */
.tw-new-search-btn--secondary {
    background: transparent;
    color: var(--aw-accent-btn);
    border: 1px solid var(--aw-accent-btn);
}
.tw-new-search-btn--secondary:hover {
    background: var(--aw-accent-wash-solid);
}

.tw-body {
    padding: 14px 16px 18px;
    overflow: auto;
    flex: 1;
    min-height: 0;
}
.tw-body .flight-table-container {
    margin: 0 0 14px;
    max-width: none;
}
/* The re-parented .view-toggle-slot (buildResultCard's panel-swap container)
   loses nothing visually here — tw-body already supplies the padding a
   .bubble would have given it. */
#tfBodyMount .view-toggle-slot { padding: 0; }
#tfBodyMount .view-toggle { display: none; } /* the tablist lives in the
   header (#tfTabsMount) — if a caller ever forgets to detach it from the
   body-bound container before append, hide it here rather than double-show. */

.tf-empty-state {
    color: var(--aw-muted);
    font-size: 0.9rem;
    padding: 40px 10px;
    text-align: center;
}

/* Border-flash so the eye finds a rail-driven update to the window (round-0
   §1 — the SSE chip's click target, and the RESULTS-restore jump target).
   Mirrors results-rail.js's `.rt-jump-flash` idiom (chat.css:3439-3448),
   applied to the window instead of a chat message. */
@keyframes tf-window-flash {
    0% { box-shadow: 0 0 0 2px var(--aw-accent); }
    100% { box-shadow: 0 0 0 2px rgba(90, 168, 255, 0); }
}
.table-window.tf-window-flash {
    animation: tf-window-flash 1.2s ease-out 1;
}
@media (prefers-reduced-motion: reduce) {
    .table-window.tf-window-flash {
        animation: tf-window-flash 0.5s steps(1, end) 1;
    }
}

/* Freshness line — net new (round-0 design doc: no existing "as of"
   element). Quiet, same visual weight as the Seats.aero attribution line it
   sits beside. Phase A ships it static ("Searched just now"); this is
   cosmetic copy, not a computed timestamp — a future phase can wire a real
   one without any layout change. */
.tw-freshness {
    font-size: 0.74rem;
    color: var(--aw-muted);
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 2px;
}
.tw-freshness .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--aw-good);
    flex: none;
}
.tw-footer-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 4px;
}

/* ---- chat rail (Variation A: narrow, symmetric to the left nav) --------- */
.chat-rail {
    background: var(--aw-surface);
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius);
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    /* Round 2: positioning context for .chat-rail-resize-handle below —
       the handle is absolutely positioned against THIS box, not the shell. */
    position: relative;
}
/* Round 2 drag-resize handle — a thin strip over the rail's own left edge,
   spanning its full height. Lives fully inside the box (left:0, not a
   negative offset) so `overflow:hidden` above never clips it; the hairline
   border sits directly under it, so the handle visually reads as "the
   rail's edge is grabbable" without needing to bleed outside the box.
   startChatRailResize()/resetChatRailWidth()/handleChatRailResizeKey()
   (index.html) own the interaction; this file only owns the affordance. */
.chat-rail-resize-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 6px;
    cursor: col-resize;
    z-index: 2;
    background: transparent;
    transition: background-color 0.12s ease;
}
.chat-rail-resize-handle:hover,
.chat-rail-resize-handle:focus-visible,
.chat-rail-resize-handle.is-resizing {
    background: var(--aw-accent);
    opacity: 0.5;
}
.chat-rail-resize-handle:focus-visible {
    outline: none;
    opacity: 0.8;
}
.chat-rail-head {
    padding: 12px 14px;
    border-bottom: 1px solid var(--aw-hairline);
    font-family: var(--aw-font-mono);
    font-size: 0.68rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--aw-muted);
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex: none;
}
/* Round 5 real-engine finding: with the degraded status chip present, the
   row gets tight enough that the bare title span (no min-width:0 in a flex
   row = never shrinks by default) wrapped its TEXT mid-word ("AwardWise ·"
   / "Chat" on two lines) instead of yielding gracefully — caught in a
   screenshot, not by eye. The title is decorative and constant-length; it
   ellipsizes instead of wrapping if space is ever this tight, and
   .crh-right (the actionable side — status + the creation door) never
   loses its own space to make room.
   feat/chat-rail-collapse note: the selector dropped `:first-child` — the
   new .chat-rail-toggle button (left edge of the row, before this span) is
   the real first child now, so `> span:first-child` stopped matching
   anything at all. `.chat-rail-head` has exactly one bare `<span>` child
   (this title), so `> span` alone still targets it uniquely. */
.chat-rail-head > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Round 5 (owner try-pr #332 finding): the always-on "● Connected" chip
   (.crh-live, formerly here) is gone — grepping health.js confirmed it was
   never wired to real connection state at all (100% decorative/static: it
   always read "Connected" regardless of actual backend health, which is
   its own separate real signal — HealthChecker's #healthIndicator, fixed
   bottom-right, untouched by this change). Replaced with .crh-right, a
   small flex cluster holding the DEGRADED-ONLY status chip (.crh-status,
   left) and the relocated "＋ New chat" door (right) — the door's real
   estate used to be occupied by the dead chip's "always showing something"
   habit; now something only shows there when there's actually something to
   say. font-family reset to sans here since .chat-rail-head itself is
   mono/uppercase (the label styling), which .new-chat-btn's own
   font-family:inherit would otherwise pick up wrong once relocated.
   flex-shrink:0 (a real-engine finding, not a guess): this is the
   actionable side of the row — it must never lose space to the decorative
   title, which ellipsizes instead (above) if the row ever gets this
   tight. */
.crh-right {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    font-family: var(--aw-font-sans);
}
.crh-status {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    text-transform: none;
    letter-spacing: 0;
    font-size: 0.72rem;
    font-weight: 600;
    white-space: nowrap;
}
.crh-status .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
}
/* Reconnecting: HealthChecker (health.js) is still actively polling
   /health on its interval — amber, not red, since it hasn't given up. */
.crh-status.is-reconnecting {
    color: var(--aw-warn);
}
.crh-status.is-reconnecting .dot {
    background: var(--aw-warn);
}
/* Offline: reserved for a harder failure state than a single unhealthy
   poll (see updateRailConnectionChip's doc comment, index.html, for why
   this class exists but nothing sets it yet — a design-refinement note
   for the owner, not a silent gap). */
.crh-status.is-offline {
    color: var(--aw-bad);
}
.crh-status.is-offline .dot {
    background: var(--aw-bad);
}
/* table-first.js's moveChatColumnIntoRail() makes .controls-row,
   #profilePanel, .session-bar, .chat-area-wrapper, #shortlistTrayMount,
   .bottom-input-row, #loading, #statusIndicator
   all DIRECT children of .chat-rail (round-0 §3.2's full move inventory —
   not just #chat/.bottom-input-row, to avoid regressing wallet-edit/session-
   switch while tf=1). chat.css's OWN desktop rule for this exact stacking
   (`.app-main > *{flex-shrink:0}` then re-enabling ONLY .chat-area-wrapper
   to shrink/grow, chat.css:3591-3608) is keyed to the `.app-main >` child
   combinator, so it silently stops applying to these nodes once they move —
   mirror it here for their new parent, or every rail child defaults to
   flex-shrink:1 and the whole column would compress unpredictably instead of
   only the transcript giving ground. */
.chat-rail > * {
    flex-shrink: 0;
}
/* The reparented .chat-area-wrapper (holding #chat) becomes the rail's
   scroll region — same idiom chat.css already uses for it, just narrower. */
.chat-rail .chat-area-wrapper {
    flex: 1 1 auto;
    flex-shrink: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 12px 12px 6px;
}
/* Narrow the real .message/.avatar/.bubble chain (chat.css:799-834) for the
   rail's ~300px column — same classes, tighter geometry only. */
.chat-rail .message { margin-bottom: 12px; }
.chat-rail .avatar { width: 26px; height: 26px; font-size: 0.7rem; margin: 0 7px; flex: none; }
.chat-rail .bubble { max-width: calc(100% - 40px); font-size: 0.85rem; padding: 9px 11px; line-height: 1.4; }
.chat-rail .bubble .meta { font-size: 0.66rem; }
.chat-rail .bottom-input-row {
    display: flex;
    align-items: flex-end;
    justify-content: flex-start;
    gap: 8px;
    padding: 10px;
    border-top: 1px solid var(--aw-hairline);
}
/* The real #userInput carries `width:50%; min-width:320px` (chat.css:936-940)
   for the ~700px+ main composer — a hard floor that alone exceeds the
   rail's usable width at the round-0 default (~314px: a 336px column minus
   this row's own 20px padding; round 2 bumped the default to 390px via
   --chat-rail-width and made it resizable down to 300px, so the overflow
   this override fixes is still very much live at the narrow end), pushing
   the textarea+Send row to overflow past the rail's own left edge (found
   rendering the real page at 1500px: textarea's left edge landed ~34px
   outside .chat-rail's box). Override the floor for this context so the
   reused element actually fits — flex:1/min-width:0 is what lets it shrink
   to the space really available, same as any other flex child asked to fit
   a narrower column than its default context. */
.chat-rail .bottom-input-row #userInput {
    flex: 1;
    width: auto;
    max-width: none;
    min-width: 0;
    min-height: 40px;
    font-size: 0.85rem;
    padding: 9px 11px;
}
.chat-rail .bottom-input-row #sendBtnText {
    flex: none;
    height: 40px;
    padding: 0 12px;
    font-size: 0.85rem;
}
/* controls-row / session-bar / profilePanel / loading /
   statusIndicator (round-0-design.md §3.2's full move inventory) render at
   their existing chat.css geometry inside the narrower rail — no bespoke
   overrides in Phase A beyond width/margin (the flex column already clamps
   them to whatever --chat-rail-width currently is — a literal 336px at
   round-0, now a 390px default that's resizable 300-560px as of round 2);
   round-0-review.md Q3 explicitly accepts #profilePanel narrow-but-functional
   as-is, no redesign this round. */
.chat-rail .controls-row,
.chat-rail .session-bar {
    padding: 8px 12px 0;
}
.chat-rail #profilePanel {
    margin: 8px 12px;
}

/* ===========================================================================
   Chat rail collapse (feat/chat-rail-collapse) — mirrors the left nav's
   .app-shell.rail-collapsed idiom (chat.css ~2114) exactly, one screen edge
   over: toggleChatRail()/index.html toggles `.chat-rail-collapsed` on
   `.app-shell`, own class + own localStorage key ('aw_chat_rail_collapsed')
   so the two rails collapse independently and compose. All new rules below
   are scoped under `.app-shell.tf-shell` like the rest of this file, so this
   block is a complete no-op off tf=1/desktop. Grid-column override lives
   next to the base `.tf-shell`/`.rail-collapsed` grid rules near the top of
   this file (a 4-class combined selector beats either 3-class rule outright,
   same technique already used there for compose-ability); everything here is
   the collapsed-strip layout for the chat-rail side specifically. No
   transition, matching this file's own note on the base rail-collapsed rule
   (animating a grid-template-columns fr track pins the computed value in
   Chrome and the collapse silently no-ops). */
.app-shell.tf-shell.chat-rail-collapsed .chat-rail-head {
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 10px;
    padding: 12px 6px;
}
/* Header text and the (optional, usually-hidden) degraded-status chip drop
   out entirely when collapsed — only the toggle + the "+" door remain. */
.app-shell.tf-shell.chat-rail-collapsed .chat-rail-head > span,
.app-shell.tf-shell.chat-rail-collapsed .crh-status {
    display: none;
}
.app-shell.tf-shell.chat-rail-collapsed .crh-right {
    flex-direction: column;
    gap: 10px;
    margin-left: 0;
}
/* The "+ New chat" door sheds its label and centers on the glyph only — same
   glyph-only treatment chat.css gives `.tab-bar-item` under the left rail's
   own `.rail-collapsed` (chat.css ~2123). The creation door stays reachable
   even at 60px wide (surface-local doctrine — a collapsed rail must never
   strand the one action a user might still need). */
.app-shell.tf-shell.chat-rail-collapsed .new-chat-btn {
    padding: 6px 8px;
    margin-left: 0;
}
.app-shell.tf-shell.chat-rail-collapsed .new-chat-label {
    display: none;
}
/* Hide-not-destroy: everything table-first.js's moveChatColumnIntoRail()
   reparented into `.chat-rail` (session bar, transcript, composer, help
   chips, ...) besides the head row is display:none'd while collapsed, never
   removed — the transcript node (and any SSE writes landing in it via
   appendMessage while collapsed) survives a collapse/expand round-trip
   untouched, so chat keeps running in the background exactly like the left
   rail's own collapse leaves nav state untouched. */
.app-shell.tf-shell.chat-rail-collapsed .chat-rail > *:not(.chat-rail-head) {
    display: none;
}
/* The toggle button itself — a compact icon control matching the existing
   .tw-nav-btn recipe (same border/radius/hover convention already used
   elsewhere in this file) rather than inventing a new one. */
.chat-rail-toggle {
    flex: none;
    width: 24px;
    height: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius-sm);
    background: var(--aw-elevated);
    color: var(--aw-ink-dim);
    font-size: 0.8rem;
    line-height: 1;
    padding: 0;
    cursor: pointer;
    position: relative;
}
.chat-rail-toggle:hover {
    border-color: var(--aw-accent);
    color: var(--aw-accent);
}
/* Unread dot (nice-to-have) — _markChatRailUnread()/index.html adds
   `.has-unread` when an agent message lands while collapsed; cleared on
   expand. Same small-dot recipe as `.crh-status .dot` above, just pinned to
   a corner instead of inline. */
.chat-rail-toggle.has-unread::after {
    content: '';
    position: absolute;
    top: -2px;
    right: -2px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--aw-accent);
    border: 1.5px solid var(--aw-surface);
}

/* RESULTS list (left nav): which query's card is the active window content
   (round-0 §3.5). Not a real class before this round — the rail had no
   "current" concept because nothing else in the app had a persistent window
   to point at. Same active-state recipe as .tab-bar-item.is-active
   (chat.css:2087-2091). */
.rail-results-item.is-active-table {
    background: var(--aw-accent-wash-solid);
    color: var(--aw-ink-2);
    border-color: var(--aw-accent-line);
}

/* Rail-only narration types (round-0 §1's A/C seam — chat/SSE turns keep
   plain-text narrations in the rail; the compact "results chip" is one of
   these, built by table-first.js's routeChatResultIntoWindow /
   renderHomeResultIntoWindow). Never a table — flight-table.css is loaded
   for the table WINDOW, nothing in this file ever mounts a
   .flight-table-container inside .chat-rail. */
.cr-narration {
    font-size: 0.8rem;
    line-height: 1.45;
}
.cr-narration .cr-kind {
    display: block;
    font-family: var(--aw-font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--aw-accent);
    margin-bottom: 3px;
}
.cr-narration a,
.cr-narration button.cr-chip-link {
    color: var(--aw-accent);
    text-decoration: none;
    background: none;
    border: none;
    font: inherit;
    padding: 0;
    cursor: pointer;
}
.cr-narration a:hover,
.cr-narration button.cr-chip-link:hover { text-decoration: underline; }

/* ===========================================================================
   B-1 P2: the results control bar (round-0-design.md / round-0-review.md
   APPROVE, scratch/handoff/b1-p2-control-bar/). A SECOND chrome row, sibling
   to .tw-header, refreshed from the SAME renderChrome(card) choke point that
   already keeps #tfChips in sync (table-first.js renderControlBar) — never
   inside #tfBodyMount/card.slot, which mountQueryCard/mutatePanelInPlace wipe
   and rebuild on every render (round-0-design.md §1d).

   Grammar (non-negotiable, frame-14-search-controls.html's own annotation
   "verbs spend · chips are free"): the LEFT group is rectangular spend
   actions (.tw-act — a verb + its spend delta printed as TEXT inside the
   button), the RIGHT group is free pill filter chips (.tw-fchip — never
   spend text, only an active/count badge). Shape CLASS NAME is the source of
   truth, never a color-only distinction — generalizes the cash cabin-strip's
   existing .cabin-strip-fetch (rectangle) vs .cabin-strip-pill (pill)
   convention (flight-table.css ~3706/3668) to the whole bar.

   Colors/spacing are the mockup's own .bar/.act/.fchip/.meter/.drop recipe
   (frame-14-search-controls.html:380-520), mapped onto the app's real --aw-
   design tokens (tokens.css) — no --wx mockup-only token carried over.
   =========================================================================== */
/* [hidden] and an author `display` rule tie at equal specificity (0,1,0
   each) — the SAME hazard table-first.css already documents+fixes for
   .table-window[hidden]/.chat-rail[hidden]/.app-main[hidden] above (this
   file loads after the UA stylesheet, so an unguarded `.tw-control-bar
   {display:flex}` would silently win the tie and show the bar/panel even
   while `hidden` is set — caught live via the real-engine interaction proof,
   not by reading the diff). Pin both down explicitly. */
.tw-control-bar[hidden],
.tw-panel-mount[hidden] {
    display: none !important;
}
.tw-control-bar {
    display: flex;
    align-items: stretch;
    gap: 0;
    border-top: 1px solid var(--aw-hairline);
    background: var(--aw-ground-2);
    padding: 10px 16px 11px;
    flex-wrap: wrap;
    row-gap: 8px;
}
.tw-scope-group,
.tw-filter-group {
    display: flex;
    flex-direction: column;
    gap: 7px;
    min-width: 0;
}
.tw-filter-group { flex: 1; }
/* renderControlBar sets `hidden` on this group for the compare tab (compare has
   no filter knobs). The base `display:flex` above (author, 0,1,0) outranks the
   UA `[hidden]{display:none}` (0,0,0), so without this explicit author rule the
   whole "Filter results" group BLEEDS onto the Award-vs-Cash tab (stale chips +
   the wrong "N flights" count). Same [hidden]-vs-author-CSS trap as .tw-fchip. */
.tw-filter-group[hidden] { display: none; }
.tw-group-label {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--aw-muted);
}
.tw-group-label b { color: var(--aw-ink-dim); font-weight: 700; }
.tw-bar-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tw-bar-divider {
    flex: none;
    width: 1px;
    margin: 2px 16px;
    background: var(--aw-hairline-strong);
}

/* LEFT — rectangular spend actions (.tw-act) */
.tw-act {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    font: inherit;
    text-align: left;
    padding: 6px 12px 7px;
    color: var(--aw-ink);
    background: color-mix(in srgb, var(--aw-accent) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--aw-accent) 38%, transparent);
    border-radius: var(--aw-radius-sm);
    cursor: pointer;
}
.tw-act:hover:not(:disabled) { background: color-mix(in srgb, var(--aw-accent) 16%, transparent); }
.tw-act:disabled { opacity: 0.5; cursor: progress; }
.tw-act-verb { font-size: 0.76rem; font-weight: 700; }
.tw-act-verb .tw-act-pencil { color: var(--aw-accent); margin-left: 4px; font-size: 0.72rem; }
.tw-act-delta { font-size: 0.66rem; font-weight: 650; color: var(--aw-accent); font-variant-numeric: tabular-nums; }
.tw-act-sub { font-size: 0.64rem; color: var(--aw-muted); }

.tw-meter { display: flex; flex-direction: column; gap: 4px; padding: 6px 2px 0 4px; }
.tw-meter-num {
    font-family: var(--aw-font-mono);
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    color: var(--aw-ink-dim);
    font-variant-numeric: tabular-nums;
}
.tw-meter-num small { color: var(--aw-muted); font-weight: 600; }
.tw-meter-track {
    width: 78px;
    height: 5px;
    border-radius: 3px;
    background: var(--aw-structure-wash);
    border: 1px solid var(--aw-hairline);
    overflow: hidden;
}
.tw-meter-fill { display: block; height: 100%; background: var(--aw-accent); border-radius: 3px; }
/* cash-date-parity fix (ruling 5): display-only over-cap treatment — the
   NUMBER is already clamped to the cap in JS (renderControlBar), this just
   gives the clamped-full bar a distinct color so "used > cap" still reads
   as different from "exactly at cap", never a bar past 100% either way. */
.tw-meter.tw-meter-over .tw-meter-fill { background: var(--aw-warn); }
.tw-meter.tw-meter-over .tw-meter-num { color: var(--aw-warn-deep); }

/* RIGHT — free pill filter chips (.tw-fchip) */
.tw-fchip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font: inherit;
    font-size: 0.74rem;
    font-weight: 600;
    padding: 5px 12px;
    color: var(--aw-ink-dim);
    background: transparent;
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius-pill);
    cursor: pointer;
    white-space: nowrap;
}
.tw-fchip:hover:not(:disabled) { border-color: var(--aw-structure); }
.tw-fchip:disabled { opacity: 0.4; cursor: not-allowed; }
.tw-fchip .tw-fchip-caret { font-size: 0.62rem; color: var(--aw-muted); }
.tw-fchip-open { color: var(--aw-ink); border-color: var(--aw-structure); background: var(--aw-structure-wash); }
.tw-fchip-active {
    color: var(--aw-on-choice);
    font-weight: 700;
    background: var(--aw-choice);
    border-color: var(--aw-choice);
}
.tw-fchip-active .tw-fchip-badge {
    font-family: var(--aw-font-mono);
    font-size: 0.64rem;
    font-weight: 700;
    background: var(--aw-on-choice);
    color: var(--aw-choice);
    border-radius: var(--aw-radius-pill);
    padding: 0 6px;
}
.tw-fchip-clear { border-style: dashed; color: var(--aw-muted); }
/* dualview-p1-reds round-1 (RED 2) — "Re-apply search filters" (spec §3.2):
   hidden by default in markup; table-first.js toggles it, never CSS-only. A
   solid, choice-colored outline (not dashed like Clear all) since this is a
   restorative CTA, not a destructive one. */
.tw-fchip-reapply { color: var(--aw-choice); border-color: var(--aw-choice); }
/* [hidden] override (see this file's own note ~L344): the base .tw-fchip sets
   display:inline-flex, which beats the UA `[hidden]{display:none}` (author
   beats UA regardless of specificity) — so a chip table-first.js toggles via
   the `hidden` attribute (#tfReapplySeed) is marked hidden in the logic but
   still LAYS OUT visibly without this. Real-engine caught it: the affordance
   showed even at the seed. Scoped to the chip family so any hidden chip hides. */
.tw-fchip[hidden] { display: none; }
.tw-count {
    align-self: center;
    margin-left: 2px;
    font-size: 0.72rem;
    font-weight: 650;
    color: var(--aw-choice);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The open filter panel — round-1 owner fix: was a full-width disclosure
   below the whole bar, which always opened flush-left no matter which chip
   (Stops, Airlines, ...) — on the RIGHT of the bar — triggered it, reading as
   disconnected from its own trigger. Now a real dropdown anchored to the
   clicked chip (table-first.js positionPanel), same pattern as
   trip-picker.js's `_mountPopover`/`.trip-picker-popover-fixed` (trips.css):
   reparented to <body> so `.table-window`'s `overflow: hidden` (this file,
   ~line 138) can never clip it, `position: fixed` with the trigger's own
   `getBoundingClientRect()` for placement, clamped to the viewport so it
   never runs off-screen at narrow widths (owner-tested 1100px). One panel
   open at a time; Esc/click-outside/re-click-same-chip closes
   (table-first.js wireControlBar) — unchanged.

   Layer: above everything the results table can raise (frozen cells 4,
   sticky header 5/6, scroll buttons 60, value-badge tooltip 980,
   flight-table.css) — same tier as the trip-picker popover (990, trips.css)
   since both are page-level floating menus — but below the save toast (999)
   and the app's modals (1000+), which must still cover it. */
.tw-panel-mount {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 990;
    min-width: 240px;
    max-width: 360px;
    max-height: min(70vh, 480px);
    overflow-y: auto;
    background: var(--aw-surface);
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius);
    box-shadow: var(--aw-shadow);
}
.tw-panel-mount:empty,
.tw-panel-mount[hidden] { display: none; }
.tw-panel {
    padding: 12px 16px 14px;
}
.tw-panel-title {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--aw-muted);
    margin-bottom: 8px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.tw-panel-close {
    all: unset;
    cursor: pointer;
    color: var(--aw-muted);
    font-size: 0.85rem;
    padding: 2px 6px;
}
.tw-panel-close:hover { color: var(--aw-ink); }
.tw-panel-row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 6px 2px;
    font-size: 0.8rem;
    color: var(--aw-ink-dim);
    border-top: 1px solid var(--aw-hairline);
    cursor: pointer;
    background: none;
    border-left: 0; border-right: 0; border-bottom: 0;
    width: 100%;
    text-align: left;
    font-family: inherit;
}
.tw-panel-row:first-of-type { border-top: 0; }
.tw-panel-row:disabled { cursor: default; opacity: 0.6; }
.tw-panel-tick {
    flex: none;
    width: 16px;
    height: 16px;
    border-radius: 4px;
    border: 1.5px solid var(--aw-hairline-strong);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.64rem;
    font-weight: 700;
}
.tw-panel-tick.is-on { background: var(--aw-choice); border-color: var(--aw-choice); color: var(--aw-on-choice); }
.tw-panel-count {
    margin-left: auto;
    font-family: var(--aw-font-mono);
    font-size: 0.68rem;
    font-weight: 700;
    color: var(--aw-muted);
    font-variant-numeric: tabular-nums;
}
.tw-panel-field { padding: 7px 2px; border-top: 1px solid var(--aw-hairline); }
.tw-panel-field:first-of-type { border-top: 0; }
.tw-panel-field-label {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 0.78rem;
    color: var(--aw-ink-dim);
    margin-bottom: 4px;
}
.tw-panel-input {
    width: 100%;
    max-width: 220px;
    padding: 6px 9px;
    font: inherit;
    font-size: 0.82rem;
    color: var(--aw-ink);
    background: var(--aw-surface);
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius-sm);
}
.tw-panel-time-row { display: flex; gap: 10px; flex-wrap: wrap; }
.tw-panel-time-field { display: flex; flex-direction: column; gap: 3px; }
.tw-panel-time-field label { font-size: 0.68rem; color: var(--aw-muted); }
.tw-panel-preset-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.tw-panel-preset {
    font: inherit;
    font-size: 0.7rem;
    font-weight: 600;
    padding: 4px 10px;
    color: var(--aw-ink-dim);
    background: var(--aw-elevated);
    border: 1px solid var(--aw-hairline-strong);
    border-radius: var(--aw-radius-pill);
    cursor: pointer;
}
.tw-panel-preset:hover { border-color: var(--aw-accent); color: var(--aw-accent-tint); }
.tw-panel-note {
    font-size: 0.68rem;
    line-height: 1.45;
    color: var(--aw-muted);
    margin-top: 8px;
}
.tw-panel-warn {
    font-size: 0.68rem;
    line-height: 1.45;
    color: var(--aw-warn);
    margin-top: 6px;
}
/* Round-1 owner fix: every other filter panel now needs an explicit Apply
   click (below) — Cabin stays the one EXCEPTION (a free, instant, client-side
   proxy over the existing #326/#327 cabin strip; buildCabinPanel below), so
   it needs its own label saying so up front, or its lack of an Apply button
   reads as broken rather than as a deliberate different model. */
.tw-panel-note-instant {
    color: var(--aw-accent-tint);
    font-weight: 650;
    margin-top: 0;
    margin-bottom: 8px;
}
.tw-panel-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 10px;
}
.tw-panel-btn {
    font: inherit;
    font-size: 0.76rem;
    font-weight: 650;
    padding: 6px 14px;
    border-radius: var(--aw-radius-sm);
    cursor: pointer;
    border: 1px solid var(--aw-hairline-strong);
    background: var(--aw-elevated);
    color: var(--aw-ink-dim);
}
.tw-panel-btn:disabled { opacity: 0.5; cursor: default; }
.tw-panel-btn-primary {
    color: var(--aw-on-accent, #fff);
    background: var(--aw-accent-btn);
    border-color: var(--aw-accent-btn);
}
.tw-panel-btn-primary:hover:not(:disabled) { background: var(--aw-accent-btn-hover); }
.tw-panel-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.tw-panel-date-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: var(--aw-font-mono);
    font-size: 0.72rem;
    padding: 3px 6px 3px 10px;
    border-radius: var(--aw-radius-pill);
    background: color-mix(in srgb, var(--aw-accent) 12%, var(--aw-surface));
    border: 1px solid color-mix(in srgb, var(--aw-accent) 40%, transparent);
    color: var(--aw-accent-tint);
}
.tw-panel-date-chip button { all: unset; cursor: pointer; color: var(--aw-muted); padding: 1px 4px; }
.tw-panel-date-chip button:hover { color: var(--aw-bad); }
