/*
 * Aetranse ID — admin panel stylesheet.
 *
 * Plain hand-written CSS, loaded straight from public/ with asset(). There is no
 * build step: edit this file and refresh. Nothing here is generated, so no class can
 * be "purged" for being invisible to a scanner — the failure that silently dropped
 * styles written from JS files under the old Tailwind setup.
 *
 * Ported from the design reference at C:\dev\aetrans-admin (src/app/globals.css plus
 * the shell markup in src/components/Shell.tsx). The reference is a Next.js mockup;
 * its component classes are kept name-for-name so the two stay comparable, and the
 * layout utilities it wrote inline are collected here into real classes.
 *
 * Layout uses LOGICAL properties throughout — margin-inline-start, padding-inline,
 * inset-inline — never left/right. Arabic is a supported panel locale and the layout
 * flips from one dir="rtl" attribute; hardcoding left/right would strand the sidebar
 * on the wrong edge.
 *
 * Sections
 *   1. Tokens
 *   2. Base + reset
 *   3. Layout helpers
 *   4. Shell — topbar, sidebar, footer
 *   5. Page furniture
 *   6. Cards
 *   7. Buttons
 *   8. Forms
 *   9. Tables
 *  10. Alerts
 *  11. Badges, chips, switches
 *  12. Modal + help bubble
 *  13. Responsive
 */

/* ------------------------------------------------------------------ *
 * 1. Tokens
 * ------------------------------------------------------------------ */

:root {
    /*
     * Tells the browser which way to paint the controls it draws ITSELF — checkboxes,
     * radios, scrollbars, the date picker. It became load-bearing on 2026-08-18: with
     * Tailwind's forms plugin gone those are native again, so without this a light
     * page on a dark OS gets dark checkboxes. The dark block below declares its own.
     */
    color-scheme: light;

    --accent: #FF6B11;
    --accent-hover: #E85F0A;
    --accent-soft: #FFF1E6;
    --accent-soft-border: #FFD8B5;
    --bg-page: #F8F8F6;
    --bg-card: #FFFFFF;
    --bg-subtle: #FAFAF9;
    --bg-hover: #F5F5F4;
    --border: #E7E5E4;
    --border-strong: #D6D3D1;
    --text-primary: #1C1917;
    --text-secondary: #57534E;
    --text-muted: #8A8683;
    --danger: #DC2626;
    --danger-border: #FCA5A5;
    --danger-bg: #FEF2F2;
    --radius-card: 14px;
    --radius-field: 10px;
    --radius-pill: 999px;
    /* 64, not the reference's 56: the logo is now the only brand element and runs
       at 44px, which would sit flush against both borders in a 56px bar. */
    --topbar-height: 64px;
    --sidebar-width: 260px;
    --page-max: 1280px;

    /*
     * Everything below exists so the dark theme can be one block of overrides
     * instead of a second copy of the stylesheet. The light values are exactly
     * what was hardcoded here before, so the light panel is unchanged.
     */

    /* An "inverse" plate: dark with light text in the light theme, and the other
       way round in dark. The active nav item, .plate, the bulk bar and the active
       pager button are all this. Without the pair, `color: #FFF` on a
       `var(--text-primary)` background becomes white on near-white in dark. */
    --bg-inverse: #1C1917;
    --on-inverse: #FFFFFF;

    /* Warm off-white surfaces: the add panel, row hover, the weekend column. */
    --bg-warm: #FFFBF7;
    --border-soft: #F0EDEA;
    --border-hover: #A8A29E;
    --text-help: #78716C;
    --text-label: #44403C;

    /* Status tones, shared by the alerts, the chips, the KPI icons and the toasts.
       Each is a background / foreground / border trio. */
    --tone-amber-bg: #FFFBEB;  --tone-amber-fg: #92400E;  --tone-amber-br: #FDE68A;
    --tone-green-bg: #F0FDF4;  --tone-green-fg: #166534;  --tone-green-br: #86EFAC;
    --tone-green-br-soft: #BBF7D0;
    --tone-red-fg: #991B1B;    --tone-red-br: #FECACA;
    --tone-blue-bg: #EFF6FF;   --tone-blue-fg: #1E40AF;   --tone-blue-br: #BFDBFE;
    --tone-orange-bg: #FFF7ED; --tone-orange-fg: #9A3412; --tone-orange-br: #FDBA74;
    /* Google's own green URL and blue title, for the search-result preview. */
    --serp-url: #15803D;       --serp-title: #1A0DAB;
    /* Heat map: busy, steady, quiet. Solid dots, not backgrounds under text. */
    --heat-high: #10B981;      --heat-mid: #FBBF24;       --heat-low: #EF4444;

    /* The select caret, drawn here rather than by the old Tailwind build. It is a
       data URI, so the stroke colour cannot be a variable — the dark theme swaps
       the whole image. */
    --select-caret: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%2378716C' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/*
 * DARK THEME
 *
 * Set by an inline script in <head> from localStorage, falling back to the
 * operating system's preference — see layouts/app.blade.php. It is an attribute on
 * <html> rather than a class so the pre-paint script can set it with one call and
 * the selector below always wins on specificity.
 *
 * Warm near-blacks, not neutral greys: the light palette is warm (#F8F8F6, #1C1917)
 * and a cold grey dark theme beside a warm light one reads as two different products.
 *
 * The accent lifts slightly. #FF6B11 on #14120F is legible but sits close to the
 * minimum for small text, and the accent is used for links and active states.
 */
:root[data-theme="dark"] {
    --accent: #FF7A28;
    --accent-hover: #FF9048;
    --accent-soft: #2A1B10;
    --accent-soft-border: #4A2E17;

    --bg-page: #14120F;
    --bg-card: #1C1A17;
    --bg-subtle: #201D19;
    --bg-hover: #2A2621;
    --bg-warm: #221E19;

    --border: #322D27;
    --border-strong: #443D35;
    --border-soft: #2A2621;
    --border-hover: #5A5148;

    --text-primary: #F5F3F0;
    --text-secondary: #BDB6AD;
    --text-muted: #8C857C;
    --text-help: #9C948A;
    --text-label: #E0DBD4;

    --danger: #F87171;
    --danger-border: #7F2A2A;
    --danger-bg: #2A1717;

    --bg-inverse: #F5F3F0;
    --on-inverse: #1C1A17;

    /* Tones invert: a deep tinted background with a light tinted foreground. The
       light theme's foreground colours are far too dark to sit on a dark card. */
    --tone-amber-bg: #2A2010;  --tone-amber-fg: #FBBF24;  --tone-amber-br: #4A3A18;
    --tone-green-bg: #12241A;  --tone-green-fg: #6EE7B7;  --tone-green-br: #1E4632;
    --tone-green-br-soft: #1E4632;
    --tone-red-fg: #FCA5A5;    --tone-red-br: #5A2424;
    --tone-blue-bg: #131E2E;   --tone-blue-fg: #93C5FD;   --tone-blue-br: #24384F;
    --tone-orange-bg: #2A1B10; --tone-orange-fg: #FDBA74; --tone-orange-br: #4A2E17;
    /* Lifted for the dark ground; the light pair is unreadable on #14120F. */
    --serp-url: #6EE7B7;       --serp-title: #93C5FD;
    /* Lifted for the dark ground — the light values read as mud on #14120F. */
    --heat-high: #34D399;      --heat-mid: #FCD34D;       --heat-low: #F87171;

    --select-caret: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239C948A' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

    color-scheme: dark;
}

/* ------------------------------------------------------------------ *
 * 2. Base + reset
 * ------------------------------------------------------------------ */

*,
*::before,
*::after { box-sizing: border-box; }

* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--bg-page);
    color: var(--text-primary);
    font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-size: 14px;
    line-height: 1.5;
}

h1, h2, h3, h4, h5, h6, p, figure { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }
table { border-collapse: separate; border-spacing: 0; }

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.hidden { display: none !important; }
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------------------------ *
 * 3. Layout helpers
 *
 * A deliberately small set. Anything that repeats across screens earns a
 * named component class below instead — a second utility framework grown by
 * hand is the thing this rewrite exists to avoid.
 * ------------------------------------------------------------------ */

.row { display: flex; align-items: center; gap: 12px; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.row-top { display: flex; align-items: flex-start; gap: 12px; }
.row-wrap { flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: 12px; }
.grow { flex: 1 1 auto; min-width: 0; }
.shrink-0 { flex-shrink: 0; }
.ms-auto { margin-inline-start: auto; }

/*
 * Gap steps for the row/col helpers. Two of them, plus the 12px default — the views
 * had grown six hand-written values (4, 6, 8, 10, 12, 14px) written inline, which is
 * a spacing scale nobody chose. 4 and 6 became `.gap-xs`, 8 and 10 `.gap-sm`, 12 and
 * 14 the default; nothing moved by more than 2px.
 */
.gap-xs { gap: 6px; }
.gap-sm { gap: 8px; }

.grid-2, .grid-3, .grid-4 { display: grid; gap: 20px; }/*
 * A block set off from the one above it. Named for the RELATIONSHIP, which is why it
 * is one value and not a scale: everywhere it is used the question was "is this part
 * of the block above, or a new one?", and the answer was always the same.
 *
 * `.card.is-spaced` (16px) is its own thing and predates this — a card between cards
 * sits in a rhythm the page already has.
 */
.is-separated { margin-top: 20px; }

/* A group of fields separated from the group above by a rule, not just by space —
   the second half of a form that means something different from the first. */
.is-divided { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--border); }
/*
 * The profile identity block: avatar, then name over email over role.
 */
.identity .name { font-size: 16px; font-weight: 700; line-height: 1.2; }
.identity .email { margin-top: 2px; }
.identity .user-role { display: inline-block; margin-top: 6px; }

/*
 * DELETE ACCOUNT. Held inside a red box of its own rather than sitting loose in the
 * card, because it is the one control on the screen that cannot be undone.
 */
.danger-zone {
    margin-top: 16px; padding: 18px;
    border: 1px solid var(--danger-border); background: var(--danger-bg);
    border-radius: var(--radius-field);
}
.danger-zone .field { margin-top: 14px; max-width: 320px; }
.danger-zone .actions { display: flex; align-items: center; gap: 10px; margin-top: 16px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.span-all { grid-column: 1 / -1; }/* The row of buttons that closes an add panel — "Add new drop point" and friends. */
.add-actions { margin-top: 12px; }
/* A second line under a value: a booking's route under its reference, a coupon's
   title under its code, a coordinate under a place name. */
.cell-sub { display: block; margin-top: 4px; }
/* Chips or badges under the field that produced them. */
.chip-row { margin-top: 8px; }
/* A mounted picker — the flag on a language row, the logo on a rates row. */
.mount-row { margin-top: 6px; }

.stack > * + * { margin-top: 20px; }
.text-muted { color: var(--text-muted); }
.text-secondary { color: var(--text-secondary); }
.text-danger { color: var(--danger); }
.text-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.text-sm { font-size: 12.5px; }
.text-center { text-align: center; }
.text-end { text-align: end; }
.nowrap { white-space: nowrap; }
/* The emphasised value in a table cell, and the same value when it is a link to the
   record's own screen. Written once here because every list draws one. */
.text-strong { font-weight: 600; }
.text-bold { font-weight: 700; }
.text-upper { text-transform: uppercase; }
.text-medium { font-weight: 500; }
.link-strong { font-weight: 600; color: var(--accent); }
.link-strong:hover { text-decoration: underline; }

/*
 * The 8-character booking reference, wherever it is drawn — the calendar's offline
 * table, the bookings list, the dashboard. Accent when it links to the record, plain
 * when the reader has no permission to open it.
 */
.booking-ref { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; font-weight: 700; }
a.booking-ref { color: var(--accent); }
a.booking-ref:hover { text-decoration: underline; }

/*
 * The text half of a media object — an avatar or an index bubble, then a name over a
 * detail line. `min-width: 0` is the whole point of it: a flex child defaults to
 * `min-width: auto`, so a long address refuses to shrink and pushes the row wider
 * than its container instead of ellipsing.
 */
.media-text { min-width: 0; }

/* ------------------------------------------------------------------ *
 * 4. Shell
 * ------------------------------------------------------------------ */

.app-shell { min-height: 100vh; display: flex; flex-direction: column; }

/* Topbar */
.topbar {
    height: var(--topbar-height);
    position: sticky;
    top: 0;
    z-index: 30;
    background: var(--bg-card);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding-inline: 20px;
}

.topbar-brand { display: flex; align-items: center; gap: 12px; }

/*
 * At desktop widths the brand block is exactly as wide as the sidebar beneath it,
 * so the logo sits on the sidebar's centre line rather than floating near the left
 * edge of the page.
 *
 * The negative start margin cancels the topbar's own 20px inline padding. Without
 * it the block would run 20px..280px while the sidebar runs 0..260px, and the logo
 * would be centred 10px to the right of the menu below it — close enough to look
 * like a mistake rather than a decision.
 *
 * Width follows --sidebar-width, so the block narrows with the rail on its own.
 */
@media (min-width: 1025px) {
    .topbar-brand {
        position: relative;
        flex: 0 0 auto;
        width: var(--sidebar-width);
        margin-inline-start: -20px;
        justify-content: center;
    }
}

/*
 * No `transition: width` here, and it is not an oversight.
 *
 * The width comes from var(--sidebar-width), and a length that resolves through a
 * custom property does not animate: the transition starts, never advances, and the
 * element keeps its OLD width indefinitely — the brand block stayed 260px wide over
 * a 76px rail. The sidebar itself has never transitioned either, so the rail snaps
 * on both, which is also what keeps them in step.
 */

/*
 * Logo only — no background fill, no wordmark beside it.
 *
 * Height is fixed and width is free, because the logo IS the brand name here: a
 * wordmark is wider than it is tall, and a square box would either crop it or
 * shrink it to fit the narrower dimension. object-fit is `contain`, so a tall or
 * wide file letterboxes instead of losing its edges.
 */
.brand-link { display: flex; align-items: center; }
.brand-mark {
    height: 44px; 
    min-width: 44px; 
    max-width: 220px;
    border-radius: 10px; 
    /*overflow: hidden;*/
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
/*
 * Sized with an ABSOLUTE max-height, not height:100%.
 *
 * A percentage height only resolves when the parent's height is definite to the
 * layout algorithm in play; centring the item meant it was not, so the logo fell
 * back to its intrinsic size — a 512px source rendered 512px tall and pushed
 * straight out of the topbar. max-height in px never has that dependency.
 *
 * width:auto with max-width:100% keeps the aspect ratio for a wide wordmark while
 * stopping a very wide one from crowding out the rest of the bar.
 */
.brand-mark img {
    display: block;
    max-height: 70px;
    height: auto;
    width: auto;
    max-width: 100%;
    object-fit: contain;
}
/* Fallback mark when no logo is uploaded — accent on the page, not white on nothing. */
.brand-mark i { font-size: 26px; color: var(--accent); }

.topbar-actions { display: flex; align-items: center; gap: 12px; }
.topbar-divider { width: 1px; height: 28px; background: var(--border); }

/* Language dropdown trigger */
.lang-button {
    display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-pill); cursor: pointer; transition: .15s;
    color: var(--text-secondary);
}
.lang-button:hover { background: var(--bg-subtle); border-color: var(--border-strong); color: var(--text-primary); }
.lang-button .lang-code { font-size: 12px; font-weight: 700; letter-spacing: .02em; }
.lang-button .chevron { font-size: 10px; color: var(--text-muted); }

/* Round icon button in the topbar */
.icon-button {
    position: relative; width: 36px; height: 36px; display: grid; place-items: center;
    border-radius: var(--radius-pill); border: 1px solid var(--border);
    background: var(--bg-card); color: var(--text-secondary); cursor: pointer; transition: .15s;
}
.icon-button:hover {
    background: var(--accent-soft); border-color: var(--accent-soft-border); color: var(--accent);
}

/*
 * THE THEME TOGGLE shows the theme you would switch TO — a moon on a light page, a sun
 * on a dark one. Both icons are in the markup and CSS picks one, so the button is
 * correct before any script runs; repainting it on load is what would make it flicker
 * on every navigation.
 *
 * Keyed off `[data-theme]` alone, with no `prefers-color-scheme` fallback, because the
 * inline <head> script ALWAYS stamps the attribute — it resolves the OS preference
 * itself when no choice is stored. That is the same reason the theme tokens have no
 * media query.
 *
 * This was missing entirely until 2026-08-18: both icons rendered, stacked, and
 * overflowed the 36px button on every screen in the panel.
 */
.theme-toggle .theme-icon-light { display: none; }
.theme-toggle .theme-icon-dark { display: block; }

:root[data-theme="dark"] .theme-toggle .theme-icon-dark { display: none; }
:root[data-theme="dark"] .theme-toggle .theme-icon-light { display: block; }
.icon-button .dot-count {
    position: absolute; top: -4px; inset-inline-end: -4px;
    width: 20px; height: 20px; border-radius: var(--radius-pill);
    background: var(--accent); color: #FFF; font-size: 10px; font-weight: 700;
    display: grid; place-items: center; border: 2px solid #FFF;
}

/* User menu */
.user-button {
    display: flex; 
    align-items: center; 
    gap: 10px;
    padding: 4px 12px 4px 4px; 
    border-radius: var(--radius-pill);
    /*border: 1px solid transparent; */
    border: 1px solid #e7e5e4; 
    background: transparent; 
    cursor: pointer; 
    transition: .15s;
}
.user-button:hover { border-color: var(--border); background: var(--bg-subtle); }
/*
 * Initials, not a photo. The reference pulls avatars from pravatar.cc; an admin
 * panel that reaches out to a third-party image host on every page leaks who is
 * logged in and breaks entirely on an office network that blocks it.
 */
.user-avatar {
    width: 32px; height: 32px; border-radius: var(--radius-pill);
    object-fit: cover; border: 1px solid var(--border);
    display: grid; place-items: center; flex-shrink: 0;
    background: var(--accent-soft); color: var(--tone-orange-fg);
    font-size: 12px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
}
.user-avatar-lg { width: 40px; height: 40px; font-size: 14px; }
.user-name { font-size: 13px; font-weight: 600; line-height: 1; display: flex; align-items: center; gap: 6px; }
.user-email { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
.user-role {
    font-size: 10px; font-weight: 700; letter-spacing: .04em; padding: 2px 6px;
    border-radius: 4px; background: var(--accent-soft); color: var(--tone-orange-fg);
    border: 1px solid var(--accent-soft-border); text-transform: uppercase;
}

.dropdown { position: relative; }
.dropdown-menu {
    position: absolute; inset-inline-end: 0; top: 100%; margin-top: 8px; width: 240px;
    background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
    box-shadow: 0 12px 40px rgba(0,0,0,.12); overflow: hidden; z-index: 40;
}
.dropdown-head { padding: 12px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--bg-hover); }
.dropdown-body { padding: 6px; }
.dropdown-item {
    display: flex; align-items: center; gap: 10px; width: 100%;
    padding: 8px 12px; border-radius: 8px; font-size: 13px;
    background: transparent; border: 0; cursor: pointer; text-align: start;
}
.dropdown-item:hover { background: var(--bg-subtle); }
.dropdown-item.is-danger { color: var(--danger); }
.dropdown-item.is-danger:hover { background: var(--danger-bg); }
.dropdown-item.is-current { background: var(--accent-soft); color: var(--tone-orange-fg); font-weight: 600; }
.dropdown-item i { width: 16px; text-align: center; color: var(--text-muted); }
.dropdown-item.is-danger i { color: inherit; }

.lang-flag { width: 22px; height: 15px; object-fit: cover; border-radius: 3px; flex-shrink: 0; }
.lang-flag-empty { background: var(--bg-hover); display: inline-block; }

.user-meta { display: flex; flex-direction: column; text-align: start; }

/* Busy indicator used by #overlay in the layout. */
.spinner {
    width: 40px; height: 40px; border-radius: 999px;
    border: 4px solid var(--border); border-top-color: var(--accent);
    animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Body split */
.shell-body { display: flex; flex: 1 1 auto; }

/* Sidebar */
.sidebar {
    width: var(--sidebar-width); flex-shrink: 0; display: flex; flex-direction: column;
    position: sticky; top: var(--topbar-height); height: calc(100vh - var(--topbar-height));
    background: var(--bg-card); border-inline-end: 1px solid var(--border); overflow: hidden;
}
.sidebar-scroll { flex: 1 1 auto; overflow-y: auto; padding: 12px; }
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }

/*
 * Nav groups are <details>, not a JS accordion: an open section then survives a
 * page load with no script and no stored state, which is what the sidebar did
 * before this redesign.
 */
.nav-group + .nav-group { margin-top: 20px; }
.nav-group-label {
    width: 100%; display: flex; align-items: center; justify-content: space-between;
    padding: 6px 8px; cursor: pointer; list-style: none;
    font-size: 10.5px; font-weight: 700; letter-spacing: .11em;
    color: var(--text-muted); text-transform: uppercase;
}
.nav-group-label::-webkit-details-marker { display: none; }
.nav-group-label:hover { color: var(--text-primary); }
.nav-group-label .chevron { transition: transform .15s; font-size: 10px; }
.nav-group:not([open]) .chevron { transform: rotate(-90deg); }
[dir="rtl"] .nav-group:not([open]) .chevron { transform: rotate(90deg); }

/* A standalone item with no group above it. */
.nav-single { margin-bottom: 4px; }

.nav-items { margin-top: 4px; display: flex; flex-direction: column; gap: 2px; }
.nav-item {
    position: relative; display: flex; align-items: center; gap: 12px;
    padding: 8px 12px; border-radius: 12px;
    font-size: 13.5px; font-weight: 500; color: var(--text-secondary); transition: .15s;
}
.nav-item:hover { background: var(--bg-hover); color: var(--text-primary); }
/*
 * Fixed icon column. Font Awesome glyphs have different natural widths — a globe is
 * wider than a gear — so without this every label starts at a slightly different
 * place and the menu reads as ragged.
 */
.nav-item i {
    width: 18px; flex-shrink: 0; text-align: center;
    font-size: 15px; opacity: .75;
}
.nav-item .label { flex: 1 1 auto; }
.nav-item.is-active { background: var(--bg-inverse); color: var(--on-inverse); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
.nav-item.is-active i { opacity: 1; }
.nav-item.is-active::before {
    content: ""; position: absolute; inset-inline-start: 0; top: 50%; transform: translateY(-50%);
    width: 4px; height: 24px; border-radius: 999px; background: var(--accent);
}
.nav-badge {
    font-size: 11px; font-weight: 700; padding: 2px 6px;
    border-radius: var(--radius-pill); background: var(--accent); color: #FFF;
}
.nav-item.is-active .nav-badge { background: var(--on-inverse); color: var(--bg-inverse); }

.sidebar-help {
    margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--bg-hover);
}
.sidebar-help-card {
    border-radius: 12px; padding: 12px;
    background: linear-gradient(135deg, var(--accent) 0%, #FF8A3D 100%); color: #FFF;
}
.sidebar-help-card .title { font-weight: 700; font-size: 14px; line-height: 1.2; }
.sidebar-help-card .sub { font-size: 12px; opacity: .8; margin-top: 4px; }
.sidebar-help-card .action {
    margin-top: 12px; width: 100%; background: #FFF; color: var(--text-primary);
    font-size: 12px; font-weight: 700; padding: 8px; border: 0;
    border-radius: 8px; cursor: pointer; transition: .15s;
}
.sidebar-help-card .action:hover { background: var(--accent-soft); }

/* .sidebar-foot is gone — the copyright it held duplicated the page footer.
   .status-dot went with the footer's "All systems operational" line on 2026-08-19;
   it had no other user, so the rule went rather than being left matching nothing. */

/* Mobile drawer */
.sidebar-toggle { display: none; }
.drawer-backdrop {
    position: fixed; inset: 0; z-index: 40;
    background: rgba(0,0,0,.3); backdrop-filter: blur(3px);
}

/* Main + footer */
.main { flex: 1 1 auto; min-width: 0; background: var(--bg-page); display: flex; flex-direction: column; }
.main-inner { flex: 1 1 auto; }

.site-footer { margin-top: 40px; border-top: 1px solid var(--border); background: var(--bg-card); }
.site-footer-inner {
    max-width: var(--page-max); margin-inline: auto; padding: 20px 24px;
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--text-muted);
}

/* ------------------------------------------------------------------ *
 * 5. Page furniture
 * ------------------------------------------------------------------ */

.page { min-height: 100vh; background: var(--bg-page); }
.page-inner { max-width: var(--page-max); margin-inline: auto; padding: 24px 24px 40px; }
.page-heading {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 16px; margin-bottom: 20px; flex-wrap: wrap;
}
/* The text block shrinks; the actions do not. Without this a long subtitle takes
   the whole line and pushes the buttons onto a row of their own. */
.page-heading > :first-child { flex: 1 1 320px; min-width: 0; }
.page-heading > :last-child:not(:first-child) { flex: 0 0 auto; margin-inline-start: auto; }
.page-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.1; color: var(--text-primary); }
.page-subtitle { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }

.breadcrumb {
    display: flex; align-items: center; gap: 8px;
    font-size: 12.5px; color: var(--text-muted); margin-bottom: 12px;
}
.breadcrumb a { color: var(--text-muted); }
/* The last crumb — the page you are on. It is a span, not a link, so it needs the
   weight and the full-strength colour that the trail's links do not carry. */
.breadcrumb .current { color: var(--text-primary); font-weight: 600; }
.breadcrumb a:hover { color: var(--text-primary); }
.breadcrumb svg { width: 14px; height: 14px; opacity: .5; }
/* Flips the separator chevron under dir="rtl" so the trail still reads forwards. */
[dir="rtl"] .breadcrumb svg { transform: scaleX(-1); }

/* The <ol> form used by partials/breadcrumbs. The ported screens write the trail as
   plain spans, so both shapes have to look identical — this makes the list behave as
   the flex row `.breadcrumb` already is, and wrap on a narrow screen. */
.breadcrumb-list {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    margin: 0; padding: 0; list-style: none;
}

/* ------------------------------------------------------------------ *
 * 6. Cards
 * ------------------------------------------------------------------ */

.card {
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-card); overflow: hidden;
    box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 12px rgba(0,0,0,.03);
}
/*
 * No `.card + .card { margin-top }` rule.
 *
 * It looks harmless and is not: inside a grid it hits every card except the first,
 * so tiles 2..n get pushed down and end up visibly shorter than tile 1 in the same
 * row. Spacing between stacked cards belongs to the container — `.stack`, or the
 * grid's own `gap` — not to the card.
 *
 * `.is-stacked` is the opt-in for the one case that does need it: a card sitting
 * directly above another in the page flow, such as the filter card over a table.
 */
.card.is-stacked { margin-bottom: 16px; }
/* …and the mirror of it, for a card that follows something rather than precedes it. */
.card.is-spaced { margin-top: 16px; }

/*
 * The "Loading…" card every Firestore-backed screen shows while its first read is in
 * flight, replaced by the real content when the data lands. Every one of them was
 * the same three declarations written inline.
 */
/*
 * ONE loading state, on every screen that has one.
 *
 * There were two. The availability calendar and the booking detail spelled out
 * `.cal-loading` + a `<span class="spinner-inline">` and got a spinner; fifteen other
 * screens wrote `.card.is-loading` with bare text and got none — so the same wait
 * looked like a working screen on one page and a stalled one on the next.
 *
 * Unified in the STYLESHEET rather than by editing fifteen views: the spinner is a
 * pseudo-element, so every existing `.card.is-loading` gains it with no markup change
 * and nothing to keep in sync. `.cal-loading` is folded in beside it and keeps its own
 * `.spinner-inline` element, which is why that one is excluded from drawing a second
 * ring below.
 */
.card.is-loading,
.cal-loading {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    padding: 48px; text-align: center; color: var(--text-muted); font-size: 13.5px;
}
.card.is-loading::before {
    content: ''; display: inline-block; width: 18px; height: 18px; border-radius: 999px;
    border: 2px solid var(--border); border-top-color: var(--accent);
    animation: spin .7s linear infinite; flex: 0 0 auto;
}
/* A screen that already writes its own spinner must not get a second one. */
.card.is-loading:has(.spinner-inline)::before { content: none; }
@media (prefers-reduced-motion: reduce) {
    .card.is-loading::before { animation: none; }
}

.card-header {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; flex-wrap: wrap;
    padding: 16px 18px; border-bottom: 1px solid var(--border); background: var(--bg-card);
}
/*
 * Same rule as .page-heading: the text block shrinks and wraps its own lines, the
 * toolbar does not. Without it a long card-subtitle takes the whole row and drops
 * "Show N entries" and the search box onto a line of their own — which put those
 * controls in a different place on one table than on every other, purely because
 * that screen's subtitle happened to be longer.
 */
.card-header > :first-child { flex: 1 1 240px; min-width: 0; }
.card-header > .table-toolbar { flex: 0 0 auto; margin-inline-start: auto; }

.card-title { font-size: 14px; font-weight: 700; color: var(--text-primary); }
.card-subtitle { font-size: 12.5px; color: var(--text-secondary); margin-top: 2px; }
.card-body { padding: 18px; }

/* ------------------------------------------------------------------ *
 * 7. Buttons
 * ------------------------------------------------------------------ */

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    height: 36px; padding: 0 14px; border-radius: var(--radius-field);
    font-size: 13.5px; font-weight: 600; border: 1px solid transparent;
    cursor: pointer; transition: .15s; white-space: nowrap;
}
/* A control that sits inside a card header, a row or a toolbar rather than in a
   form: the full 40px button would tower over the text beside it. */
/* Fills its column — the add panels put one under a pair of fields. */
.btn.is-block { width: 100%; justify-content: center; }
.btn.is-compact { height: 32px; padding: 0 12px; }
.btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
.btn-primary { background: var(--accent); color: #FFF; border-color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,.07); }
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); transform: translateY(-1px); }
.btn-secondary { background: var(--bg-card); color: var(--text-primary); border-color: var(--border-strong); }
.btn-secondary:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-hover); }
.btn-danger { background: var(--bg-card); color: var(--danger); border-color: var(--danger-border); }
.btn-danger:hover:not(:disabled) { background: var(--danger-bg); }
.btn-ghost { background: transparent; color: var(--text-secondary); border-color: transparent; }
.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }

.btn-icon {
    width: 32px; height: 32px; padding: 0; display: inline-grid; place-items: center;
    border-radius: 8px; border: 1px solid var(--border);
    background: var(--bg-card); color: var(--text-secondary); cursor: pointer; transition: .15s;
}
.btn-icon:hover { background: var(--bg-hover); color: var(--text-primary); }
.btn-icon-edit {
    width: 32px; height: 32px; display: grid; place-items: center; border-radius: 8px;
    background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary); cursor: pointer; transition: .15s;
}
.btn-icon-edit:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.btn-icon-delete {
    width: 32px; height: 32px; display: grid; place-items: center; border-radius: 8px;
    background: var(--bg-card); border: 1px solid var(--border); color: var(--text-help); cursor: pointer; transition: .15s;
}
.btn-icon-delete:hover { border-color: var(--danger-border); color: var(--danger); background: var(--danger-bg); }

/*
 * Block / unblock, toned amber and green. Given their own tones rather than a plain
 * .btn-icon because the pair sits next to Delete: three identical grey squares beside
 * a destructive one is where a mis-click comes from.
 */
.btn-icon-block, .btn-icon-unblock {
    width: 32px; height: 32px; display: grid; place-items: center; border-radius: 8px;
    background: var(--bg-card); border: 1px solid var(--border); color: var(--text-help); cursor: pointer; transition: .15s;
}
.btn-icon-block:hover { border-color: var(--tone-amber-br); color: var(--tone-amber-fg); background: var(--tone-amber-bg); }
.btn-icon-unblock:hover { border-color: var(--tone-green-br); color: var(--tone-green-fg); background: var(--tone-green-bg); }

/* ------------------------------------------------------------------ *
 * 8. Forms
 * ------------------------------------------------------------------ */

.form-label {
    display: block; font-size: 12px; font-weight: 600; letter-spacing: .02em;
    text-transform: uppercase; color: var(--text-label); margin-bottom: 6px;
}
.form-input, .form-select, .form-textarea {
    width: 100%; height: 38px; padding: 0 12px;
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    /* background-COLOR, not the shorthand: the shorthand resets background-image,
       which is where the select's caret lives. */
    background-color: var(--bg-card); font-size: 13.5px; color: var(--text-primary);
    outline: none; transition: .15s;
}
.form-textarea { height: auto; padding: 10px 12px; min-height: 80px; resize: vertical; }

/*
 * The dropdown caret, drawn here rather than left to the browser.
 *
 * A textbook case of the leak: the old Tailwind build sets `appearance: none` on
 * every select AND supplies its own arrow as a background-image. This file then sets
 * `background: #FFF` — a SHORTHAND, which resets background-image — so the arrow went
 * and the `appearance: none` stayed, leaving a select with no caret at all.
 *
 * `appearance` is declared explicitly so nothing changes when Tailwind is removed and
 * the native control would otherwise come back with a second, different arrow.
 */
.form-select {
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    background-image: var(--select-caret);
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 11px 8px;
    padding-inline-end: 32px;
    cursor: pointer;
}
/* `background-position` has no logical form, so the flip is spelled out. */
[dir="rtl"] .form-select { background-position: left 12px center; }
.form-select:disabled { cursor: default; opacity: .7; }
.form-input:focus, .form-select:focus, .form-textarea:focus {
    border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.form-input:disabled, .form-select:disabled, .form-textarea:disabled {
    background-color: var(--bg-hover); color: var(--text-muted);
}
.form-help { font-size: 12px; color: var(--text-help); margin-top: 6px; line-height: 1.4; }
/* A character counter past its recommended length. A WARNING, not an error: Google
   truncates rather than rejects, and a longer description is sometimes deliberate. */
.form-help .is-over { color: var(--tone-amber-fg); font-weight: 600; }/* Help text directly under a heading, with nothing above it to be spaced from. */
.form-help.is-flush { margin-top: 0; }
.form-error, .field-error { font-size: 12px; color: var(--danger); margin-top: 6px; }/* A label for a field that follows another field rather than opening a group. */
.form-label.is-spaced { margin-top: 8px; }
/*
 * A NATIVE checkbox, sized. Only the size is set: under `appearance: auto` the browser
 * draws its own box, and a `border` or `border-radius` here would be a rule that reads
 * as if it applies and does nothing. The colour comes from `accent-color` below, which
 * is the one property that reaches a native control.
 *
 * It used to carry both, because Tailwind's forms plugin set `appearance: none` and
 * made this a plain box we painted ourselves. That went with Tailwind on 2026-08-18.
 */
.form-checkbox { width: 16px; height: 16px; }

/*
 * Radios and checkboxes in the panel accent.
 *
 * `accent-color` is now the whole mechanism, which is what this rule was always
 * waiting for: Tailwind's forms plugin used to set `appearance: none` — making
 * accent-color inert — and paint the checked state with `background-color:
 * currentColor`, where currentColor defaulted to its own blue. That is why `color` was
 * being set here as well.
 *
 * The `color` line stays: it costs nothing, and it is what the checked glyph inherits
 * anywhere a browser still honours the old behaviour.
 */
input[type="radio"],
input[type="checkbox"] {
    accent-color: var(--accent);
    color: var(--accent);
}
/*
 * `border-color` was declared here too and does NOTHING — a native control under
 * `appearance: auto` draws its own box and ignores it, the same trap `.form-checkbox`
 * was caught by above. Removed 2026-08-19; the ring is what was doing the work.
 */
input[type="radio"]:focus-visible,
input[type="checkbox"]:focus-visible {
    box-shadow: 0 0 0 3px var(--accent-soft);
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/*
 * Inline validation, applied by public/js/field-error.js. The class goes on the
 * control that failed — which may be a wrapper div, not an <input>, because the
 * searchable dropdowns are button-driven.
 */
.field-invalid,
.field-invalid .form-input,
.field-invalid [data-ss-toggle] {
    border-color: var(--danger) !important;
    box-shadow: 0 0 0 3px rgba(220, 38, 38, .12);
}

/* ------------------------------------------------------------------ *
 * 9. Tables
 * ------------------------------------------------------------------ */

.table-wrap { overflow-x: auto; }
.table { width: 100%; font-size: 13.5px; }
.table thead th {
    text-align: start; font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-help); background: var(--bg-subtle);
    border-bottom: 1px solid var(--border); padding: 10px 14px;
    white-space: nowrap; user-select: none;
}
/*
 * Alignment overrides have to out-specify `.table thead th` (0,1,2) — a bare
 * `.text-end` (0,1,0) loses to it, which left every right-aligned column with a
 * left-aligned heading sitting over right-aligned numbers.
 */
.table thead th.text-end, .table tbody td.text-end { text-align: end; }
.table thead th.text-center, .table tbody td.text-center { text-align: center; }
/* Two-line cells are common here (reference + source, date + time). Aligning to the
   top keeps the first line of every column on the same baseline across the row. */
.table tbody td.align-top { vertical-align: top; }
/* Fixed-width columns. The tick box is as narrow as the box itself, so the first
   real column starts where the eye expects it; the permission grid's menu column is
   held steady so its rows read as a list rather than a ragged edge. */
.table th.col-check { width: 40px; }
.table th.col-menu { width: 220px; }/*
 * Columns that must not collapse when a row goes into edit mode and swaps its text for
 * an input — a 4-character airport code and a 40-character place name are the same
 * column, and the table would resize under the operator's cursor as they typed.
 */
.table td.col-airport { min-width: 140px; }
.table td.col-direction { min-width: 120px; }
.table td.col-place { min-width: 170px; }
.table td.col-times { min-width: 130px; }
.table td.col-notes { min-width: 200px; }
.table thead th.sortable { cursor: pointer; user-select: none; }
.table thead th.sortable:hover { color: var(--text-primary); }
/* The arrow holds its space even when idle, so sorting a column does not nudge the
   heading sideways. */
.table thead th.sortable .sort-arrow {
    display: inline-block; width: 10px; margin-inline-start: 4px;
    opacity: .35; font-size: 11px; text-align: center;
}
.table thead th.sortable.is-sorted { color: var(--text-primary); }
/* `.is-active` is set by window.sortArrowClass alongside the icon, so the arrow does
   not depend on its own TH also having been given `.is-sorted` in the same tick. */
.table thead th.sortable.is-sorted .sort-arrow,
.table thead th.sortable .sort-arrow.is-active { opacity: 1; color: var(--accent); }

.table tbody td {
    padding: 12px 14px; border-bottom: 1px solid var(--border-soft);
    vertical-align: middle; color: var(--text-primary); background: var(--bg-card);
}
.table tbody tr:hover td { background: var(--bg-warm); }
.table tbody tr.editing td { background: var(--tone-orange-bg); }
.table-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.table-footer {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 12px 16px; border-top: 1px solid var(--border);
    background: var(--bg-subtle); font-size: 13px; color: var(--text-secondary);
}
/*
 * The count line at the foot of every table — "Showing 6 of 6 • 2 unsaved row(s)".
 * It is the first child of .table-footer, so the footer's space-between keeps it at
 * the start of the row and the pager or Save button at the end.
 */
.table-info { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
.table-info .sep { color: var(--border); }
.table-info .is-dirty { color: var(--tone-amber-fg); font-weight: 600; }

.table-empty { text-align: center; padding: 48px 24px; color: var(--text-secondary); }
.table-empty h4 { font-weight: 700; color: var(--text-primary); margin-bottom: 6px; }/* The way out of an empty state — "add the first one", "clear the filters". Every
   empty state that offers one had spaced it by hand. */
.table-empty .btn { margin-top: 12px; }
/* An empty state inside a modal has less room to give away than one filling a page. */
.table-empty.is-compact { padding: 24px; }

/* ------------------------------------------------------------------ *
 * 10. Alerts
 * ------------------------------------------------------------------ */

.alert {
    display: flex; gap: 12px; align-items: flex-start;
    padding: 12px 14px; border-radius: var(--radius-field);
    border: 1px solid; font-size: 13.5px; line-height: 1.5;
}
.alert-success { background: var(--tone-green-bg); border-color: var(--tone-green-br-soft); color: var(--tone-green-fg); }
.alert-warning { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); color: var(--tone-amber-fg); }
.alert-error { background: var(--danger-bg); border-color: var(--tone-red-br); color: var(--tone-red-fg); }
.alert-info { background: var(--tone-blue-bg); border-color: var(--tone-blue-br); color: var(--tone-blue-fg); }

/* An alert used as the PAGE banner — the `#status` box each Firestore screen paints,
   and the "Firebase is not configured" warning above it. The gap below it belongs to
   that role, not to every alert: one inside a card body needs no margin at all. */
.alert.is-page { margin-bottom: 16px; }
/* …and for one that follows the block it comments on rather than preceding it. */
.alert.is-spaced { margin-top: 16px; }
/* An alert inside a padded container — a modal body, a panel — that has to keep
   clear of the container's own edges. */
.alert.is-inset { margin: 12px 28px 0; }
/* The leading icon sits on the first line of text, not on the box: without the
   nudge it rides 2px high against a 13.5px line. Was written inline on every alert. */
.alert > i { margin-top: 2px; }

/* A multi-line alert body — the validation summary is the only user so far.
   `margin-inline-start` rather than `padding-left`, so the bullets sit inside the
   alert in Arabic too; padding has no logical form that flips. */
.alert-list { margin: 0; margin-inline-start: 18px; padding: 0; list-style: disc; }
.alert-list > li + li { margin-top: 4px; }/* A link inside an alert takes the alert's own colour — an accent link on a red or
   amber ground is unreadable, and the underline is what marks it as a link. */
.alert a { color: inherit; font-weight: 600; text-decoration: underline; }
/* Title over detail in an alert's body. */
.alert .grow > * + * { margin-top: 2px; }

/* CMS image preview, written from cms_pages/_script.blade.php. */
.cms-thumb {
    height: 80px; width: auto; display: block;
    border-radius: var(--radius-field); border: 1px solid var(--border);
}

/* A field that is readOnly rather than disabled — a locked slug still has to POST its
   value, so `:disabled` styling cannot be used and the greying needs its own class. */
.form-input.is-locked, .form-select.is-locked {
    background-color: var(--bg-subtle); color: var(--text-muted); cursor: default;
}

/* ------------------------------------------------------------------ *
 * 11. Badges, chips, switches
 * ------------------------------------------------------------------ */

.count-badge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; height: 22px; padding: 0 7px; border-radius: var(--radius-pill);
    background: var(--accent); color: #FFF; font-size: 12px; font-weight: 700; line-height: 1;
}
.data-badge {
    display: inline-flex; align-items: center; gap: 4px;
    font-size: 11px; font-weight: 600; padding: 4px 8px;
    border-radius: var(--radius-pill); border: 1px solid var(--border);
    background: var(--bg-subtle); color: var(--text-secondary);
}
.data-badge strong { color: var(--text-primary); }

.status-chip {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
    padding: 5px 10px; border-radius: var(--radius-pill); border: 1px solid; line-height: 1;
}
.status-chip .dot { width: 7px; height: 7px; border-radius: 999px; background: currentColor; }
.chip-confirmed { background: var(--tone-orange-bg); color: var(--tone-orange-fg); border-color: var(--tone-orange-br); }
.chip-pending   { background: var(--tone-amber-bg); color: var(--tone-amber-fg); border-color: var(--tone-amber-br); }
.chip-completed { background: var(--tone-green-bg); color: var(--tone-green-fg); border-color: var(--tone-green-br); }
.chip-cancelled { background: var(--danger-bg); color: var(--tone-red-fg); border-color: var(--tone-red-br); }
.chip-paid      { background: var(--tone-green-bg); color: var(--tone-green-fg); border-color: var(--tone-green-br); }
.chip-unpaid    { background: var(--danger-bg); color: var(--tone-red-fg); border-color: var(--danger-border); }
.chip-dp        { background: var(--tone-amber-bg); color: var(--tone-amber-fg); border-color: var(--tone-amber-br); }
.chip-neutral   { background: var(--bg-subtle); color: var(--text-secondary); border-color: var(--border); }

/* The denominator in a stat tile — "121 / 168". Smaller and quieter than the figure it
   qualifies, so the eye lands on the number that matters first. */
.stat-tile .sub-total { font-size: .72em; font-weight: 600; color: var(--text-muted); }
/* Not a booking status like the rest: something needs the operator's attention. */
.chip-warning   { background: var(--tone-amber-bg); color: var(--tone-amber-fg); border-color: var(--tone-amber-br); }

.switch {
    position: relative; width: 44px; height: 24px; border-radius: var(--radius-pill);
    background: var(--border); border: 1px solid var(--border-strong);
    cursor: pointer; transition: .2s; flex-shrink: 0; padding: 0;
}
.switch:disabled { opacity: .5; cursor: not-allowed; }
.switch-knob {
    position: absolute; top: 2px; inset-inline-start: 2px;
    width: 18px; height: 18px; border-radius: var(--radius-pill);
    background: #FFF; box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: .2s;
}
.switch-on { background: var(--accent); border-color: var(--accent); }
.switch-on .switch-knob { transform: translateX(20px); }
/* The knob travels the other way under RTL; without this it slides off-track. */
[dir="rtl"] .switch-on .switch-knob { transform: translateX(-20px); }
.switch-off { background: var(--border); }

/* ------------------------------------------------------------------ *
 * 12. Modal + help bubble
 * ------------------------------------------------------------------ */

.modal-overlay {
    position: fixed; inset: 0; background: rgba(28,25,23,.55);
    backdrop-filter: blur(4px); display: grid; place-items: center;
    padding: 20px; z-index: 50;
}
.modal {
    background: var(--bg-card); border-radius: 16px; border: 1px solid var(--border);
    box-shadow: 0 20px 60px rgba(0,0,0,.2); width: 100%; max-width: 560px;
    overflow: hidden; animation: modalIn .18s ease;
}
.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border); }
.modal-body { padding: 20px; }
.modal-footer {
    padding: 16px 20px; border-top: 1px solid var(--border);
    display: flex; align-items: center; justify-content: flex-end; gap: 12px;
}
@keyframes modalIn {
    from { opacity: 0; transform: translateY(8px) scale(.98); }
    to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .modal { animation: none; }
    * { transition-duration: .01ms !important; }
}

/*
 * Help tip: a small dot that opens a dark bubble. Dark on light because help is an
 * aside — a white panel on a white card needs a border just to be visible, and then
 * reads as another section of the page rather than a note about it.
 */
.help-tip { position: relative; display: inline-flex; vertical-align: middle; }
.help-dot {
    width: 20px; height: 20px; display: inline-grid; place-items: center;
    border-radius: 999px; border: 0; cursor: pointer;
    background: var(--bg-inverse); color: var(--on-inverse); font-size: 10px;
    /* Normalised: these sit inside uppercase, letter-spaced headings. */
    text-transform: none; letter-spacing: normal; line-height: 1;
}
.help-dot:hover { background: var(--accent); }

/*
 * Revealed on hover and on keyboard focus — no JavaScript for the reveal.
 *
 * Position is set to `fixed` by placeTip() in app.js when the tip is hovered: these
 * sit in table headings inside `.table-wrap` (overflow-x:auto) and inside `.card`
 * (overflow:hidden), and an absolutely-positioned bubble was being cut off
 * mid-sentence by both. Only appearance lives here.
 *
 * visibility rather than display so it can fade, and pointer-events:none so the
 * bubble never sits between the cursor and what is underneath it.
 */
.help-bubble {
    position: absolute; top: 100%; inset-inline-start: 50%; transform: translateX(-50%);
    margin-top: 8px; width: 280px;
    background: var(--bg-inverse); color: var(--on-inverse);
    font-size: 12.5px; line-height: 1.5; padding: 10px 12px; text-align: start;
    border-radius: var(--radius-field); box-shadow: 0 12px 32px rgba(0,0,0,.24); z-index: 45;
    display: flex; flex-direction: column; gap: 3px;
    /* These tips live inside uppercase, letter-spaced labels and headings; without
       this the note inherits them and renders as shouty capitals. */
    text-transform: none; letter-spacing: normal; font-weight: 400;
    visibility: hidden; opacity: 0; pointer-events: none;
    transition: opacity .12s ease, visibility .12s ease;
}
.help-tip:hover .help-bubble,
.help-tip:focus-within .help-bubble { visibility: visible; opacity: 1; }

/* Arrow tracks the dot via --arrow-x, since the bubble shifts away from screen
   edges and its centre no longer lines up with the trigger. */
.help-bubble::before {
    content: ""; position: absolute; bottom: 100%;
    left: var(--arrow-x, 50%); transform: translateX(-50%);
    border: 6px solid transparent; border-bottom-color: var(--text-primary);
}
.help-tip.is-above .help-bubble::before {
    bottom: auto; top: 100%;
    border-bottom-color: transparent; border-top-color: var(--text-primary);
}

.help-bubble-title {
    font-weight: 700; font-size: 12.5px;
    text-transform: none; letter-spacing: normal;
}
.help-bubble-text { opacity: .88; }

/* ------------------------------------------------------------------ *
 * 13. Dashboard
 * ------------------------------------------------------------------ */

/* LIVE badge. Only honest while the screen holds Firestore listeners — the dot
   pulses to show the connection is open, not to decorate. */
.live-badge {
    display: inline-flex; align-items: center; gap: 6px; vertical-align: middle;
    margin-inline-start: 10px; padding: 4px 10px; border-radius: var(--radius-pill);
    background: var(--bg-inverse); color: var(--on-inverse); font-size: 11px; font-weight: 700;
    letter-spacing: .04em;
}
.live-badge .dot { width: 6px; height: 6px; border-radius: 999px; background: #34D399; }
.live-badge.is-live .dot { animation: pulse 1.8s ease-in-out infinite; }
.live-badge.is-stale { background: var(--text-muted); }
.live-badge.is-stale .dot { background: #FCA5A5; animation: none; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .live-badge .dot { animation: none !important; } }

/* Filter bar, revealed by the Filter button */
.filter-bar {
    display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap;
    padding: 16px; margin-bottom: 16px;
}
.filter-bar .field { min-width: 180px; }

/* Small square nav buttons (calendar month stepper) */
.step-button {
    width: 28px; height: 28px; display: grid; place-items: center;
    border-radius: 8px; border: 1px solid var(--border); background: var(--bg-card);
    color: var(--text-secondary); cursor: pointer; font-size: 11px; transition: .15s;
}
.step-button:hover { background: var(--bg-hover); color: var(--text-primary); }

/* Range pills (7D / 30D / 90D) */
.pill-group {
    display: flex; align-items: center; gap: 2px;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-pill); padding: 4px;
}
/* Buttons as well as links: the banner's preview switcher is a set of buttons,
   since it changes what is on screen rather than navigating anywhere. */
.pill-group a, .pill-group button {
    padding: 6px 14px; border-radius: var(--radius-pill); border: 0; background: none;
    font-size: 12px; font-weight: 700; color: var(--text-muted); transition: .15s;
    cursor: pointer; font-family: inherit;
}
.pill-group a:hover, .pill-group button:hover { color: var(--text-primary); }
.pill-group a.is-active, .pill-group button.is-active { background: var(--bg-inverse); color: var(--on-inverse); box-shadow: 0 1px 2px rgba(0,0,0,.1); }

/*
 * The language tabs on a CMS page.
 *
 * The house pill group, not a new control. A tab swaps which language's words are in
 * the translated fields; it navigates nowhere and saves nothing, which is what the
 * line of help underneath it is for.
 */
.lang-tabs { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.lang-tabs .pill-group { flex-wrap: wrap; }
.lang-tabs .form-help { margin: 0; }
/* The Default marker and the plus on a language with no page yet. Dimmed against the
   label so the language itself still reads first. */
.lang-tabs .tab-note { margin-inline-start: 6px; font-size: 10px; opacity: .65; }
/* The active code beside a label, on the fields the tab strip actually swaps. It has
   to read as part of the label and not as a second one, hence the weight drop. */
.lang-suffix { margin-inline-start: 6px; font-weight: 500; color: var(--text-muted); }

/* One block of copy per language on the Banner screen. */
.lang-block + .lang-block { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--border); }
.lang-block > .lang-block-head {
    display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
    font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
    color: var(--text-muted);
}

/* KPI tiles */
/* align-items:stretch is the grid default, but it is stated because it is the whole
   contract of this row: four tiles, identical size, whatever their content. */
.kpi-grid {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 16px; align-items: stretch;
}
.kpi-tile { padding: 16px; display: flex; flex-direction: column; }
.kpi-top { display: flex; align-items: flex-start; justify-content: space-between; }
.kpi-icon {
    width: 36px; height: 36px; display: grid; place-items: center;
    border-radius: 12px; border: 1px solid; font-size: 15px;
}
.kpi-icon.tone-orange { background: var(--tone-orange-bg); border-color: var(--accent-soft-border); color: var(--tone-orange-fg); }
.kpi-icon.tone-green  { background: var(--tone-green-bg); border-color: var(--tone-green-br-soft); color: var(--tone-green-fg); }
.kpi-icon.tone-blue   { background: var(--tone-blue-bg); border-color: var(--tone-blue-br); color: var(--tone-blue-fg); }
.kpi-icon.tone-amber  { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); color: var(--tone-amber-fg); }
.kpi-tag {
    font-size: 11px; font-weight: 700; letter-spacing: .02em;
    padding: 4px 8px; border-radius: var(--radius-pill);
    background: var(--bg-subtle); border: 1px solid var(--border); color: var(--text-secondary);
}
.kpi-label {
    margin-top: 12px; font-size: 11px; font-weight: 700;
    letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted);
}
.kpi-value {
    font-size: 26px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1;
    margin-top: 4px; margin-bottom: 8px;
}
/*
 * margin-top:auto pins the sub-label to the bottom of the tile, so the four values
 * stay on one line even when one tile's caption wraps to two.
 * :empty is hidden rather than left as a bare pill: the caption is written by the
 * screen script, and an empty bordered capsule before it arrives reads as a defect.
 */
.kpi-sub {
    align-self: flex-start; margin-top: auto; padding-top: 0;
    display: inline-flex; font-size: 12px; font-weight: 500;
    padding: 4px 8px; border-radius: var(--radius-pill);
    background: var(--bg-subtle); border: 1px solid var(--border); color: var(--text-secondary);
}
.kpi-sub:empty, .kpi-tag:empty { display: none; }

/* Two-thirds / one-third split under the KPIs */
.dash-split { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; margin-top: 16px; }

/* Bar chart. Heights are set inline as percentages by the screen script; the
   shape, spacing and colour live here so no styling is written from JS. */
/* ------------------------------------------------------------------ *
 * Revenue & volume figure (dashboard)
 * ------------------------------------------------------------------ *
 *
 * SMALL MULTIPLES. Two plots, one measure each, sharing a single row of day ticks.
 * Rebuilt 2026-08-19: revenue (rupiah) and volume (a count) previously shared one
 * track at 70% and 25% of it, which is a two-scale chart wearing one axis — the
 * heights were not comparable and, being stacked, invited the eye to add them.
 *
 * Fill colours are their OWN tokens, not --accent. The brand accent measured 2.73:1
 * against the card surface, under the 3:1 a filled mark needs; --chart-revenue is the
 * next step down the same ramp and clears it. Both were checked with the palette
 * validator in light AND dark against each mode's real surface, and the one pair
 * passes both, so there is nothing to fork per theme.
 */
:root {
    --chart-revenue: #E85F0A;
    --chart-count: #0D9488;
}

/*
 * The card is a flex column so the figure can take the height the card actually HAS.
 * `.dash-split` makes this card and the availability one the same height, and with a
 * fixed-height chart that left 93px of the card empty under the bars while the plots
 * themselves were cramped at 76px and 44px.
 */
.card.is-figure-card { display: flex; flex-direction: column; }
.card.is-figure-card > .card-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 12px; }

/* Height is not fixed: it is the two plots plus the axis band, grown to fill. A fixed
   height that excluded the ticks is what gives a card its own tiny inner scrollbar. */
.chart {
    flex: 1 1 auto; min-height: 210px;
    border-radius: 12px; background: var(--bg-subtle); border: 1px solid var(--border-soft);
    padding: 14px 14px 10px; display: flex; flex-direction: column; gap: 18px;
}

/* Revenue takes the room; volume is the supporting read at ~60% of it. Ratios rather
   than pixels, so both grow together when the card is tall. */
.chart-figure { flex: 1 1 0; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.chart-figure.is-secondary { flex: 0.6 1 0; }
.chart-figure-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
}
/* Text wears text tokens. The plot's own colour is carried by its bars; repeating it
   on the label would make the heading compete with the data. */
.chart-figure-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
.chart-figure-peak { font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/*
 * The plot. The bars are a PERCENTAGE of it, so it needs a definite height — this is
 * the bug the first version of this chart shipped with: its columns sized to their
 * content, the height was indefinite, every `height: 44%` resolved to auto and every
 * bar drew at zero. The figures were right the whole time and nothing was drawn. A
 * `flex: 1 1 auto` item in a column with a definite height is definite; `min-height`
 * keeps it honest when the card is short.
 *
 * The hairline is the baseline the bars stand on. Solid and one shade off the surface:
 * a dashed rule reads as a threshold or a projection when it is just a floor.
 */
.chart-plot {
    flex: 1 1 auto; min-height: 52px;
    display: flex; align-items: flex-end; gap: 2px;
    border-bottom: 1px solid var(--border);
}
/* On a short card both plots bottom out on that min-height and the 2:1 hierarchy
   flattens — revenue keeps a taller floor so it still reads as the headline. */
.chart-figure:not(.is-secondary) .chart-plot { min-height: 76px; }

/*
 * One slot per day, full height so the whole column is the hit target rather than the
 * few pixels the bar happens to occupy — a 3px bar on a quiet day is otherwise
 * impossible to hover.
 */
.chart-slot {
    flex: 1 1 0; min-width: 0; align-self: stretch;
    display: flex; align-items: flex-end; justify-content: center;
    cursor: default; border-radius: 4px 4px 0 0;
}
.chart-slot:hover, .chart-slot:focus-visible { background: var(--bg-hover); outline: none; }
.chart-slot:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }

/*
 * THIN MARKS. The bar filled its whole 38px slot until 2026-08-19, which is the
 * "thick saturated blocks" failure: at that width a saturated fill stops reading as a
 * mark on a scale and becomes a wall of colour, and the shape of the series is lost in
 * it. Capped and centred, so the air around each bar is what separates it from its
 * neighbour — no border, no divider.
 *
 * A percentage floor as well as the cap: on a narrow card the slot shrinks and a fixed
 * 14px would fill it again.
 */
.chart-bar {
    width: 46%; max-width: 18px; min-width: 4px;
    border-radius: 4px 4px 0 0; background: var(--chart-revenue);
}
.is-count .chart-bar { background: var(--chart-count); }

.chart-axis { display: flex; gap: 2px; }
.chart-tick {
    flex: 1 1 0; min-width: 0; text-align: center;
    font-size: 9px; color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.chart-empty { padding: 24px 0; display: grid; place-items: center; color: var(--text-muted); font-size: 13px; }

/* ------------------------------------------------------------------ *
 * Ranked list — Top routes (dashboard)
 * ------------------------------------------------------------------ *
 *
 * Horizontal bars, because the rows are being COMPARED and their names are words:
 * a vertical bar chart would turn six route names into six rotated labels, and a pie
 * of six close values is a puzzle rather than a reading.
 *
 * One measure, so one colour and no legend — each row's own name labels it, and every
 * value is printed beside its bar rather than left to a tooltip.
 */
.rank-list { display: flex; flex-direction: column; gap: 14px; }
.rank-row { display: flex; flex-direction: column; gap: 5px; }
.rank-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
/* The name may be long ("Juanda International Airport → Malang City Center"); it
   takes the space and ellipses, and the value never moves. */
.rank-name {
    font-size: 13px; font-weight: 500; color: var(--text-primary);
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rank-value { flex: 0 0 auto; font-size: 12.5px; font-weight: 600; color: var(--text-primary); }
/* The track is the surface the bar is a share of — it is what makes a short bar mean
   "little of the leader" rather than just "short". */
.rank-track { height: 8px; border-radius: 999px; background: var(--bg-hover); overflow: hidden; }
.rank-fill { display: block; height: 100%; border-radius: 999px; background: var(--chart-revenue); }
.rank-sub { font-size: 11px; color: var(--text-muted); }

/* The hover bubble, shared by anything using public/js/hover-tip.js. Fixed, so a
   card's own overflow cannot clip it. */
.hover-tip {
    position: fixed; z-index: 60; pointer-events: none;
    padding: 6px 10px; border-radius: 8px;
    background: var(--bg-inverse); color: var(--on-inverse);
    font-size: 11.5px; font-weight: 500; white-space: nowrap;
    box-shadow: 0 6px 20px rgba(0,0,0,.18);
}

/*
 * Month heat calendar (dashboard only).
 *
 * `.heat-*`, NOT `.cal-*`: the availability screen owns `.cal-head`, `.cal-row` and
 * `.cal-legend`, and while these shared a namespace this block's `gap: 4px` and
 * `margin-top` leaked onto that grid — the day headings drifted out of line with the
 * cells below them. Keep the two widgets in separate namespaces.
 */
.heat-head, .heat-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
.heat-head div { padding: 4px 0; font-size: 11px; font-weight: 700; text-align: center; color: var(--text-muted); }
.heat-day {
    height: 34px; border-radius: 8px; border: 1px solid var(--border-soft); background: var(--bg-card);
    display: grid; place-items: center; position: relative;
    font-size: 12px; font-weight: 600; color: var(--text-primary); transition: .15s;
}
a.heat-day:hover { border-color: var(--accent-soft-border); background: var(--tone-orange-bg); }
.heat-day.is-today { background: var(--bg-inverse); color: var(--on-inverse); border-color: var(--bg-inverse); }
.heat-day.is-empty { border: 0; background: transparent; }
.heat-dot { position: absolute; bottom: 4px; width: 22px; height: 3px; border-radius: 999px; opacity: .85; }
.heat-dot.high { background: #10B981; }
.heat-dot.mid  { background: #FBBF24; }
.heat-dot.low  { background: #EF4444; }
.heat-legend { display: flex; align-items: center; gap: 14px; margin-top: 12px; font-size: 11px; font-weight: 500; color: var(--text-secondary); }
.heat-legend span { display: inline-flex; align-items: center; gap: 6px; }
.heat-legend .key { width: 8px; height: 8px; border-radius: 999px; }
/* Busy / steady / quiet. Their own colours rather than the tone tokens: this is a
   traffic-light reading of a heat map, not a status chip, and the greens and reds in
   the token set are backgrounds meant to sit under text. */
.heat-legend .key.is-high { background: var(--heat-high); }
.heat-legend .key.is-mid  { background: var(--heat-mid); }
.heat-legend .key.is-low  { background: var(--heat-low); }/* The month name and its arrows, above the grid. */
.heat-nav { margin-bottom: 12px; }

/* ------------------------------------------------------------------ *
 * Lists — filter bar, bulk bar, pagination
 * ------------------------------------------------------------------ */

.list-filters { padding: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; justify-content: space-between; }
.list-filters .group { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* Compact controls inside a filter bar — full-height fields would dwarf the pills. */
.list-filters .form-select, .list-filters .form-input { height: 36px; width: auto; font-size: 13px; }

/* Search field with a leading icon. The icon is positioned rather than set as a
   background image so it inherits currentColor and flips under RTL. */
.search-field { position: relative; display: inline-flex; align-items: center; }
.search-field i {
    position: absolute; inset-inline-start: 12px;
    color: var(--text-muted); font-size: 13px; pointer-events: none;
}
.search-field .form-input { padding-inline-start: 34px; min-width: 240px; }

/* Selection bar. Dark so it reads as a mode the table is in, not another filter. */
.bulk-bar {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin: 0 12px 12px; padding: 10px 14px; border-radius: 12px;
    background: var(--bg-inverse); color: var(--on-inverse); font-size: 13px;
}/* When the bar sits under a toolbar rather than directly under the card's head. */
.bulk-bar.is-spaced { margin-top: 12px; }
.bulk-bar .btn-secondary { background: #FFF; border-color: #FFF; }
.bulk-bar .btn-ghost { color: #FFF; border-color: rgba(255,255,255,.25); }
.bulk-bar .btn-ghost:hover { background: rgba(255,255,255,.12); color: #FFF; }

.pager { display: flex; align-items: center; gap: 6px; }
.pager .page-num {
    width: 32px; height: 32px; display: grid; place-items: center;
    border-radius: 8px; border: 1px solid var(--border); background: var(--bg-card);
    font-size: 12px; font-weight: 700; cursor: pointer; transition: .15s;
}
.pager .page-num:hover { background: var(--bg-hover); }
.pager .page-num.is-active { background: var(--bg-inverse); color: var(--on-inverse); border-color: var(--bg-inverse); }
/* The numbers between Previous and Next. They are written into one mount by
   data-table.js, so that mount is the flex row rather than the buttons themselves. */
.pager .pager-nums { display: flex; align-items: center; gap: 6px; }

/*
 * Row actions, kept on one line and starting where their column starts.
 *
 * They used to be pushed to the far end of the row. That left them floating away
 * from the last value and from their own column heading, which is what the heading
 * sitting hard against the table's right edge looked like.
 */
/*
 * A LOCKED row's actions stay on one line — switch, edit, delete. They are three
 * small controls and wrapping them reads as a broken cell, not as a considered
 * layout. The cell is given room so a narrow table squeezes another column first.
 */
.row-actions {
    display: flex; align-items: center; justify-content: flex-start; gap: 6px;
    flex-wrap: nowrap; white-space: nowrap;
}
/*
 * The actions column takes only the width its buttons need, so the browser squeezes
 * a text column first. Without it the last column absorbed the slack, and once the
 * shuttle table grew to seven columns the buttons were the thing pushed off screen.
 */
.table td:last-child, .table th:last-child { width: 1%; }

/*
 * Badge stacks inside a cell wrap rather than widening their column — three
 * departure times, or a lat/long pair, must not be what makes the table scroll
 * sideways. The row grows a little taller instead, which costs nothing.
 */
.table td .data-badge { white-space: nowrap; }
.table td > .row-wrap { flex-wrap: wrap; }

/*
 * An editing row carries four controls where a locked one carries three icons, and
 * on the shuttle screen — which gained a direction, a timetable and a seat count —
 * that pushed the table past the viewport. Save and Cancel ended up off screen,
 * which is the one pair that must never be.
 *
 * The map button drops to a line of its own, the way the drop point's coordinates
 * sit under its name. Save and Cancel stay on the first line where they are looked
 * for, and the cell wraps instead of stretching the table.
 *
 * `flex-basis: 100%` rather than a max-width on the cell: a fixed width would be
 * wrong the moment the labels are translated — "Simpan"/"Batal" and the Arabic pair
 * are not the widths of "Save"/"Cancel".
 */
tr.editing .row-actions { flex-wrap: wrap; white-space: normal; max-width: 260px; }
.btn-row-map { height: 32px; padding: 0 10px; }
tr.editing .row-actions .btn-row-map { flex: 0 0 100%; justify-content: center; }

/*
 * Route as a stop list: a hollow ring for where the trip starts, a filled dot for
 * where it ends, joined by a dotted connector.
 *
 * A plain "A → B" string reads badly once an address contains its own commas, and
 * charter trips can have several drop-offs — as a vertical list each stop stays
 * legible however many there are.
 */
.route-stops { display: flex; flex-direction: column; gap: 2px; }
.route-stop {
    position: relative; display: flex; align-items: flex-start; gap: 8px;
    padding-inline-start: 2px; font-size: 13px;
}
.route-stop .marker {
    position: relative; flex-shrink: 0; width: 10px; height: 10px; margin-top: 4px;
    border-radius: 999px; border: 2px solid var(--accent);
}
/* The last stop is solid — it is the destination, not another waypoint. */
.route-stop.is-end .marker { background: var(--accent); }
/* Connector drawn from each stop except the last, so it never dangles below. */
.route-stop:not(:last-child) .marker::after {
    content: ""; position: absolute; inset-inline-start: 50%; top: 100%;
    transform: translateX(-50%);
    width: 0; height: 12px; border-inline-start: 2px dotted var(--border-strong);
}
/*
 * A route is free-typed by the customer and can be very long. Capped and ellipsised
 * so one verbose address cannot stretch the column and push Status and Cost off the
 * screen; the full text stays available in the title attribute.
 */
.route-stop .label {
    max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.route-stop.is-end .label { font-weight: 600; }

/* ------------------------------------------------------------------ *
 * Forms — create / edit screens
 * ------------------------------------------------------------------ */

/* Field grids. The child is the field, so a control that needs the full width
   just takes .span-all rather than the grid being restated. */
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
.form-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
/* Four narrow fields that belong together — the sitemap and schema row on a CMS page,
   where each control is a short select or a single number. */
.form-grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }

/*
 * A single-subject settings page — Profile — held to a readable column instead of the
 * full page width, and centred in it. Applied to the heading block as well as the
 * content so the two stay one column.
 */
.form-page { max-width: 760px; margin-inline: auto; }

/* The danger card: destructive settings kept visually apart from ordinary ones. */
.card.is-danger { border-color: var(--danger-border); }
.card.is-danger .card-header { background: var(--danger-bg); border-bottom-color: var(--danger-border); }

/*
 * Action bar pinned to the bottom of the viewport.
 *
 * These forms are long enough to scroll past the save button, and a save you have
 * to hunt for gets missed — which on this screen means a booking taken by phone and
 * never recorded.
 */
/*
 * The action bar at the foot of a form.
 *
 * Deliberately the same shape as .table-footer — helper text at the start, the
 * buttons paired at the end — so every form in the panel ends the same way and the
 * Save button is always in the same place. Set as a house rule 2026-08-11.
 *
 * It stays STICKY, which .table-footer is not: these sit under forms long enough to
 * scroll the Save button off screen, and a save nobody finds is a booking that never
 * got recorded. Same look, one extra behaviour.
 */
.form-actions {
    position: sticky; bottom: 0; z-index: 20;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin-top: 20px; padding: 12px 16px;
    background: var(--bg-subtle); border: 1px solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: 0 -2px 12px rgba(0,0,0,.05);
    font-size: 13px; color: var(--text-secondary);
}

/*
 * The buttons themselves, paired at the end. Used by BOTH bar types, so the pairing
 * is written once instead of as an inline style on every screen.
 */
.form-actions .actions, .table-footer .actions {
    display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
}

/* A read-only summary strip above a form — booking reference, who booked, when. */
.summary-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; }/* Flush to its card's edge — used where the strip IS the card rather than sitting
   inside one with its own padding. */
.summary-strip.is-flush { padding: 0; }
/*
 * The map on the booking screens. One height, set once: it was written inline on the
 * edit screen and by the create screen's script, which is how the two came to differ.
 */
.map-canvas { height: 320px; width: 100%; border-radius: 12px; background: var(--bg-hover); }
/* The numbered stops under it. */
.route-points { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.summary-strip > div { padding: 14px 18px; border-inline-end: 1px solid var(--border); }
.summary-strip > div:last-child { border-inline-end: 0; }
.summary-strip dt {
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-muted);
}
.summary-strip dd { margin: 4px 0 0; font-weight: 600; }

/*
 * A read-only field's VALUE, where an editable screen would put an input. Sized to
 * the input it stands in for so a detail screen and its edit screen line up: the
 * label sits at the same height on both, and the eye does not have to re-find it.
 */
.detail-value { margin: 0; min-height: 38px; display: flex; align-items: center; font-weight: 500; }

@media (max-width: 1024px) {
    .form-grid, .form-grid-2, .form-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .summary-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .summary-strip > div:nth-child(2n) { border-inline-end: 0; }
}

@media (max-width: 640px) {
    .form-grid, .form-grid-2, .form-grid-4, .summary-strip { grid-template-columns: minmax(0, 1fr); }
    .summary-strip > div { border-inline-end: 0; border-bottom: 1px solid var(--border); }
}

/*
 * Radio presented as a selectable card. The whole tile is the label, so the hit
 * area is the tile rather than a 16px circle — this is the first choice on the
 * offline booking form and it decides which half of the form appears.
 */
.choice {
    display: inline-flex; align-items: center; gap: 10px;
    padding: 10px 16px; border: 1px solid var(--border-strong);
    border-radius: var(--radius-field); background: var(--bg-card);
    font-size: 13.5px; font-weight: 600; cursor: pointer; transition: .15s;
}
.choice:hover { border-color: var(--accent-soft-border); background: var(--accent-soft); }
/*
 * THE ONE PLACE THIS PANEL DRAWS A CONTROL ITSELF, and it is deliberate.
 *
 * Everywhere else radios and checkboxes are native with only their size and
 * `accent-color` set — see the note by `.form-checkbox`. That is right for a bare
 * checkbox in a form. It is NOT right here: `accent-color` on a checked radio paints
 * a solid disc whose colour the browser then re-derives for the inner dot, and inside
 * an accent-tinted pill (`.choice:has(input:checked)` gives the pill `--accent-soft`)
 * the result was a heavy dark ring around a muddy centre — the "looks weird" the user
 * reported, and it differs by browser and by theme because the derivation is the
 * engine's, not ours.
 *
 * `appearance: none` and two rules give a crisp ring and a solid dot, identical in
 * every engine and in both themes. Two radios exist in the whole panel and both are
 * in this control, so nothing else is affected.
 */
.choice input {
    appearance: none; -webkit-appearance: none;
    width: 16px; height: 16px; margin: 0; flex: 0 0 auto;
    border: 1.5px solid var(--border-strong); border-radius: 999px;
    background: var(--bg-card); cursor: pointer; transition: .15s;
    /* The dot, drawn as a shadow so it scales from the centre and needs no child. */
    box-shadow: inset 0 0 0 4px var(--bg-card);
}
.choice input:checked {
    border-color: var(--accent);
    background: var(--accent);
    /*
     * The gap between the accent ring and the accent dot is the PILL's background,
     * not the card's — `.choice:has(input:checked)` tints the pill `--accent-soft`,
     * so a card-coloured gap would sit on tinted ground as a visible pale sliver.
     */
    box-shadow: inset 0 0 0 2.5px var(--accent-soft);
}
.choice input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.choice:has(input:checked) {
    border-color: var(--accent); background: var(--accent-soft); color: var(--tone-orange-fg);
}

/*
 * The heading of a field GROUP inside a form — its title and its own control on one
 * line, wrapping on a narrow screen. Distinct from `.card-header`: this sits inside
 * the card body and introduces one field, not the whole card.
 */
.field-head {
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap; gap: 12px; margin-bottom: 10px;
}

/* A checkbox and its label as one clickable line. */
.check-label {
    display: inline-flex; align-items: center; gap: 8px;
    font-weight: 600; cursor: pointer;
}

/*
 * A fixed part of the value, printed against the field rather than typed into it —
 * the leading "/" on a page slug. It is NOT a placeholder: a placeholder disappears
 * the moment anyone types, and this has to stay put, because it is the difference
 * between "about-us" and "/about-us".
 */
.input-prefix { display: flex; align-items: center; gap: 8px; }
.input-prefix > .prefix {
    flex: 0 0 auto; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 14px; color: var(--text-muted);
}

/*
 * A file input. The browser's own control is unstyleable beyond its button, so this
 * shapes the button and leaves the rest to the platform — a bespoke drop zone here
 * would be a second uploader to maintain for no gain.
 */
/*
 * A colour swatch. Sized to the field row rather than stretched to the column: the
 * value is a single colour and a full-width one reads as an empty text input.
 */
.form-color {
    width: 72px; height: 38px; padding: 3px; cursor: pointer;
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    background-color: var(--bg-card);
}

.form-file { display: block; width: 100%; font-size: 13px; color: var(--text-secondary); }
.form-file::file-selector-button {
    margin-inline-end: 12px; padding: 7px 12px; border-radius: var(--radius-field);
    border: 1px solid var(--border-strong); background: var(--bg-card);
    color: var(--text-primary); font: inherit; font-size: 12.5px; font-weight: 600;
    cursor: pointer; transition: .15s;
}
.form-file::file-selector-button:hover { border-color: var(--accent); color: var(--accent); }
/* What was chosen, under the control that chose it. */
/* The thumbnail and its Remove button, side by side. */
.field-preview { margin-top: 8px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* A text button, not a bordered one: it sits beside the picture it acts on, and a
   second solid button next to "Choose File" reads as the more important of the two. */
.btn-remove-image {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 8px; border: 0; background: none; cursor: pointer;
    font-family: inherit; font-size: 12px; font-weight: 600; color: var(--danger);
    border-radius: var(--radius-field);
}
.btn-remove-image:hover { background: var(--bg-subtle); }
.field-preview img { max-width: 200px; max-height: 120px; border-radius: var(--radius-field); border: 1px solid var(--border); }

/*
 * THE SEARCH RESULT PREVIEW on a CMS page — what Google will actually print.
 *
 * It answers the question the SEO boxes raise and cannot: what happens if I leave
 * this blank? Every field falls back (title → page title, description → excerpt), so
 * the preview shows the RESULT rather than the input, and the effect of an empty box
 * is visible instead of theoretical.
 *
 * The three colours are Google's, not the panel's, which is the whole point — it is a
 * picture of another site's UI. They are the one place in this stylesheet where a
 * literal colour is right, so they are tokens with a dark-mode pair rather than the
 * accent family.
 */
.serp-preview {
    padding: 16px 18px; border-radius: var(--radius-field);
    border: 1px solid var(--border); background: var(--bg-subtle);
}
.serp-preview .label {
    font-size: 11px; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--text-muted); margin: 0;
}
.serp-preview .url { margin: 10px 0 0; font-size: 12.5px; color: var(--serp-url); word-break: break-all; }
.serp-preview .title { margin: 2px 0 0; font-size: 17px; line-height: 1.3; color: var(--serp-title); }
.serp-preview .desc { margin: 4px 0 0; font-size: 13px; line-height: 1.5; color: var(--text-secondary); }

/*
 * A table that is ONE FIELD of a form — the permission grid — rather than the
 * screen's own content. Framed like a card because it needs an edge, but not a
 * `.card`: a card nested in a card reads as a second subject.
 */
.table-wrap.is-framed { border: 1px solid var(--border); border-radius: var(--radius-card); }

/* Labelled filter fields */
.filter-field { display: flex; flex-direction: column; gap: 6px; }
.filter-field > .form-label { margin-bottom: 0; }

/* Quick actions */
.quick-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.quick-grid .btn { justify-content: flex-start; }

/* ------------------------------------------------------------------ *
 * 14. Responsive
 * ------------------------------------------------------------------ */

@media (max-width: 1024px) {
    /* The sidebar becomes a drawer, shown by toggling .is-open from app.js. */
    .sidebar {
        position: fixed; inset-block: 0; inset-inline-start: 0; z-index: 50;
        height: 100vh; width: 300px; max-width: 85vw;
        transform: translateX(-100%); transition: transform .2s ease;
        box-shadow: 0 10px 40px rgba(0,0,0,.2);
    }
    [dir="rtl"] .sidebar { transform: translateX(100%); }
    .sidebar.is-open { transform: translateX(0); }
    .sidebar-toggle { display: grid; }
}

@media (max-width: 1024px) {
    .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .dash-split { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 768px) {
    .page-inner { padding: 16px 16px 32px; }
    .topbar { padding-inline: 12px; }
    .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0, 1fr); }
    .kpi-grid { grid-template-columns: minmax(0, 1fr); }
    .quick-grid { grid-template-columns: minmax(0, 1fr); }
    /*
     * 14 days of bars are unreadable at phone width, so the figure scrolls. THE WHOLE
     * FIGURE, not each row: scrolling the plots independently of the axis would slide
     * the bars out of step with their own day labels, which is worse than not
     * scrolling. One scroller, one min-width, so all three rows move together.
     */
    .chart { overflow-x: auto; }
    .chart-plot, .chart-axis { min-width: 340px; }
    /* The language control stays reachable on a phone — only its label is dropped.
       Hiding the whole button would leave a mobile admin unable to switch locale. */
    .lang-button .lang-code, .lang-button .chevron { display: none; }
    .lang-button { padding: 0 10px; }
    .user-email { display: none; }
    /* The logo shrinks rather than disappearing — it is the only brand cue left. */
    .brand-mark { height: 36px; min-width: 36px; max-width: 150px; }
    .brand-mark img { max-height: 36px; }
    .site-footer-inner { flex-direction: column; align-items: flex-start; }
}

/* Numbered marker for a drop-off in the booking detail/edit route list. */
.stop-index {
    display: inline-grid; place-items: center; flex-shrink: 0;
    width: 20px; height: 20px; margin-top: 2px; border-radius: 999px;
    background: var(--accent-soft); color: var(--tone-orange-fg);
    font-size: 11px; font-weight: 700;
}

/* Definition lists on the booking detail screen. */
dl dt {
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-muted);
}
dl dd { margin: 4px 0 0; }

/* Location picker (partials/location-picker.blade.php) */
.lp-results {
    margin-top: 8px; max-height: 192px; overflow-y: auto;
    border: 1px solid var(--border); border-radius: var(--radius-field);
}
.lp-results li + li { border-top: 1px solid var(--border); }
.lp-map {
    margin-top: 16px; height: 288px; width: 100%;
    border-radius: var(--radius-field); background: var(--bg-hover);
}

/* ------------------------------------------------------------------ *
 * Availability calendar
 *
 * A CSS grid rather than a table: every day column has to be exactly the same
 * width whatever a cell contains, and the unit column has to stay pinned while
 * the days scroll. --cal-days is set inline by the screen, since the number of
 * visible days is a runtime choice.
 * ------------------------------------------------------------------ */

.cal-wrap { overflow-x: auto; }
.cal-inner { min-width: 900px; }

.cal-head, .cal-row {
    display: grid;
    grid-template-columns: 220px repeat(var(--cal-days), minmax(0, 1fr));
}
.cal-head {
    position: sticky; top: 0; z-index: 10;
    background: var(--bg-subtle); border-bottom: 1px solid var(--border);
}
.cal-row { border-bottom: 1px solid var(--bg-hover); }
.cal-row:hover .cal-unit { background: var(--bg-warm); }

/* Pinned unit column. Its own background is required — without it the day cells
   scroll visibly underneath the label. */
.cal-corner, .cal-unit {
    position: sticky; inset-inline-start: 0; z-index: 2;
    border-inline-end: 1px solid var(--border);
    padding: 8px 10px;
}
.cal-corner {
    background: var(--bg-subtle);
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-muted);
    display: flex; align-items: center;
}
.cal-unit { background: var(--bg-card); display: flex; align-items: center; gap: 8px; }
.cal-unit .meta { min-width: 0; flex: 1 1 auto; }
.cal-unit .title { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700; }
.cal-unit .sub { font-size: 11px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/*
 * The shuttle row's second line carries a code plate, a fare and a departure count,
 * so it WRAPS where the charter row's single sentence must not. `white-space` and the
 * ellipsis are undone with it — three short facts should stack, not be cut off.
 */
.cal-unit .sub.row { white-space: normal; text-overflow: clip; margin-top: 3px; }
.cal-unit .sub .plate { font-size: 9.5px; padding: 1px 5px; }

/* The code reads as a badge, so it never underlines itself as a link would. */
a.plate { text-decoration: none; }
.plate {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    background: var(--bg-inverse); color: var(--on-inverse);
    padding: 2px 5px; border-radius: 4px; font-size: 11px; white-space: nowrap;
}

/* Day headings */
.cal-day-head {
    padding: 4px 2px; text-align: center;
    border-inline-end: 1px solid var(--border-soft); background: var(--bg-subtle);
}
.cal-day-head:last-child { border-inline-end: 0; }
.cal-day-head.is-weekend { background: var(--tone-amber-bg); }
.cal-day-head.is-today { background: var(--text-primary); }
.cal-day-head .dow { font-size: 11px; font-weight: 700; color: var(--text-secondary); line-height: 1.2; }
.cal-day-head .dom { font-size: 11px; font-weight: 800; }
.cal-day-head .mon { font-size: 10px; color: var(--text-muted); }
.cal-day-head.is-today .dow,
.cal-day-head.is-today .dom { color: var(--on-inverse); }
.cal-day-head.is-today .mon { color: rgba(255,255,255,.7); }
.cal-day-head .today-tag {
    display: inline-block; margin-top: 2px; padding: 0 5px; border-radius: 999px;
    background: var(--accent); color: #FFF; font-size: 9px; font-weight: 700; letter-spacing: .04em;
}

/* Cells */
.cal-cell {
    position: relative; height: 48px; display: grid; place-items: center;
    border: 1px solid var(--border-soft); font-size: 10px; font-weight: 700;
    transition: .12s;
}
.cal-cell.is-clickable { cursor: pointer; }
.cal-cell.is-free { background: var(--bg-card); }
.cal-cell.is-free .dot { width: 6px; height: 6px; border-radius: 999px; background: #10B981; }
.cal-cell.is-free.is-clickable:hover { background: var(--tone-green-bg); border-color: var(--tone-green-br); }
/* Online booking — solid accent, the busiest state on the grid. */
.cal-cell.is-online { background: var(--accent); border-color: var(--accent); color: #FFF; }
/*
 * Offline booking — striped, not just a different flat colour. The client takes
 * phone and walk-in orders alongside the website, and at a glance across 14 days
 * a texture separates them faster than two similar hues.
 */
/* The booking's short reference inside an offline cell, as in the reference design —
   a held day can be matched to its booking without opening anything. */
.cal-cell .ref { display: block; font-size: 9px; font-weight: 500; opacity: .8; }

.cal-cell.is-offline {
    background: repeating-linear-gradient(45deg, var(--tone-orange-bg) 0 6px, var(--tone-amber-br) 6px 12px);
    border-color: var(--tone-amber-br); color: var(--tone-amber-fg);
}
/*
 * Two block states, matching the reference. A vehicle in for servicing is a
 * different operational fact from one deliberately withheld, and the block reason
 * already records which — so the calendar can say it without asking.
 */
.cal-cell.is-maintenance { background: var(--danger-bg); border-color: var(--tone-red-br); color: var(--tone-red-fg); }
.cal-cell.is-maintenance.is-clickable:hover { background: var(--danger-bg); }
.cal-cell.is-blocked { background: var(--bg-inverse); border-color: var(--bg-inverse); color: var(--on-inverse); }
.cal-cell.is-blocked.is-clickable:hover { background: #000; }
/* Unit withdrawn from the fleet — inert, and not clickable. */
.cal-cell.is-out { background: var(--bg-hover); border-color: var(--border); }
.cal-cell.is-weekend.is-free { background: var(--bg-warm); }

/*
 * A booking with no vehicle assigned — one row per booking, sitting under the units
 * of the type it was booked against. Mostly bookings from the mobile app, which sells
 * by type and never names a van.
 *
 * The label is tinted amber because it is a to-do, not a state of the fleet: someone
 * has to open it and assign a vehicle. The days it does NOT cover are blank rather
 * than green — this row is a booking, not a vehicle, so "available" means nothing on it.
 */
/* ------------------------------------------------------------------ *
 * Booking detail
 * ------------------------------------------------------------------ */

/*
 * Two columns: the route and the trip on the left, the records that support them on
 * the right. Single column below 1100px — the map needs real width to be worth
 * drawing, and stacking is better than two cramped halves.
 */
.detail-layout { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr); }

@media (min-width: 1100px) {
    .detail-layout { grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); align-items: start; }
}

.map-frame {
    height: 340px; width: 100%; border-radius: 12px;
    background: var(--bg-hover); border: 1px solid var(--border);
}

/* The stop list under the map. Numbered, because the order IS the route. */
.route-stops { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.route-stop { display: flex; align-items: flex-start; gap: 10px; }
.route-stop-label { font-weight: 600; line-height: 1.35; }

.booking-ref { font-size: 17px; font-weight: 800; letter-spacing: -.01em; }
.booking-total { font-size: 18px; font-weight: 800; }

/* Phone and email as real links — an operator reading this screen is usually about to
   ring the customer, and retyping a number off a screen is how the wrong person gets
   called. */
.contact-line {
    display: flex; align-items: center; gap: 8px;
    color: var(--text-primary); font-size: 13.5px; text-decoration: none;
}
.contact-line:hover { color: var(--accent); }
.contact-line i { width: 14px; font-size: 12px; color: var(--text-muted); flex: 0 0 auto; }
.contact-line:hover i { color: var(--accent); }

/*
 * Loading state, matching the reference: a spinner and a line saying what is being
 * waited for. The grid is drawn entirely from snapshots that arrive after the page,
 * so a bare card with no spinner reads as a screen that failed rather than one that
 * is still filling.
 */
/* `.cal-loading`'s own box lives with `.card.is-loading` now — see the note there.
   Only the spinner element it writes by hand is defined here. */
.spinner-inline {
    display: inline-block; width: 18px; height: 18px; border-radius: 999px;
    border: 2px solid var(--border); border-top-color: var(--accent);
    animation: spin .7s linear infinite; flex: 0 0 auto;
}
/* A spinner that never stops is the one thing motion-sensitivity settings ask us to
   stop; the ring still reads as "busy" without turning. */
@media (prefers-reduced-motion: reduce) {
    .spinner-inline { animation: none; }
}

/*
 * The calendar's hover tooltip — the reference's dark bubble, in place of the
 * browser's own `title`, which waits about a second and cannot be styled.
 *
 * FIXED, because `.cal-wrap` sets `overflow-x: auto` and a non-visible overflow on one
 * axis forces the other to auto too — an absolutely-positioned bubble above the top
 * row would be clipped by its own container. Positioned from JS; hidden on scroll,
 * since a fixed element cannot follow the cell it belongs to.
 */
.cal-tip {
    position: fixed; z-index: 90; top: 0; left: 0;
    max-width: 320px; padding: 6px 10px; border-radius: 8px;
    background: var(--bg-inverse); color: var(--on-inverse);
    font-size: 11.5px; font-weight: 500; line-height: 1.35;
    box-shadow: 0 8px 20px rgba(0,0,0,.18);
    pointer-events: none; opacity: 0; visibility: hidden;
    transition: opacity .12s;
}
.cal-tip.is-open { opacity: 1; visibility: visible; }
@media (prefers-reduced-motion: reduce) {
    .cal-tip { transition: none; }
}

/*
 * A shuttle day cell: a SLOT per scheduled departure, the reference panel's shape —
 * a time, a load bar and a count, each in its own rounded pill.
 *
 * The cell itself is bare: the pills carry the colour, so the cell is only their
 * container and must not tint behind them.
 */
.cal-cell.is-seats {
    display: flex; flex-direction: column; align-items: stretch; justify-content: center;
    gap: 3px; padding: 4px; cursor: pointer; border: 0; background: none;
    font-family: inherit; height: auto; align-self: stretch;
}
.cal-cell.is-seats .n { font-size: 10.5px; font-weight: 700; text-align: center; }

/*
 * The day's load, and the whole of a shuttle cell.
 *
 * `booked/total` over a bar. The cell used to hold one pill per departure, each with
 * its own bar and its own copy of the day's denominator — three to six repetitions of
 * one number, which the operator then had to add up to reach the only figure that
 * refuses a sale. Cut 2026-08-18 against the reference panel.
 *
 * The CELL carries the tone now, because there is no longer a pill to carry it. That
 * also retires `.is-dense-seats`: the cell tints at every width, so the dense view
 * needs no separate treatment — it only drops the figure, which stops fitting.
 */
.cal-cell.is-seats {
    display: flex; flex-direction: column; align-items: stretch; justify-content: center;
    gap: 3px; padding: 5px 6px; cursor: pointer; border: 1px solid transparent;
    background: none; font-family: inherit; height: auto; align-self: stretch;
    position: relative;
}
.cal-cell.is-seats .n { font-size: 10.5px; font-weight: 700; text-align: center; }

.seat-load { display: flex; flex-direction: column; gap: 3px; align-items: stretch; }
.seat-load .fill {
    height: 4px; border-radius: 2px; background: rgba(0, 0, 0, .10); overflow: hidden;
}
.seat-load .fill > span { display: block; height: 100%; background: currentColor; }

.cal-cell.is-seats.is-free { background: var(--bg-card); border-color: var(--border); color: var(--text-secondary); }
.cal-cell.is-seats.is-some { background: var(--tone-orange-bg); border-color: var(--tone-orange-br); color: var(--tone-orange-fg); }
.cal-cell.is-seats.is-limited { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); color: var(--tone-amber-fg); }
.cal-cell.is-seats.is-full { background: var(--danger-bg); border-color: var(--tone-red-br); color: var(--tone-red-fg); }
.cal-cell.is-seats.is-cancelled {
    background: var(--bg-subtle); border-color: var(--border);
    color: var(--text-muted); text-decoration: line-through;
}
.cal-cell.is-seats:hover { filter: brightness(.97); }

/* ------------------------------------------------------------------ *
 * A day cell holding one bar PER RUN (2026-08-25)
 *
 * Seats belong to a city and a run, so a day carries three numbers rather
 * than one. The cell is now a container and each run is the clickable
 * control — which is why the tones below live on `.seat-run` and not on
 * the cell as they do for the day-pool layout above.
 * ------------------------------------------------------------------ */
.cal-cell.is-runs {
    display: flex; flex-direction: column; align-items: stretch; justify-content: center;
    gap: 3px; padding: 4px; position: relative; height: auto; align-self: stretch;
}
.seat-run {
    display: flex; flex-direction: column; gap: 2px; align-items: stretch;
    padding: 3px 5px; border-radius: 6px; border: 1px solid transparent;
    background: none; font-family: inherit; text-align: center; cursor: default;
}
.seat-run.is-clickable { cursor: pointer; }
.seat-run.is-clickable:hover { filter: brightness(.97); }
/* The run's own label — the shared departure time, or "Run 2" when the stops
   in a city are collected at different times on the same vehicle. */
.seat-run .run-label { font-size: 9px; opacity: .75; letter-spacing: .02em; }
.seat-run .n { font-size: 10.5px; font-weight: 700; text-align: center; }

.seat-run.is-free { background: var(--bg-card); border-color: var(--border); color: var(--text-secondary); }
.seat-run.is-some { background: var(--tone-orange-bg); border-color: var(--tone-orange-br); color: var(--tone-orange-fg); }
.seat-run.is-limited { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); color: var(--tone-amber-fg); }
.seat-run.is-full { background: var(--danger-bg); border-color: var(--tone-red-br); color: var(--tone-red-fg); }
.seat-run.is-cancelled {
    background: var(--bg-subtle); border-color: var(--border);
    color: var(--text-muted); text-decoration: line-through;
}

/* Passengers on a time that is not on the timetable. They hold their seats — the day
   is the pool — but nobody knows which vehicle carries them. A corner mark, not a
   state: the cell's tone is already saying how full the day is. */
.seat-stray {
    position: absolute; inset-block-start: 3px; inset-inline-end: 3px;
    width: 5px; height: 5px; border-radius: 50%; background: var(--tone-amber-br);
}

/* The stored-versus-derived line in the departure panel. It only appears when the two
   disagree, so it is allowed to be loud. */
.trip-drift {
    display: flex; gap: 8px; align-items: flex-start;
    margin-top: 10px; padding: 10px 12px; border-radius: var(--radius-field);
    background: var(--tone-amber-bg); border: 1px solid var(--tone-amber-br);
    color: var(--tone-amber-fg); font-size: 12.5px; line-height: 1.5;
}

/*
 * MODAL WIDTHS. The base `.modal` is 560px; these are the three steps around it.
 *
 * They replace five loose numbers written inline — 460, 520, 560, 640, 760 — of which
 * 520 and 560 were a 40px distinction nobody could see and 560 was the default being
 * restated. A modal now says how big it is, not how many pixels it is.
 */
.modal.is-narrow { max-width: 460px; }   /* a title and two buttons */
.modal.is-wide   { max-width: 640px; }   /* a form grid, or a tray of photos */
.modal.is-widest { max-width: 760px; }   /* a table, or a map */
/* A body that can outgrow the screen scrolls inside the modal rather than pushing its
   footer off the bottom — the sweep list is as long as the number of drifted days. */
.modal.is-scrollable .modal-body { max-height: 60vh; overflow-y: auto; }
/*
 * The location picker sits over its own backdrop rather than the shared overlay: it is
 * opened FROM a form that may itself be in a modal, and reusing the shared one would
 * dismiss both together.
 */
.lp-backdrop { position: absolute; inset: 0; }
.lp-modal { position: relative; }

/* The two figures the sweep compares, in the same weight as the seat figures above
   them. Its panel is `.modal is-widest is-scrollable`. */
.audit-figure { font-weight: 700; font-size: 15px; }


/* Seats for one date. A number input has no business being full width. */
.trip-seats-field { margin-top: 14px; }
.trip-seats-field .form-input { max-width: 120px; }

/* Seat figures at the head of the departure panel. */
.trip-stats { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 14px; }
/* The status reads as words, not as a number, so it does not take the figure size. */
.trip-stats dd.is-text { font-size: 13.5px; }
.trip-stats > div { min-width: 84px; }
.trip-stats dt {
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-muted);
}
.trip-stats dd { margin: 4px 0 0; font-size: 18px; font-weight: 700; }

/* The departure times repeated in the row header, so the times are readable even
   when the columns are too narrow to label. */
.dep-pills { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
.dep-pill {
    font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 999px;
    border: 1px solid var(--border); background: var(--bg-subtle); color: var(--text-secondary);
}

/* Says the time on the booking is on no scheduled departure. A seat belongs to a
   RUN since 2026-08-25, so this is no longer informational: a time on no departure
   is on no run and its seats are counted nowhere. The save refuses it, and this is
   what says why. */
.off-timetable-note { color: var(--tone-amber-fg); }

/* At the quarter view a slot cannot hold a bar and two numbers; the colour carries
   it, exactly as a charter cell drops its label there. */
/* At a quarter the figure stops fitting and the bar alone carries the cell; the row
   header's timetable goes with it, because six pills in a 220px column wrap to four
   lines and push every row apart. */
.cal-inner.is-dense .cal-cell.is-seats { padding: 3px; }
.cal-inner.is-dense .cal-cell.is-seats .n,
.cal-inner.is-dense .dep-pills { display: none; }

.cal-row.is-unassigned .cal-unit { background: var(--tone-amber-bg); }
.cal-row.is-unassigned .cal-unit .title { color: var(--tone-amber-fg); font-size: 11.5px; }
.cal-row.is-unassigned .cal-unit .title i { font-size: 10px; }
.cal-cell.is-blank { background: var(--bg-card); border-color: var(--border-soft); }
.cal-cell.is-blank.is-weekend { background: var(--bg-warm); }

/* Legend */
.cal-legend { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 12px; }
/* nowrap per item: without it "Booked (website)" breaks across two lines and the
   keys visually overlap each other rather than wrapping as whole units. */
.cal-legend span.key {
    display: inline-flex; align-items: center; gap: 6px;
    font-weight: 500; white-space: nowrap;
}
.cal-legend .swatch { width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border); }
.cal-legend .swatch.free { background: var(--bg-card); }
.cal-legend .swatch.online { background: var(--accent); border-color: var(--accent); }
.cal-legend .swatch.offline { background: repeating-linear-gradient(45deg, var(--tone-orange-bg) 0 4px, var(--tone-amber-br) 4px 8px); border-color: var(--tone-amber-br); }
.cal-legend .swatch.maintenance { background: var(--danger-bg); border-color: var(--tone-red-br); }
.cal-legend .swatch.blocked { background: var(--text-primary); border-color: var(--text-primary); }
.cal-legend .swatch.out { background: var(--bg-hover); }

/* Shuttle: a fill level rather than a state — a seat is never "blocked", it is taken. */
.cal-legend .swatch.seats-free { background: var(--bg-card); }
.cal-legend .swatch.seats-some { background: var(--tone-orange-bg); border-color: var(--tone-orange-br); }
.cal-legend .swatch.seats-limited { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); }
.cal-legend .swatch.seats-full { background: var(--danger-bg); border-color: var(--tone-red-br); }

/* Compact stat tiles above the grid */
.stat-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.stat-tile { display: flex; align-items: center; gap: 12px; padding: 12px; }
.stat-tile .icon {
    width: 36px; height: 36px; border-radius: 12px; display: grid; place-items: center;
    border: 1px solid; flex-shrink: 0; font-size: 14px;
}
.stat-tile .label { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.stat-tile .value { font-size: 18px; font-weight: 800; line-height: 1.1; }
.stat-tile .note { font-size: 11px; font-weight: 500; }

/*
 * TONES. `.kpi-icon.tone-*` above set the precedent; these are the same five for the
 * stat strip.
 *
 * They exist because every one of them was written inline as a hex pair — a light-mode
 * value frozen into a panel that has a dark theme, since a style attribute cannot
 * answer to `[data-theme="dark"]`. Put on the ICON where the note keeps its muted
 * colour (the booking detail), and on the TILE where the note restates the figure in
 * the same colour (the availability strip).
 */
.stat-tile .icon.tone-blue,   .stat-tile.tone-blue .icon   { background: var(--tone-blue-bg); border-color: var(--tone-blue-br); color: var(--tone-blue-fg); }
.stat-tile .icon.tone-green,  .stat-tile.tone-green .icon  { background: var(--tone-green-bg); border-color: var(--tone-green-br-soft); color: var(--tone-green-fg); }
.stat-tile .icon.tone-orange, .stat-tile.tone-orange .icon { background: var(--tone-orange-bg); border-color: var(--accent-soft-border); color: var(--tone-orange-fg); }
.stat-tile .icon.tone-amber,  .stat-tile.tone-amber .icon  { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); color: var(--tone-amber-fg); }
.stat-tile .icon.tone-red,    .stat-tile.tone-red .icon    { background: var(--danger-bg); border-color: var(--tone-red-br); color: var(--tone-red-fg); }

.stat-tile.tone-green .note  { color: var(--tone-green-fg); }
.stat-tile.tone-orange .note { color: var(--tone-orange-fg); }
.stat-tile.tone-red .note    { color: var(--tone-red-fg); }

/*
 * The one tile that is highlighted rather than merely toned: offline seats held. The
 * whole card is tinted, so its icon inverts to sit ON the tint instead of matching it,
 * and the label goes with the note.
 */
.stat-tile.is-highlight { background: var(--tone-amber-bg); border-color: var(--tone-amber-br); }
.stat-tile.is-highlight .icon { background: var(--bg-card); border-color: var(--tone-amber-br); color: var(--tone-amber-fg); }
.stat-tile.is-highlight .label,
.stat-tile.is-highlight .note { color: var(--tone-amber-fg); }

@media (max-width: 1024px) {
    .stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .stat-strip { grid-template-columns: minmax(0, 1fr); }
}

/* Availability toolbar — one compact strip, as in the reference. */
.cal-toolbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; flex-wrap: wrap; padding: 12px;
}
.cal-toolbar .group { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cal-toolbar .form-select { height: 36px; width: auto; font-size: 13px; }
/* Tab that is visibly present but not yet usable — the shuttle half of this screen
   is unbuilt, and hiding the tab would misrepresent the module as finished. */
/*
 * TABBED SETTINGS.
 *
 * Global settings is ten sections and a single Save. As one column it was a screen
 * nobody could hold in their head, and the Save button sat two thousand pixels below
 * the field most people came to change.
 *
 * The panels are HIDDEN, NOT UNMOUNTED — every field stays in the DOM, keeps its id
 * and keeps its value, because the save gathers the whole form by id in one write.
 * A tab that unmounted its fields would silently blank every setting on it.
 */
.settings-tabs { margin-bottom: 16px; }
.settings-panel > .card + .card { margin-top: 16px; }

.pill-group a.is-disabled { opacity: .45; cursor: not-allowed; }
.pill-group a.is-disabled:hover { color: var(--text-muted); }
.range-badge {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    background: var(--bg-inverse); color: var(--on-inverse);
    padding: 5px 10px; border-radius: var(--radius-pill);
    font-size: 11px; font-weight: 700; white-space: nowrap;
}
.offline-count {
    background: var(--accent-soft); border: 1px solid var(--accent-soft-border);
    color: var(--tone-orange-fg); padding: 5px 12px; border-radius: var(--radius-pill);
    font-size: 12px; font-weight: 600; white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * Service rate screens
 * ------------------------------------------------------------------ */

/* Master on/off for a whole service, sat in the page heading. */
.service-switch {
    display: flex; align-items: center; gap: 12px;
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-pill); padding: 8px 14px;
}
.service-switch .state { font-size: 12px; font-weight: 700; letter-spacing: .02em; }
.service-switch .state.is-off { color: var(--text-muted); }
.service-switch .hint { font-size: 12px; color: var(--text-muted); }

/*
 * Inline add-new panel at the foot of a list card, shaded so it reads as a
 * separate act from the rows above it — creating is not editing.
 */
.add-panel { background: var(--bg-warm); border-top: 1px solid var(--accent-soft-border); padding: 16px 18px; }

/*
 * The add panel standing on its own, ABOVE the table rather than flush inside the
 * card below it — asked for on the three Service Rates screens 2026-08-13, so the
 * commonest action on a master-data screen is the first thing under the heading
 * instead of a scroll away past every row.
 *
 * Needs its own frame: the base rule draws only a top border, because inside a card
 * the card supplies the other three sides and the corners.
 */
.add-panel.is-standalone {
    border: 1px solid var(--accent-soft-border);
    border-radius: 12px;
    margin-bottom: 16px;
}
.add-panel .title {
    display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--tone-orange-fg);
}
/*
 * One row, columns proportioned to their contents rather than six equal shares:
 * a name and an upload target need room, "Seats" holds two digits. align-items:end
 * keeps the controls on a common baseline when one label wraps to two lines.
 */
/*
 * Cells align to their START, not their end. Aligning to the end meant any column
 * with something extra underneath — a coordinate readout, a hint — pushed its own
 * label and control upward, out of line with its neighbours.
 */
.add-grid {
    display: grid;
    grid-template-columns: 1.6fr 0.7fr 1.1fr 1.1fr 1.1fr 1.8fr;
    gap: 12px;
    /* STRETCH, not start. Each cell is a label over a field, so aligning the CELLS to
       the top of the row means a label that wraps drags its own field down with it —
       and a help-dot alone makes a label 20px instead of 18px, enough to leave a row
       of inputs visibly ragged. */
    align-items: stretch;
}
/*
 * One line of fields, by giving every LABEL the same height rather than by pushing the
 * fields down.
 *
 * Pushing was tried first and was worse: `margin-top: auto` on the field bottom-aligns
 * everything after the label, so a cell carrying help text or a coordinate readout
 * below its control ended up lifting that control above its neighbours — the spread
 * went from 20px to 42px. The labels are what differ, so the labels are what to fix.
 *
 * 20px is one line plus the help-dot, which is what makes an otherwise identical label
 * 20px instead of 18px and leaves a row of inputs 2px ragged. A label long enough to
 * genuinely wrap is a column that is too narrow — widen the track instead.
 */
.add-grid > * > .form-label { min-height: 20px; }

.add-grid .wide { grid-column: span 2; }

/* Unsaved marker on a row, and the counter in the heading. */
.dirty-dot {
    display: inline-block; width: 7px; height: 7px; border-radius: 999px;
    background: #F59E0B; flex-shrink: 0;
}
.unsaved-badge {
    display: inline-flex; align-items: center; gap: 6px;
    background: #F59E0B; color: #FFF; padding: 4px 10px;
    border-radius: var(--radius-pill); font-size: 11px; font-weight: 700;
}

/*
 * Card-header toolbar (show N entries + search).
 *
 * Sat on a white card header, controls with a white fill and a hairline border
 * read as washed out — there is nothing to separate field from background. They
 * take the subtle grey fill instead, and go white on focus so the active field is
 * unmistakable.
 */
/* Pushed to the END of its row: the toolbar sits above a table whose content starts
   at the left, so its controls belong on the opposite side. This lived in the old
   Tailwind-neutralising block until 2026-08-18 and was the one rule in it that was
   not merely undoing app.css. */
.table-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 16px; flex-wrap: wrap; }
.table-toolbar label {
    display: flex; align-items: center; gap: 8px;
    font-size: 13px; color: var(--text-secondary); white-space: nowrap;
}
.table-toolbar .form-select,
.table-toolbar .form-input {
    height: 36px; width: auto; font-size: 13px;
    background-color: var(--bg-card); border-color: var(--border-strong);
}
/* Room for the caret: the old `padding-inline-end: 8px` sat the arrow on top of the
   number. Smaller than the base 32px because this select holds two digits. */
.table-toolbar .form-select { min-width: 76px; padding-inline-end: 28px; background-position: right 9px center; }
[dir="rtl"] .table-toolbar .form-select { background-position: left 9px center; }
.table-toolbar .form-select:focus,
.table-toolbar .form-input:focus { border-color: var(--accent); }
.table-toolbar .search-field .form-input { min-width: 220px; }

/* Small inputs used by row-edit mode inside a table cell. */
.table .form-input, .table .form-select { height: 32px; font-size: 13px; }
.cell-narrow { width: 74px; }
.cell-mid { width: 116px; }
.cell-wide { width: 180px; }

/* Logo thumbnail in a rate row. */
.thumb {
    width: 36px; height: 36px; border-radius: 8px; object-fit: contain;
    background: var(--bg-card); border: 1px solid var(--border); padding: 3px; flex-shrink: 0;
}

/*
 * Six fields stop fitting before the viewport reaches 1280: at 1366 the longest label
 * on the charter panel already wraps, which drops its field below the rest of the row.
 * The four- and five-up variants are still comfortable there, and their own selectors
 * out-specify this one, so they keep the 1280 step below.
 */
@media (max-width: 1400px) {
    .add-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 1280px) {
    .add-grid,
    .add-grid.is-units,
    .add-grid.is-languages,
    .add-grid.is-currency { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .add-grid,
    .add-grid.is-units,
    .add-grid.is-languages,
    .add-grid.is-currency { grid-template-columns: minmax(0, 1fr); }
    .add-grid .wide { grid-column: auto; }
}

/* ------------------------------------------------------------------ *
 * Image upload (public/js/image-upload.js)
 * ------------------------------------------------------------------ */

/*
 * Full width, and no fixed height.
 *
 * Both were wrong and showed up worst on Global Settings, where these sit in a form
 * beside ordinary inputs: `width: 82%` left every uploader stopping short of the
 * fields above and below it, and `height: 40px` was SHORTER than the 44px thumbnail
 * inside it, so the preview burst out of its own dashed box. The height now comes
 * from the content — thumbnail plus padding.
 */
.uploader-drop {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 10px 12px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-field);
    background: var(--bg-subtle);
    cursor: pointer;
    transition: .15s;
    text-align: start;
    position: relative;
}
.uploader-drop:hover,
.uploader-drop:focus-visible { border-color: var(--accent); background: var(--accent-soft); }
/* Dashed → solid while a file is over the target: the border is the only thing
   that can confirm the drop will land here. */
.uploader-drop.is-drag {
    border-style: solid; border-color: var(--accent);
    background: var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-soft);
}

.uploader-thumb {
    width: 44px; height: 44px; flex-shrink: 0; border-radius: 8px;
    background: var(--bg-card); border: 1px solid var(--border);
    display: grid; place-items: center; overflow: hidden;
}
.uploader-thumb img { width: 100%; height: 100%; object-fit: contain; }
.uploader-thumb.is-empty { color: var(--text-muted); font-size: 16px; }

.uploader-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.uploader-title { font-size: 13px; font-weight: 600; }
.uploader-hint { font-size: 12px; color: var(--text-muted); }
.uploader-hint.is-error { color: var(--danger); font-weight: 500; }

.uploader-clear {
    margin-inline-start: auto; flex-shrink: 0;
    width: 28px; height: 28px; display: grid; place-items: center;
    border-radius: 8px; border: 1px solid var(--border);
    background: var(--bg-card); color: var(--text-secondary); cursor: pointer;
}
.uploader-clear:hover { border-color: var(--danger-border); color: var(--danger); background: var(--danger-bg); }

/* Compact variant for inside a table row. */
.uploader.is-compact .uploader-drop { padding: 6px 8px; gap: 8px; }
.uploader.is-compact .uploader-thumb { width: 32px; height: 32px; }
.uploader.is-compact .uploader-title { font-size: 12px; }
.uploader.is-compact .uploader-hint { display: none; }

/* ------------------------------------------------------------------ *
 * Toasts and confirmation dialogs (public/js/notify.js)
 * ------------------------------------------------------------------ */

.toast-stack {
    position: fixed; top: 16px; inset-inline-end: 16px; z-index: 60;
    display: flex; flex-direction: column; gap: 10px;
    width: 360px; max-width: calc(100vw - 32px);
    /* The stack must not swallow clicks on the page behind it; each toast turns
       pointer events back on for itself. */
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    display: flex; align-items: flex-start; gap: 12px;
    padding: 14px; border-radius: var(--radius-field);
    background: var(--bg-card); border: 1px solid var(--border);
    box-shadow: 0 10px 30px rgba(0,0,0,.12);
    opacity: 0; transform: translateY(-8px);
    transition: opacity .18s ease, transform .18s ease;
}
.toast.is-in { opacity: 1; transform: none; }
.toast.is-leaving { opacity: 0; transform: translateY(-8px); }

.toast-icon {
    width: 32px; height: 32px; flex-shrink: 0; border-radius: 999px;
    display: grid; place-items: center; color: #FFF; font-size: 13px;
}
.toast.is-success { border-color: var(--tone-green-br-soft); }
.toast.is-success .toast-icon { background: #10B981; }
.toast.is-error { border-color: var(--tone-red-br); }
.toast.is-error .toast-icon { background: var(--danger); }
.toast.is-warning { border-color: var(--tone-amber-br); }
.toast.is-warning .toast-icon { background: #F59E0B; }
.toast.is-info { border-color: var(--tone-blue-br); }
.toast.is-info .toast-icon { background: #3B82F6; }

.toast-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.toast-title { font-size: 13.5px; font-weight: 700; line-height: 1.3; }
.toast-text { font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; word-break: break-word; }

.toast-close {
    flex-shrink: 0; width: 24px; height: 24px; display: grid; place-items: center;
    border: 0; background: transparent; color: var(--text-muted);
    border-radius: 6px; cursor: pointer; font-size: 12px;
}
.toast-close:hover { background: var(--bg-hover); color: var(--text-primary); }

/* Confirmation dialog */
.dialog { max-width: 420px; }
.dialog-body { padding: 28px 24px 20px; text-align: center; }
.dialog-icon {
    width: 56px; height: 56px; margin: 0 auto 16px; border-radius: 999px;
    display: grid; place-items: center; font-size: 22px;
}
.dialog-icon.is-danger { background: var(--danger-bg); color: var(--danger); }
.dialog-icon.is-accent { background: var(--accent-soft); color: var(--tone-orange-fg); }
.dialog-title { font-size: 17px; font-weight: 700; line-height: 1.3; }
.dialog-text { margin-top: 8px; font-size: 13.5px; color: var(--text-secondary); line-height: 1.55; }
.dialog .modal-footer { justify-content: center; }
.dialog .modal-footer .btn { min-width: 110px; }

@media (max-width: 640px) {
    .toast-stack { inset-inline: 16px; width: auto; }
}

/*
 * Shuttle add panel — seven fields since a rate became a ROUTE with its own
 * timetable: airport, direction, drop point, departures, seats, fare, pick-on-map.
 */
/*
 * SIX fields, six tracks. It declared seven until 2026-08-18 — the seventh was left
 * behind when seats-per-departure came off this screen, and it both stole 185px into
 * an empty column and squeezed Fix cost to 104px, where its label wrapped. Fix cost
 * and Drop point location are widened here: they carry the longest labels on the row.
 */
/* SEVEN fields since the city group joined them, 2026-08-25. Track count must equal
   field count: a template with more tracks than fields leaves dead columns and squeezes
   the real ones until their labels wrap. */
.add-grid.is-shuttle { grid-template-columns: 1.1fr 1fr 1.2fr 1fr 1fr 0.85fr 1.15fr; }

/* Two fields, so two tracks — the base six-track grid would leave four dead columns. */
.add-grid.is-city-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Fewer fields than the base six-track grid. Without a template of their own these
   three left up to two dead columns and 469px of empty row. */
.add-grid.is-units { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.add-grid.is-languages { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.add-grid.is-currency { grid-template-columns: repeat(5, minmax(0, 1fr)); }
/* Stepped at 1500 rather than 1400 to match .is-airports: seven fields need more room
   than six, and the longest label here wraps before 1400 is reached. */
@media (max-width: 1500px) {
    .add-grid.is-shuttle { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .add-grid.is-city-groups { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
    .add-grid.is-shuttle { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .add-grid.is-shuttle { grid-template-columns: minmax(0, 1fr); }
}


/* ------------------------------------------------------------------ *
 * Searchable select (public/js/searchable-select.js)
 *
 * The Select2 behaviour without jQuery or Select2. Its markup is built in JS, so
 * every class here has a counterpart in that file — rename one and the control
 * renders unstyled with no error.
 *
 * The panel is positioned `fixed` from JS, because this control is used inside
 * `.card` (overflow: hidden, for the rounded corners) and inside `.table-wrap`
 * (overflow-x: auto). Absolute positioning was clipped by both, and the option you
 * wanted was simply unreachable.
 * ------------------------------------------------------------------ */

.ss-toggle {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    width: 100%; min-height: 38px; padding: 8px 12px;
    border: 1px solid var(--border-strong); border-radius: var(--radius-field);
    background: var(--bg-card); color: var(--text-primary);
    font-family: inherit; font-size: 13.5px; text-align: start; cursor: pointer;
    transition: .15s;
}
.ss-toggle:hover:not(:disabled) { border-color: var(--border-hover); }
.ss-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ss-toggle:disabled { background: var(--bg-hover); color: var(--text-muted); cursor: default; }

.ss-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Nothing chosen yet: the placeholder must read as a prompt, not as a value. */
.ss-label.is-placeholder { color: var(--text-muted); }
.ss-chevron { flex: 0 0 auto; font-size: 10px; color: var(--text-muted); }
/* The country picker's clear button — a chevron-sized control, but a "×" glyph
   rather than an icon font, so it needs its own size and a pointer. */
.ss-clear { flex: 0 0 auto; font-size: 14px; line-height: 1; color: var(--text-muted); cursor: pointer; }
.ss-clear:hover { color: var(--text-primary); }

.ss-panel {
    z-index: 80; display: flex; flex-direction: column;
    /*
     * THE CAP IS LOAD-BEARING, and it was missing. `.ss-list` already asks to scroll
     * (`overflow-y: auto`) but a flex column with no ceiling has nothing to scroll
     * INSIDE — so the 243-country picker rendered a panel 8,202px tall running off the
     * bottom of the document, and the search box it is opened for scrolled away with
     * it. Measured on the settings screen 2026-08-19; more style.css truncation
     * damage, and it affected every searchable select with a long list, not just the
     * phone one.
     */
    max-height: 300px;
    background: var(--bg-card); border: 1px solid var(--border-strong);
    border-radius: var(--radius-field); box-shadow: 0 12px 28px rgba(0,0,0,.14);
    overflow: hidden;
}
.ss-search { padding: 8px; border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.ss-search .form-input { height: 34px; }

.ss-list { list-style: none; margin: 0; padding: 4px; overflow-y: auto; flex: 1 1 auto; }
.ss-option {
    display: flex; align-items: center; gap: 8px; width: 100%;
    padding: 7px 10px; border: 0; border-radius: 8px;
    background: transparent; color: var(--text-primary);
    font-family: inherit; font-size: 13px; text-align: start; cursor: pointer;
}
.ss-option:hover { background: var(--bg-hover); }
.ss-option.is-current { background: var(--accent-soft); color: var(--text-primary); font-weight: 600; }
.ss-option-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A right-aligned hint — the fare beside a drop point, "no longer available"
   beside an option kept only so saving cannot silently blank it. */
.ss-option-note { flex: 0 0 auto; font-size: 11.5px; color: var(--text-muted); }
.ss-empty { padding: 12px 10px; font-size: 12.5px; color: var(--text-muted); text-align: center; }

/* The flag in the phone and country pickers (public/js/country-select.js). */
.ss-flag { flex: 0 0 auto; width: 20px; height: 14px; border-radius: 3px; object-fit: cover; }

/* ------------------------------------------------------------------ *
 * Phone field — flag + dial code joined to the national number
 * ------------------------------------------------------------------ */

/*
 * `.picker-wrap`, `.phone-field` and `.dial` had NO RULES AT ALL. All three are built
 * from JavaScript by public/js/country-select.js, so the Blade style ratchet cannot
 * see them and nothing failed — the field simply rendered as a raw native button
 * (2px outset border, grey background, square corners) stacked ABOVE a full-width
 * number input, 79px tall instead of 38.
 *
 * More style.css truncation damage from 2026-08-17, and the third thing it has cost
 * after the theme toggle and the auth brand glow. The `ss-*` half of this same file
 * survived, which is why every other country picker in the panel looks right and only
 * the phone one did not.
 *
 * The two controls read as ONE field: the dial button carries the leading corners, the
 * number carries the trailing ones, and they share a single border down the middle
 * rather than each drawing their own. Logical properties throughout — this field is on
 * the settings screen, which renders RTL in Arabic.
 */
.picker-wrap { position: relative; }

.phone-field { display: flex; align-items: stretch; width: 100%; }

.phone-field .dial {
    display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
    height: 38px; padding: 0 10px;
    border: 1px solid var(--border-strong); border-inline-end: 0;
    border-start-start-radius: var(--radius-field);
    border-end-start-radius: var(--radius-field);
    border-start-end-radius: 0; border-end-end-radius: 0;
    background: var(--bg-subtle); color: var(--text-primary);
    font-family: inherit; font-size: 13.5px; line-height: 1; cursor: pointer;
    transition: .15s; white-space: nowrap;
}
.phone-field .dial:hover { border-color: var(--border-hover); }
.phone-field .dial:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The number takes the rest of the row and gives up its leading corners to the button
   it is joined to. */
.phone-field .form-input {
    flex: 1 1 auto; min-width: 0;
    border-start-start-radius: 0; border-end-start-radius: 0;
}
/*
 * A focused number must not lose the seam it shares with the dial button. `.form-input`
 * only recolours its OWN border on focus, which on a joined control leaves the shared
 * edge the old grey and reads as a crack down the middle of one field.
 */
.phone-field .form-input:focus { position: relative; z-index: 1; }
.phone-field:focus-within .dial { border-color: var(--accent); }

/*
 * Anchored variant: the country and phone pickers are never inside an
 * overflow-hidden card, so they can position themselves in the normal way and
 * follow the field on scroll.
 */
/*
 * FIXED, not absolute — `.card` sets `overflow: hidden`, and every one of these
 * pickers lives in one, so an absolute panel is clipped at the card's edge. The short
 * lists got away with it; the 243-country phone panel was cut to 44 visible pixels.
 * `top`, `left` and `width` are written by the `place()` function in
 * public/js/country-select.js, which also flips it above the field when there is more
 * room there and keeps it inside the viewport.
 */
.ss-panel.is-anchored { position: fixed; }



/* ------------------------------------------------------------------ *
 * Field add-ons: icons, the password reveal, the map button
 * ------------------------------------------------------------------ */

/*
 * A field with something inside it. The input carries the padding so the glyph sits
 * over empty space rather than over text.
 */
.field-icon { position: relative; display: block; }
.field-icon > i {
    position: absolute; inset-inline-start: 12px; top: 50%; transform: translateY(-50%);
    font-size: 13px; color: var(--text-muted); pointer-events: none;
}
.field-icon > i ~ .form-input { padding-inline-start: 36px; }

/*
 * The password reveal. Delegated from public/js/app.js via
 * `[data-reveal="<input id>"]`, so it needs no per-page script and works in both
 * layouts.
 */
.form-input.has-reveal { padding-inline-end: 40px; }
.reveal-btn {
    position: absolute; inset-inline-end: 6px; top: 50%; transform: translateY(-50%);
    width: 28px; height: 28px; border: 0; border-radius: 8px;
    background: transparent; color: var(--text-muted);
    display: grid; place-items: center; cursor: pointer; font-size: 13px;
}
.reveal-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.reveal-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/*
 * An address input with its pick-on-map button joined to the end. One row, so the
 * button reads as part of the field rather than as a separate control.
 */
.address-field { display: flex; align-items: stretch; gap: 8px; }
.address-field .form-input { flex: 1 1 auto; min-width: 0; }
.address-field .btn { flex: 0 0 auto; }

/* A checkbox and its label on one line, aligned with the fields beside it. */
.check-field { display: flex; align-items: center; min-height: 38px; }
.check-field label { display: flex; align-items: center; gap: 8px; font-size: 13.5px; cursor: pointer; }

/* The coordinate readout under the pick-on-map button in an add panel. */
.coord-status { margin-top: 6px; font-size: 11.5px; color: var(--text-muted); }

/* Inline spinner on an accent button — white on the accent, unlike .spinner-inline. */
.spinner-sm {
    display: inline-block; width: 16px; height: 16px; border-radius: 999px;
    border: 2px solid rgba(255,255,255,.35); border-top-color: #FFF;
    animation: spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .spinner-sm { animation: none; }
}

/* ------------------------------------------------------------------ *
 * Vehicle unit photos (vehicle_units/index.blade.php)
 * ------------------------------------------------------------------ */

/*
 * images[0] IS the cover — there is no separate flag, because a flag and an order
 * can disagree and the website reads the first element.
 */
/*
 * A flex box with no alignment puts its child at the start, which is why the
 * placeholder icon sat in the corner rather than in the middle. `position` is here
 * for the count badge below, which is positioned against this box.
 */
.unit-thumb {
    display: inline-flex; align-items: center; justify-content: center;
    position: relative;
    width: 44px; height: 44px; flex: 0 0 auto;
    border-radius: 10px; overflow: hidden; border: 1px solid var(--border);
    background: var(--bg-hover);
}
.unit-thumb img { width: 100%; height: 100%; object-fit: cover; }
.unit-thumb.is-empty { border-style: dashed; }

/*
 * The icon needs its own size and colour: it inherits the row's font size otherwise,
 * which is a 13px glyph adrift in a 44px box. Muted, because "no photo yet" is an
 * absence — it should not draw the eye harder than the real thumbnails beside it.
 */
.unit-thumb.is-empty i {
    font-size: 18px; line-height: 1; color: var(--text-muted);
}

/*
 * "2" over the corner of the cover, for a unit with more photos behind it.
 * This class was in the markup with NO rule anywhere, so the number rendered as bare
 * text on top of the image.
 */
.unit-thumb .count {
    position: absolute; inset-block-end: 0; inset-inline-end: 0;
    min-width: 16px; padding: 0 3px;
    font-size: 10px; line-height: 16px; text-align: center;
    color: #fff; background: rgba(0, 0, 0, .62);
    border-start-start-radius: 6px;
}

.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
.photo-tile {
    position: relative; border: 1px solid var(--border); border-radius: 12px;
    overflow: hidden; background: var(--bg-hover); aspect-ratio: 4 / 3;
}
.photo-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-tile .actions {
    position: absolute; inset-inline: 0; bottom: 0; display: flex; gap: 4px;
    padding: 6px; background: linear-gradient(transparent, rgba(0,0,0,.55));
}
.photo-tile .actions .btn-icon { background: rgba(255,255,255,.92); color: #1C1917; }
/* The first tile is the cover, and says so rather than relying on position alone. */
.photo-tile.is-cover { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* ------------------------------------------------------------------ *
 * Sidebar rail
 *
 * `.is-rail` on <html> narrows the sidebar to an icon column. DESKTOP ONLY: below
 * 1025px the sidebar is already a drawer, and railing a drawer is meaningless.
 *
 * Every nav item carries a `title`, because the label is exactly what is hidden.
 * ------------------------------------------------------------------ */

@media (min-width: 1025px) {
    :root.is-rail { --sidebar-width: 76px; }

    .is-rail .sidebar { padding-inline: 10px; }
    .is-rail .nav-item { justify-content: center; padding-inline: 0; gap: 0; }
    .is-rail .nav-item .label,
    .is-rail .nav-item .nav-badge,
    .is-rail .sidebar-help { display: none; }

    /* A group heading with no room for its words becomes a rule instead. */
    .is-rail .nav-group-label { font-size: 0; height: 1px; padding: 0; margin: 10px 6px; background: var(--border); }
    .is-rail .nav-group-label .chevron { display: none; }

    /* Groups are forced open: a collapsed group in a rail would be an icon column
       that hides icons, with nothing to say why. */
    .is-rail .nav-group > .nav-items { display: flex !important; }

    /*
     * The active marker becomes a full fill. The 4px bar reads as an accident at
     * this width — it sits hard against the icon with nothing either side.
     */
    .is-rail .nav-item.is-active::before { display: none; }

    /*
     * The collapse toggle steps just outside the rail: a 44px logo and a 34px
     * button cannot share 76px, and overlapping them looks like a bug.
     */
    .is-rail .sidebar-rail-toggle { inset-inline-end: -46px; }
}

/*
 * The collapse toggle, pinned to the brand block's trailing edge.
 *
 * ABSOLUTE, not in the flow: in the flow it takes half its own width out of the
 * block, and the logo — which is centred on the sidebar's centre line — drifts off
 * it by 17px. Close enough to read as a mistake rather than a decision.
 */
.sidebar-rail-toggle {
    position: absolute; inset-inline-end: 8px; top: 50%; transform: translateY(-50%);
    z-index: 2;
}

/* Desktop only: below 1025px the sidebar is a drawer, and railing a drawer is
   meaningless — the drawer's own toggle does that job. */
@media (max-width: 1024px) {
    .sidebar-rail-toggle { display: none; }
}

/* ------------------------------------------------------------------ *
 * Signed-out screens (layouts/guest.blade.php + auth/*)
 *
 * A split screen: brand panel on the left, the card on the right. Below 900px the
 * brand panel is dropped entirely rather than stacked — it is atmosphere, and on a
 * phone it would push the form below the fold.
 * ------------------------------------------------------------------ */

.auth-shell { min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1fr); background: var(--bg-page); }

@media (min-width: 900px) {
    .auth-shell { grid-template-columns: 1.05fr 1fr; }
}

.auth-brand {
    display: none; position: relative; flex-direction: column; justify-content: space-between;
    padding: 48px; background: var(--bg-inverse); color: var(--on-inverse);
    overflow: hidden;
}
@media (min-width: 900px) {
    .auth-brand { display: flex; }
}
/*
 * The backdrop.
 *
 * The reference loads a stock photograph from an external image host. This is the one
 * page that has to render when nothing else is reachable, so it is two accent glows in
 * pure CSS instead — no request, no layout shift, nothing to fail. The rebuilt
 * stylesheet dropped this and left `position: relative` behind with nothing to
 * position, which is how a flat panel got shipped.
 */
.auth-brand::before {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background:
        radial-gradient(60% 45% at 12% 8%, rgba(255, 107, 17, .22), transparent 70%),
        radial-gradient(55% 45% at 92% 96%, rgba(255, 107, 17, .13), transparent 70%);
}
/* Above the glow: without a stacking position of their own the panel's blocks sit
   under an absolutely positioned sibling. */
.auth-brand > * { position: relative; }

/* The panel's body, held clear of the logo above it. */
.auth-brand-body { margin-top: 44px; }
.auth-logo { display: flex; align-items: center; gap: 12px; }
/*
 * BIG on the brand panel, by the user's instruction 2026-08-19.
 *
 * This half of the screen exists to say whose panel this is, and at 44px the logo was
 * the smallest thing on it — smaller than the headline it sat above. The cap is a
 * height AND a width because a client logo can be either a square mark or a long
 * wordmark, and only constraining one lets the other run off the panel.
 */
.auth-logo img { height: 88px; width: auto; max-width: 320px; object-fit: contain; object-position: left; }
/* Shown when no logo has been published yet — the panel must not open with a gap. */
.auth-logo-fallback {
    width: 88px; height: 88px; border-radius: 22px; display: grid; place-items: center;
    background: var(--accent); color: #FFF; font-size: 36px;
}
.auth-headline { font-size: 30px; font-weight: 700; line-height: 1.15; letter-spacing: -.02em; margin: 0 0 12px; }
.auth-lede { font-size: 14.5px; line-height: 1.6; opacity: .78; max-width: 42ch; margin: 0; }
.auth-tag {
    display: inline-flex; align-items: center; gap: 8px; margin-bottom: 20px;
    padding: 5px 12px; border-radius: var(--radius-pill);
    background: rgba(255,255,255,.1); font-size: 12px; font-weight: 600;
}
.auth-tag .dot { width: 7px; height: 7px; border-radius: 999px; background: var(--accent); }

.auth-stats { display: flex; flex-wrap: wrap; gap: 28px; }
.auth-stat .k { display: block; font-size: 20px; font-weight: 700; }
.auth-stat .l { display: block; font-size: 12px; opacity: .7; margin-top: 2px; }
.auth-brand-foot { font-size: 12px; opacity: .6; }

/*
 * A COLUMN, not a row.
 *
 * `.auth-main` holds two children — the card and the foot note — so as a flex row it
 * put the note BESIDE the card, vertically centred against it, and made the two share
 * the column's width. Measured in the browser 2026-08-18: the card rendered 361px wide
 * instead of the 420px `.auth-main-inner` asks for. Rebuild damage; the direction was
 * lost when style.css was reconstructed on the 17th.
 */
.auth-main {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 32px 20px;
}
.auth-main-inner { width: 100%; max-width: 420px; }
/* The logo only appears here once the brand panel is gone. */
.auth-mobile-logo { display: flex; justify-content: center; margin-bottom: 24px; }
.auth-mobile-logo img { max-height: 44px; width: auto; object-fit: contain; }
@media (min-width: 900px) {
    .auth-mobile-logo { display: none; }
}
.auth-card-wrap { width: 100%; }
/* `.auth-foot-note` lived here. Its markup was removed from layouts/guest on
   2026-08-19; the rule went with it rather than being left behind as a selector
   matching nothing. */

.auth-card {
    background: var(--bg-card); border: 1px solid var(--border);
    border-radius: var(--radius-card); box-shadow: 0 8px 24px rgba(0,0,0,.06);
    overflow: hidden;
}
.auth-card-head { padding: 28px 28px 4px; }
.auth-card-head h1 { font-size: 21px; font-weight: 700; letter-spacing: -.02em; margin: 0 0 6px; }
.auth-card-head p { font-size: 13.5px; color: var(--text-muted); margin: 0; }

.auth-form { display: flex; flex-direction: column; gap: 16px; padding: 20px 28px 28px; }
/* Label on one side, "forgot password?" on the other, sitting directly above their
   field — so the label's own bottom margin is cancelled and the row carries it. */
/* `.auth-row` paired the password label with a "forgot password?" link on the same
   line. The link was removed 2026-08-19, so the label is a plain label again and the
   row had nothing left to lay out. */
.auth-check { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
.auth-submit { width: 100%; justify-content: center; height: 42px; margin-top: 4px; }
.auth-link { color: var(--accent); font-weight: 600; }
.auth-link:hover { color: var(--accent-hover); }

.auth-card-foot {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    padding: 16px 28px; border-top: 1px solid var(--border);
    background: var(--bg-subtle); font-size: 13px;
}/* The "resend the verification email" control is a FORM SUBMIT, not a link — it
   posts. It should still read as the link beside it. */
button.auth-link {
    background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
}

/* ------------------------------------------------------------------ *
 * Airports add panel
 * ------------------------------------------------------------------ */

/* Seven fields: name, code, city, terminals, lat, lng, pick-on-map. */
/* Latitude and longitude give width back to the map field, whose label is the
   longest on the row and was wrapping at 185px and dropping its button 18px
   below every other control. */
.add-grid.is-airports { grid-template-columns: 1.5fr 0.6fr 1fr 1.2fr 0.75fr 0.75fr 1.5fr; }
/* Seven fields need more room than six, so this steps down first — at 1366 the map
   field's label wraps and takes its button 18px below the row. */
@media (max-width: 1500px) {
    .add-grid.is-airports { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 1280px) {
    .add-grid.is-airports { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .add-grid.is-airports { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------------ *
 * Availability cell chooser (#cell-modal)
 *
 * Clicking a free day offers SELLING it first and blocking second — the reference
 * makes the offline booking the primary action, and it is the commonest thing an
 * operator on the phone actually needs. Each choice is a full-width row with its
 * consequence spelled out underneath, because "Block" alone does not say that the
 * vehicle stops being sellable.
 * ------------------------------------------------------------------ */

.cell-actions { display: flex; flex-direction: column; gap: 8px; }
.cell-action {
    display: flex; align-items: flex-start; gap: 12px; width: 100%;
    padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px;
    background: var(--bg-card); color: var(--text-primary);
    font-family: inherit; text-align: start; cursor: pointer; transition: .15s;
}
.cell-action:hover { border-color: var(--border-hover); background: var(--bg-subtle); }
.cell-action > i { flex: 0 0 auto; width: 18px; margin-top: 2px; text-align: center; color: var(--text-muted); }
.cell-action span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cell-action strong { font-size: 13.5px; font-weight: 600; }
.cell-action small { font-size: 11.5px; color: var(--text-muted); line-height: 1.4; }

/* The action an operator wants most often, marked without shouting. */
.cell-action.is-primary { border-color: var(--accent-soft-border); background: var(--accent-soft); }
.cell-action.is-primary > i { color: var(--accent); }
.cell-action.is-primary:hover { border-color: var(--accent); }

/* Lifting a block undoes an audited decision, so it reads as the weightier choice. */
.cell-action.is-danger > i { color: var(--danger); }
.cell-action.is-danger:hover { border-color: var(--danger-border); background: var(--danger-bg); }

/* ------------------------------------------------------------------
 * Day calendar (public/js/day-calendar.js)
 *
 * The month grid the booking forms pick a travel date on, modelled on the
 * customer app's departure-date screen: every day carries its own count and
 * colour, so a month is read at a glance instead of a date being typed and
 * then refused.
 *
 * `.daycal-*`, NOT `.cal-*`: the availability screen owns that namespace (see
 * `.cal-head` / `.cal-row` further up) and it is a different calendar entirely —
 * one row per vehicle across fourteen days, against this one month of days.
 * Sharing the prefix is how the heat grid and the availability grid once ended
 * up fighting over `gap`.
 * ------------------------------------------------------------------ */

.daycal { display: flex; flex-direction: column; gap: 10px; }

.daycal-head {
    display: flex; align-items: center; justify-content: center; gap: 14px;
}
.daycal-month { font-size: 14px; font-weight: 700; letter-spacing: .01em; }
.daycal-nav {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0;
    border: 1px solid var(--border); border-radius: 8px;
    background: var(--bg-card); color: var(--text-secondary);
    font-size: 11px; cursor: pointer; transition: .15s;
}
.daycal-nav:hover { border-color: var(--border-hover); background: var(--bg-subtle); color: var(--text-primary); }

/* The weekday strip and the day grid are two grids with the SAME template, so a
   column heading sits over its column whatever the month's lead-in is. */
.daycal-week, .daycal-grid {
    display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px;
}
.daycal-dow {
    text-align: center; font-size: 11px; font-weight: 700;
    color: var(--text-muted); text-transform: uppercase;
}

.daycal-cell {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px; min-height: 58px; padding: 6px 4px;
    border: 1px solid var(--border); border-radius: 10px;
    background: var(--bg-card); color: var(--text-primary);
    font-family: inherit; cursor: pointer; transition: .15s;
}
.daycal-cell:hover:not(:disabled) { border-color: var(--border-hover); background: var(--bg-subtle); }
.daycal-cell:disabled { cursor: not-allowed; }
.daycal-dom { font-size: 15px; font-weight: 700; line-height: 1.1; }
.daycal-note {
    font-size: 9.5px; font-weight: 600; line-height: 1.2;
    text-align: center; overflow: hidden;
}

/* A lead-in day from the previous month: it holds the column, and nothing else. */
.daycal-cell.is-empty { border: 0; background: none; cursor: default; min-height: 0; }

/* Nothing is known about this day yet — no route chosen, so no pool to count.
   Deliberately NOT green: "we have not looked" must not read as "there is room". */
.daycal-cell.is-blank { background: var(--bg-card); }

.daycal-cell.is-available {
    border-color: var(--tone-green-br); background: var(--tone-green-bg); color: var(--tone-green-fg);
}
.daycal-cell.is-limited {
    border-color: var(--tone-amber-br); background: var(--tone-amber-bg); color: var(--tone-amber-fg);
}
.daycal-cell.is-full {
    border-color: var(--tone-red-br); background: var(--danger-bg); color: var(--tone-red-fg);
}
/* Nothing runs that day, or the departure was cancelled. Grey rather than red:
   it is an absence, not a sell-out, and the two need different answers. */
.daycal-cell.is-closed,
.daycal-cell.is-past {
    border-color: var(--border); background: var(--bg-subtle); color: var(--text-muted);
}

/* Today is marked even when it carries a count, so the month has an anchor. */
.daycal-cell.is-today { border-color: var(--accent); }

/* The chosen day wins over every state colour — it is the one thing the operator
   is looking for when they come back to the field. */
.daycal-cell.is-selected {
    border-color: var(--bg-inverse); background: var(--bg-inverse); color: var(--on-inverse);
}
.daycal-cell.is-selected .daycal-note { color: var(--on-inverse); opacity: .8; }

.daycal-legend {
    display: flex; flex-wrap: wrap; gap: 6px 16px;
    padding-top: 8px; border-top: 1px solid var(--border);
}
.daycal-key {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 11px; color: var(--text-secondary);
}
.daycal-swatch {
    width: 10px; height: 10px; border-radius: 3px;
    border: 1px solid var(--border); background: var(--bg-card);
}
.daycal-swatch.is-available { border-color: var(--tone-green-br); background: var(--tone-green-bg); }
.daycal-swatch.is-limited { border-color: var(--tone-amber-br); background: var(--tone-amber-bg); }
.daycal-swatch.is-full { border-color: var(--tone-red-br); background: var(--danger-bg); }
.daycal-swatch.is-closed { border-color: var(--border); background: var(--bg-subtle); }

/* ------------------------------------------------------------------
 * The date group the booking forms move between the two service blocks
 *
 * It holds the travel date, the return date, the pickup time (edit only) and
 * the trip type. Which of those show, and in what order, is the SERVICE's —
 * one `order` per field rather than a second copy of markup carrying ids
 * that cannot be duplicated.
 *
 * The two calendars sit HALF AND HALF, at the client's request (2026-08-25),
 * which is what the two-column grid is for.
 * ------------------------------------------------------------------ */

/* Shuttle: the departure first — a seat belongs to a run, so the month cannot
   count until the run is known — then the date, then the trip type. There is no
   return leg to pick. */
.date-fields .date-field-time { order: 1; }
.date-fields .date-field-day { order: 2; }
.date-fields .date-field-trip { order: 3; }
.date-fields .date-field-return { display: none; }

/* Charter: the dates lead the group (it already sits after the vehicle), and the
   return date takes the other half of the row. */
.date-fields.is-charter .date-field-day { order: 1; }
.date-fields.is-charter .date-field-return { display: block; order: 2; }
.date-fields.is-charter .date-field-time { order: 3; }
.date-fields.is-charter .date-field-trip { order: 4; }

/* A phone gives each day about 36px, which is too little for "8 seats left" at the
   desktop size — it wraps to three lines and the row grows. The count stays (it is
   the whole point of the grid) and the type comes down to meet the column. */
@media (max-width: 640px) {
    .daycal-week, .daycal-grid { gap: 4px; }
    .daycal-cell { min-height: 52px; padding: 4px 2px; border-radius: 8px; }
    .daycal-dom { font-size: 13px; }
    .daycal-note { font-size: 8.5px; }
}

/* ------------------------------------------------------------------
 * Drop-off list (public/js/dropoff-list.js)
 *
 * A charter trip can stop more than once — the client's note of 2026-08-25 —
 * so the single address field became a list of them. Rows stack; the map and
 * remove buttons sit on the field itself through `.address-field`.
 * ------------------------------------------------------------------ */

.dropoff-list { display: flex; flex-direction: column; gap: 12px; }
.dropoff-row { display: flex; flex-direction: column; gap: 4px; }
/* The add button belongs to the list, not to a row, so it is spaced from the
   last one rather than from the field above it. */
.dropoff-add { align-self: flex-start; margin-top: 4px; }
