/* DEMO BUILD — the demos' standalone CSS. Palette inherited from
   evaluate.css (same product, same door family); stage styles are ours. */

:root {
    --dm-navy: #0b2545;
    --dm-navy-2: #13315c;
    --dm-teal: #0d9488;
    --dm-teal-dark: #0f766e;
    --dm-teal-light: #2dd4bf;
    --dm-ink: #1e293b;
    --dm-muted: #64748b;
    --dm-line: #e2e8f0;
    --dm-red: #dc2626;
    --dm-amber: #d97706;
    --dm-font: -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; }

.dm-body {
    margin: 0;
    min-height: 100vh;
    font-family: var(--dm-font);
    color: var(--dm-ink);
    background: linear-gradient(160deg, var(--dm-navy) 0%, var(--dm-navy-2) 55%, #164e63 100%);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 24px 16px 48px;
}

.dm-wrap { width: 100%; max-width: 560px; }

/* ── the shell background (S171) ──────────────────────────────────────────
   One image per persona, behind everything, under a scrim heavy enough that
   the panels stay readable whatever the picture turns out to be. No image on
   disk = no background rule matches = the body gradient shows through, which
   is exactly how all four looked before. Nothing breaks on a missing file. */
.dm-bg {
    position: fixed;
    inset: 0;
    z-index: -1;
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;
    pointer-events: none;
}
/* The scrim. Tuned dark: these are photographic port scenes behind white
   cards, and legibility beats the picture every time. */
.dm-bg::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(160deg, rgba(11, 37, 69, 0.88) 0%, rgba(19, 49, 92, 0.86) 55%, rgba(22, 78, 99, 0.88) 100%);
}

/* 640 for phones, 1080 where the viewport or the pixel ratio earns it — the
   same two-width pattern /evaluate uses, expressed as media queries because
   there is no step event to hang a JS upgrade on. */
.dm-bg--agent      { background-image: url('/images/demo/agent-640.webp'); }
.dm-bg--pilot      { background-image: url('/images/demo/pilot-640.webp'); }
.dm-bg--dispatcher { background-image: url('/images/demo/dispatcher-640.webp'); }
.dm-bg--admin      { background-image: url('/images/demo/admin-640.webp'); }

@media (min-width: 720px), (min-resolution: 2dppx) {
    .dm-bg--agent      { background-image: url('/images/demo/agent-1080.webp'); }
    .dm-bg--pilot      { background-image: url('/images/demo/pilot-1080.webp'); }
    .dm-bg--dispatcher { background-image: url('/images/demo/dispatcher-1080.webp'); }
    .dm-bg--admin      { background-image: url('/images/demo/admin-1080.webp'); }
}
.dm-wrap--wide { max-width: 720px; }
.dm-wrap--app { max-width: 1080px; }

.dm-card {
    background: #fff;
    border-radius: 16px;
    padding: 40px 28px;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
}

.dm-brand {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--dm-teal-dark);
    margin-bottom: 10px;
    /* Top RIGHT corner of the card (Al, demo S8) — where a mark belongs, and
       it stops the brand competing with the heading for the same corner. In
       flow rather than absolutely positioned, so it can never overlap the
       heading on a narrow screen. */
    text-align: right;
}

.dm-h1 { margin: 0 0 8px; font-size: 26px; line-height: 1.2; color: var(--dm-navy); }
.dm-lead { margin: 0 0 20px; font-size: 15px; color: var(--dm-muted); line-height: 1.5; }
.dm-foot { margin: 18px 0 0; font-size: 12px; color: var(--dm-muted); }

/* The visit was restarted because a release rebuilt the port under it. Stated,
   not hidden: a demo that silently starts itself over is its own confusion. */
.dm-restarted {
    margin: 0 0 18px;
    padding: 10px 12px;
    border-left: 3px solid var(--dm-amber);
    background: rgba(217, 119, 6, 0.07);
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--dm-ink);
}

/* ── chooser ─────────────────────────────────────────────────────────── */

.dm-choice-list { display: grid; gap: 10px; margin-top: 16px; }

.dm-choice {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas: "title route" "line route";
    gap: 2px 12px;
    padding: 14px 16px;
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.15s, box-shadow 0.15s;
}
.dm-choice:hover { border-color: var(--dm-teal); box-shadow: 0 4px 16px rgba(13, 148, 136, 0.18); }

/* ONE SEAT, TWO STYLES (Al, S14) — the standard and river dispatch boards read
   as a pair rather than as two of the five things on this page. Joined edges
   and a shared left rule do it without a heading, which would have added a
   fifth line of chrome to a list whose whole job is to be scanned. */
.dm-choice--pair-top {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    border-bottom-color: transparent;
    margin-bottom: -8px;
}
.dm-choice--pair-bottom {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    margin-left: 18px;
}
.dm-choice--pair-top,
.dm-choice--pair-bottom { box-shadow: inset 3px 0 0 #cbd5e1; }
.dm-choice--pair-top:hover,
.dm-choice--pair-bottom:hover { box-shadow: inset 3px 0 0 var(--dm-teal), 0 4px 16px rgba(13, 148, 136, 0.18); }

.dm-choice-title { grid-area: title; font-weight: 600; color: var(--dm-navy); }
.dm-choice-line { grid-area: line; font-size: 13px; color: var(--dm-muted); }
.dm-choice-route { grid-area: route; align-self: center; font-size: 12px; color: var(--dm-teal-dark); font-family: ui-monospace, monospace; }

/* ── consent ─────────────────────────────────────────────────────────── */

.dm-card--consent { max-width: 560px; margin: 0 auto; }
.dm-consent-copy { border: 1px solid var(--dm-line); border-radius: 12px; padding: 16px 18px; margin: 8px 0 18px; }
.dm-consent-copy p { margin: 0 0 10px; font-size: 14px; line-height: 1.55; }
.dm-consent-copy p:last-child { margin-bottom: 0; }
.dm-consent-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* ── §13 the invitation, and the demo without one ─────────────────────────
   Deliberately NOT styled as an error or a paywall: no red, no lock icon, no
   greyed-out button behind glass. It is a teal-lined note in the same family
   as the consent copy above it, because what it says is "here is where she is
   free", not "you cannot come in". */

.dm-invite {
    border: 1px solid var(--dm-line);
    border-left: 3px solid var(--dm-teal);
    border-radius: 12px;
    padding: 14px 16px;
    margin: 18px 0 0;
    background: #f8fafc;
}
.dm-invite p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.55; }
.dm-invite p:last-child { margin-bottom: 0; }
.dm-invite-title { font-weight: 700; color: var(--dm-navy); font-size: 14px !important; }
.dm-invite-actions { margin-top: 12px !important; }
.dm-invite-form {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px dashed var(--dm-line);
}
.dm-invite-form label { font-size: 12.5px; color: var(--dm-muted); }
.dm-invite-form input {
    flex: 1 1 150px;
    min-width: 0;
    font: inherit;
    font-size: 14px;
    font-family: ui-monospace, monospace;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 8px 10px;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
}
.dm-invite-form button { padding: 8px 14px; }
.dm-invite-err { color: var(--dm-red); font-size: 13px; margin-top: 10px; }

/* The allowance line — printed, never hidden. A budget nobody was told about
   is a trap; one on the card is an understanding (Al, S16). */
.dm-allowance {
    margin: 14px 0 0;
    font-size: 13px;
    line-height: 1.6;
    color: var(--dm-muted);
}
.dm-allowance strong { color: var(--dm-navy); }
.dm-allowance-who {
    display: inline-block;
    background: var(--dm-teal);
    color: #fff;
    font-size: 11.5px;
    font-weight: 700;
    border-radius: 6px;
    padding: 2px 8px;
    margin-right: 8px;
}

/* The tour, as captions — manual mode's whole narration. */
.dm-invite--rail { margin: 0; border-radius: 0; border-left: 0; border-right: 0; border-bottom: 0; background: #fff; }
.dm-chat--manual .dm-tour { padding: 16px; }
.dm-tour-say { margin: 0 0 14px; font-size: 14.5px; line-height: 1.6; color: var(--dm-ink); }

/* ── buttons ─────────────────────────────────────────────────────────── */

.dm-btn {
    border: 1px solid var(--dm-teal);
    background: #fff;
    color: var(--dm-teal-dark);
    font: inherit;
    font-weight: 600;
    border-radius: 10px;
    padding: 10px 18px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}
.dm-btn:disabled { opacity: 0.45; cursor: default; }
.dm-btn--solid { background: var(--dm-teal); color: #fff; }
.dm-btn--solid:hover:not(:disabled) { background: var(--dm-teal-dark); }
.dm-btn--big { font-size: 16px; padding: 14px 22px; }
/* The declined ending's way back: present, but not a second pitch. */
.dm-btn--quiet {
    background: transparent;
    color: var(--dm-teal-dark);
    border: 1px solid var(--dm-teal);
}
.dm-btn--quiet:hover:not(:disabled) { background: #ecfdf5; }

/* ── the app layout: stage + rail ────────────────────────────────────── */

.dm-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(300px, 1fr);
    gap: 16px;
    align-items: start;
    width: 100%;
}
@media (max-width: 900px) {
    .dm-layout { grid-template-columns: 1fr; }
}

.dm-stage {
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
    overflow: hidden;
}

.dm-stage-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 18px;
    border-bottom: 1px solid var(--dm-line);
    background: #f8fafc;
}
.dm-back {
    border: none;
    background: none;
    color: var(--dm-teal-dark);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    padding: 4px 6px;
    border-radius: 8px;
}
.dm-back:hover { background: #ecfdf5; }
/* 🚨 THE BRAND SITS ON THE RIGHT (Al, S13). *"Move PilotLink Demo Port to the
   right side of the top grid — since it is near to the Back button it looks
   wrong."* Two unrelated things sharing the left corner read as one control;
   pushed to the far end it reads as what it is, a mark on the room.

   And the WORDMARK takes the Back button's colour — his call, *"for visual
   standout effect"* — while the port's name stays quiet beside it. The gradient
   and the real logo are his to give later. */
.dm-stage-brand {
    margin-left: auto;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--dm-muted);
}
.dm-stage-brand .dm-wordmark { color: var(--dm-teal-dark); }

.dm-screen { padding: 20px 22px 24px; }
.dm-screen-title { margin: 0 0 4px; font-size: 20px; color: var(--dm-navy); }
.dm-screen-sub { margin: 0 0 16px; font-size: 13px; color: var(--dm-muted); }

.dm-stage-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
.dm-badge {
    background: #ecfdf5;
    color: var(--dm-teal-dark);
    border-radius: 999px;
    padding: 4px 12px;
    font-size: 12px;
    font-weight: 600;
}
.dm-hint { font-size: 12px; color: var(--dm-muted); }

/* traffic list */
.dm-traffic { display: grid; gap: 8px; }

/* pilot job board */
.dm-job--cancelled { opacity: 0.55; border-style: dashed; }
.dm-job--moved { color: var(--dm-amber); }
.dm-job-flag {
    display: inline-block;
    margin-left: 8px;
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: #f1f5f9;
    color: var(--dm-muted);
}
.dm-job-flag--good { background: #ecfdf5; color: var(--dm-teal-dark); }
.dm-job-flag--wait { background: #fffbeb; color: var(--dm-amber); }
.dm-job-flag--new { background: var(--dm-navy); color: #fff; }
/* A finished job reads as finished across the room — solid, not a tint, so it
   is plainly a different thing from "acknowledged"/"aboard" on the way there. */
.dm-job-flag--done { background: var(--dm-teal-dark); color: #fff; }
/* A declared draft deeper than the vessel's own marks — a data error the pilot
   should catch before he is on her bridge, not a styling flourish. */
.dm-job-flag--warn { background: #fef2f2; color: #b91c1c; }
.dm-prow--warn { background: #fef2f2; }
.dm-traffic-row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 3px 10px;
    text-align: left;
    border: 1px solid var(--dm-line);
    background: #fff;
    border-radius: 12px;
    padding: 12px 14px;
    font: inherit;
    cursor: pointer;
    transition: border-color 0.15s;
}
.dm-traffic-row:hover { border-color: var(--dm-teal); }
.dm-traffic-name { font-weight: 600; color: var(--dm-navy); }
.dm-traffic-meta { font-size: 12px; color: var(--dm-muted); }
.dm-traffic-state { grid-column: 1 / -1; font-size: 13px; font-weight: 600; color: var(--dm-teal-dark); }
.dm-traffic-state--inbound { color: var(--dm-amber); }

/* A job row reads: vessel, its route beneath in green, and the time it has to
   move — big, on the right, because that is what he is scanning for. */
.dm-traffic-route { grid-column: 1; color: var(--dm-teal-dark); font-weight: 600; }
.dm-traffic-when {
    grid-column: 2;
    grid-row: 1 / span 2;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: center;
    line-height: 1.05;
}
.dm-when-date { font-size: 11px; color: var(--dm-muted); }
.dm-when-time { font-size: 26px; font-weight: 700; color: var(--dm-navy); font-variant-numeric: tabular-nums; }
.dm-when-label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dm-muted); }
/* S18b — the ORDER's state sits under the clock now, in the slot the word
   ETA/ETD used to have. It is a badge, not a label, so it needs no left margin
   and it must not stretch the column. */
.dm-when-flag { margin-left: 0; margin-top: 3px; }
.dm-traffic-when.dm-job--moved .dm-when-time  { color: var(--dm-amber); }
.dm-traffic-when.dm-job--moved .dm-when-label { color: var(--dm-amber); }

/* vessel particulars */
.dm-particulars { border: 1px solid var(--dm-line); border-radius: 12px; overflow: hidden; }
.dm-prow { display: flex; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--dm-line); }
.dm-prow:last-child { border-bottom: none; }
.dm-prow--fresh { background: #ecfdf5; }
/* A row that is a door. Full-width so it is a real phone target, and it has to
   restate the button reset because .dm-prow is written for a div. */
.dm-prow--tap {
    width: 100%;
    font: inherit;
    text-align: left;
    background: none;
    border: none;
    border-bottom: 1px solid var(--dm-line);
    cursor: pointer;
    align-items: center;
}
.dm-prow--tap:hover { background: #f8fafc; }
/* A time the JOB proposes, not one he has given (times model §6). It has to
   read as an offer rather than a record — greyed, and it says what pressing it
   does — or he confirms nothing and the card quietly claims his times. */
.dm-prow--suggested .dm-pvalue { font-weight: 500; color: var(--dm-muted); }
.dm-prow--suggested:hover .dm-pvalue { color: var(--dm-navy); }
.dm-tap-hint { margin-left: 8px; font-size: 12px; font-weight: 700; color: var(--dm-teal-dark); }
.dm-plabel { color: var(--dm-muted); font-size: 13px; }
.dm-pvalue { font-weight: 600; }

/* the sheet (order form + filled rows) */
.dm-sheet { border: 1px solid var(--dm-line); border-radius: 12px; overflow: hidden; margin-top: 14px; }
.dm-srow { display: flex; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--dm-line); background: #fbfdfe; }
.dm-srow:last-child { border-bottom: none; }
.dm-srow--filled { background: #ecfdf5; }
.dm-slabel { color: var(--dm-muted); font-size: 13px; }
.dm-svalue { font-weight: 600; text-align: right; }
/* A correction reads as a correction: the number he gave, and what it is
   replacing. Without the old value beside it a proposed particular is just a
   number on a card he is being asked to trust (J2). */
.dm-swas { display: block; font-weight: 400; font-size: 12px; color: var(--dm-muted); }

/* A measurement says what it is measured in (Brian's bug 2; David gave a draft
   in feet and an air draft in metres in one sentence). Quiet — it is a unit,
   not a value — and it rides beside the number rather than inside it, so the
   box still holds a bare number for the tool. */
.dm-unit { margin-left: 4px; font-weight: 400; font-size: 12px; color: var(--dm-muted); }

/* ⭐⭐ THE SAME LENGTH, THE OTHER WAY ROUND (David, D1).
   He gives a draft in feet and an air draft in metres in one sentence, so the
   port's metres are only half of what he needs to read. The echo is QUIETER
   than the unit — it is a courtesy, not a second value, and nothing reads it
   back into a tool — and parenthesised so it can never be mistaken for the
   number that is stored. */
.dm-alt {
    margin-left: 6px;
    font-weight: 400;
    font-size: 11px;
    color: var(--dm-muted);
    opacity: 0.8;
}
.dm-alt::before { content: '\2248\00a0'; }

/* A measurement box holds text, not a number, because it takes "38.2 ft" as
   well as "11.64" — see job-field.blade.php. Narrower than a default text box
   so a row of them still reads as numbers. */
.dm-picker-input--measure { min-width: 96px; }

.dm-order-vessel { display: grid; gap: 6px; margin-bottom: 4px; }
.dm-order-vessel label { font-size: 13px; color: var(--dm-muted); }
.dm-order-vessel select {
    font: inherit;
    padding: 10px 12px;
    border: 1px solid var(--dm-line);
    border-radius: 10px;
    background: #fff;
    color: var(--dm-ink);
}
.dm-order-kind { font-size: 12px; color: var(--dm-teal-dark); font-weight: 600; }
.dm-missing { font-size: 13px; color: var(--dm-amber); font-weight: 600; margin: 12px 0 0; }
/* advance notice — order time against the movement (§6.1) */
.dm-notice { font-size: 13px; color: var(--dm-muted); margin: 12px 0 0; }
/* SHORT NOTICE IS RED, not amber (Al, demo S8) — it is the thing on the
   screen he wants seen without looking for it, and amber reads as a caution
   next to a row that already carries an amber "awaiting pilot". */
.dm-notice--short { color: var(--dm-red); font-weight: 600; }
.dm-job-flag--short { background: #fee2e2; color: #b91c1c; }
/* Too far out for a pilot to be spoken for — tentative, not a warning. */
.dm-job-flag--prospect { background: #f1f5f9; color: var(--dm-muted); }
/* Where she is parked, on a finished arrival — a cell, at the vessel name's
   own size, so "at Berth 9" reads as a fact about the ship and not a footnote. */
.dm-berth {
    display: inline-block;
    font-size: 15px;
    font-weight: 700;
    color: var(--dm-navy);
    background: #eef2f7;
    border: 1px solid var(--dm-line);
    border-radius: 4px;
    padding: 1px 8px;
    margin-left: 8px;
    vertical-align: baseline;
}
/* THE KIND IS A BADGE, not coloured text (Al) — a square chip so the type of
   movement stands out as a type, before the words are read at all. */
.dm-kind {
    display: inline-block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    border-radius: 3px;
    padding: 2px 7px;
    margin-right: 8px;
    vertical-align: baseline;
}
/* ⭐ Q70 — THE VISITOR PICKS THESE (Al, S15; built demo-S31).
   Every meaning on the board is one token with its own default as the FALLBACK,
   so a port that has chosen nothing renders byte-identically to before and a
   chosen palette is one <style> block of custom properties on :root. The slot
   names here are the source of truth `DemoPalette` is asserted against. */
.dm-kind--inbound  { background: var(--dmc-kind-inbound-bg,  #d1fae5); color: var(--dmc-kind-inbound-fg,  #065f46); }
.dm-kind--shift    { background: var(--dmc-kind-shift-bg,    #ede9fe); color: var(--dmc-kind-shift-fg,    #5b21b6); }
.dm-kind--outbound { background: var(--dmc-kind-outbound-bg, #dbeafe); color: var(--dmc-kind-outbound-fg, #1e40af); }
/* An edit she has written but he has not confirmed. */
.dm-pending { color: var(--dm-teal-dark); font-weight: 600; }
/* PILOTLINK IS A MARK — Madrid application filed 17 Aug 2026, so ™ and never
   ®, which would claim a registration that does not exist yet. Small, raised,
   and never so faint it reads as a smudge. */
.dm-tm {
    /* 0.5em on a 13px brand was 6px and 0.72em was still a smudge (Al, twice).
       The ™ glyph is drawn small inside its own em box, so it needs a size
       close to the text's to read at all. A mark you cannot see is not a mark. */
    font-size: 0.85em;
    font-weight: 700;
    vertical-align: super;
    line-height: 0;
    margin-left: 2px;
    letter-spacing: 0;
}
/* The chooser's first seat — where a job begins, and the one to take first. */
.dm-sequence { font-size: 13px; }
.dm-choice--first { border-color: var(--dm-teal); }
.dm-btn--tiny { font-size: 12px; padding: 4px 10px; margin-left: 8px; }
/* the vessel row on a job card — a door, not a label */
.dm-vessel-door { margin: 10px 0 4px; }
.dm-orders-placed { margin-top: 18px; border-top: 1px dashed var(--dm-line); padding-top: 12px; }
.dm-order-line { margin: 4px 0; font-size: 13px; color: var(--dm-ink); }

/* invoices */
.dm-invoices { display: grid; gap: 6px; }
.dm-irow {
    display: grid;
    grid-template-columns: auto 1fr auto auto;
    gap: 12px;
    align-items: baseline;
    padding: 10px 12px;
    border: 1px solid var(--dm-line);
    border-radius: 10px;
    font-size: 13px;
}
/* S18 — a row is a BUTTON now (a door to invoice-card), so it carries the
   traffic row's reset: inherit the font, left-align, hand cursor. */
button.dm-irow {
    width: 100%;
    text-align: left;
    font: inherit;
    color: var(--dm-ink);
    background: #fff;
    cursor: pointer;
    transition: border-color 0.15s;
}
button.dm-irow:hover { border-color: var(--dm-teal); }
.dm-irow--overdue { border-color: #fecaca; background: #fef2f2; }
.dm-irow--settled { opacity: 0.7; }
.dm-irow-num { font-family: ui-monospace, monospace; color: var(--dm-muted); }
.dm-irow-vessel { font-weight: 600; color: var(--dm-navy); }
.dm-irow-date { color: var(--dm-muted); }
.dm-irow-amount { font-weight: 700; }
.dm-irow-paid { font-weight: 600; font-size: 11px; color: var(--dm-teal-dark); text-transform: uppercase; letter-spacing: 0.06em; margin-left: 4px; }
.dm-invoices-settled-head { margin: 18px 0 8px; }

/* ── THE BILLING SEAT (demo S25, wave 1) ─────────────────────────────────
   Desktop only, deliberately (Al): a bill card is a table of money and it
   needs the width. Nothing here is in a narrow-screen media query. */

/* the port-wide invoice list carries a WHO-PAYS column the agent's does not */
.dm-irow--port { grid-template-columns: auto 1fr 1fr auto auto; }
.dm-irow-agency { color: var(--dm-muted); }
.dm-irow--draft { border-style: dashed; border-color: var(--dm-teal); }

/* the bill queue: vessel, agency, route, then the facts a charge comes from */
.dm-bill-row { grid-template-columns: 1fr 1fr 1fr auto; }
.dm-bill-route { color: var(--dm-muted); }
.dm-bill-facts { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.dm-bill-fact {
    font-size: 11px;
    padding: 2px 7px;
    border-radius: 999px;
    background: #f1f5f9;
    color: var(--dm-muted);
    white-space: nowrap;
}
.dm-bill-fact--delay { background: #fffbeb; color: var(--dm-amber); }

.dm-bill-head { margin: 20px 0 8px; }
.dm-bill-pilots { margin-top: 12px; }

/* the tariff picker — every row is clickField('apply_<code>', ...) */
.dm-tariffs { display: grid; gap: 6px; }
.dm-tariff {
    display: grid;
    grid-template-columns: 22px 1fr auto;
    gap: 12px;
    align-items: center;
    width: 100%;
    text-align: left;
    font: inherit;
    color: var(--dm-ink);
    background: #fff;
    padding: 10px 12px;
    border: 1px solid var(--dm-line);
    border-radius: 10px;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
}
.dm-tariff:hover { border-color: var(--dm-teal); }
.dm-tariff--on { border-color: var(--dm-teal); background: #f0fdfa; }
.dm-tariff-box {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border: 1px solid var(--dm-line);
    border-radius: 4px;
    background: #fff;
    font-size: 12px;
    color: var(--dm-teal-dark);
    font-weight: 700;
}
.dm-tariff--on .dm-tariff-box { border-color: var(--dm-teal); }
.dm-tariff-body { display: flex; flex-direction: column; gap: 2px; }
.dm-tariff-name { font-weight: 600; color: var(--dm-navy); font-size: 13px; }
.dm-tariff-rule { font-size: 12px; color: var(--dm-muted); }
.dm-tariff-qty {
    font-size: 12px;
    font-family: ui-monospace, monospace;
    color: var(--dm-muted);
    white-space: nowrap;
}

/* ⭐⭐ WAVE 3 — THE TARIFF AS AN EDITABLE SENTENCE.
   The list reuses the picker's row (no checkbox column); the card renders the
   sentence itself with the four values as inline controls. */
.dm-tariff--rule { grid-template-columns: 1fr auto; }
.dm-tariff--edited { border-color: var(--dm-teal); background: #f0fdfa; }
.dm-tariff-flag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--dm-teal);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    vertical-align: 1px;
}
.dm-tariff-was { font-size: 11px; color: var(--dm-muted); font-style: italic; }

/* the sentence: words and controls on one baseline, wrapping like prose */
.dm-rule {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin: 16px 0 10px;
    padding: 16px;
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    background: #fff;
    font-size: 16px;
    line-height: 2;
    color: var(--dm-navy);
}
.dm-rule-word { color: var(--dm-ink); }
.dm-rule-input {
    font: inherit;
    font-size: 15px;
    color: var(--dm-navy);
    background: #f8fafc;
    border: 1px solid var(--dm-line);
    border-bottom: 2px solid var(--dm-teal);
    border-radius: 6px;
    padding: 4px 8px;
}
.dm-rule-input:focus { outline: none; border-color: var(--dm-teal); background: #fff; }
.dm-rule-input--money { width: 110px; text-align: right; font-family: ui-monospace, monospace; }
.dm-rule-input--select { max-width: 220px; }
/* demo-S33 — the receipt's own date, and the agency's instruction beside it. */
.dm-rule-input--date { width: 150px; }
.dm-rule-input--wide { min-width: 230px; flex: 1 1 230px; }
.dm-rule-input--yesno { width: 80px; }
.dm-rule-reads { font-size: 13px; color: var(--dm-muted); margin: 0 0 6px; }
.dm-rule-reads strong { color: var(--dm-navy); font-family: ui-monospace, monospace; }
.dm-rule-reads--zero { color: var(--dm-amber); }
.dm-rule-was { font-size: 12px; color: var(--dm-muted); margin: 0 0 12px; }
.dm-icard-edits { display: flex; flex-direction: column; gap: 2px; margin-top: 10px; }
.dm-icard-edit { font-family: ui-monospace, monospace; font-size: 11px; }

/* the working shown beside a calculated line: quantity x rate = amount */
.dm-ilines-qty { font-weight: 400; color: var(--dm-muted); font-family: ui-monospace, monospace; }

/* S18 — the pilot's card is an EDITOR now: every row of his ladder carries the
   same control the other seats use, with the "confirm" shortcut beside it. */
.dm-prow--edit { align-items: center; gap: 8px; }
.dm-pvalue--edit { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; justify-content: flex-end; min-width: 0; }
.dm-pvalue--edit .dm-picker-input { max-width: 150px; }
.dm-pvalue--edit .dm-datetime { display: inline-flex; gap: 6px; min-width: 0; }

/* 🚨 AND IT HAS TO FIT A PHONE, because this is the seat that IS a phone
   (§10). A label beside a date box, a clock box and a "confirm" chip is four
   things on one line: at 390px the clock and the chip were cut off at the
   card's right edge — controls a visitor could see the left half of and never
   reach. Below 560px the row stacks: label on its own line, controls full
   width under it. */
@media (max-width: 560px) {
    .dm-prow--edit { display: block; }
    .dm-prow--edit .dm-plabel { display: block; margin-bottom: 6px; }
    .dm-pvalue--edit { justify-content: flex-start; }
    .dm-pvalue--edit .dm-datetime { display: flex; width: 100%; }
    .dm-pvalue--edit .dm-picker-input { max-width: none; min-width: 0; }
    .dm-pvalue--edit > .dm-picker-input { width: 100%; }
}
.dm-tap-suggest {
    border: 1px dashed var(--dm-teal);
    background: #ecfdf5;
    color: var(--dm-teal-dark);
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    border-radius: 999px;
    padding: 2px 10px;
    cursor: pointer;
    white-space: nowrap;
}
.dm-tap-suggest:hover { background: #d1fae5; }

/* ⭐ AL'S S18 RULE ON THE AGENT'S CARD — why dispatch sent an order back, and
   the note that is all he may do when they have not. */
.dm-refusal {
    margin: 0 0 14px;
    padding: 10px 12px;
    border: 1px solid #fecaca;
    border-left: 3px solid var(--dm-red);
    border-radius: 8px;
    background: #fef2f2;
    font-size: 13px;
    line-height: 1.5;
    color: #7f1d1d;
}
.dm-refusal strong { color: #b91c1c; }

.dm-amend {
    margin-top: 18px;
    padding: 12px 14px;
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    background: #f8fafc;
}
.dm-amend-head {
    margin: 0 0 4px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--dm-navy);
}
.dm-amend-sent {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    margin-top: 8px;
    padding: 8px 10px;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
    background: #fff;
    font-size: 13px;
}
.dm-amend-when { color: var(--dm-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
.dm-amend-note { flex: 1; min-width: 180px; color: var(--dm-ink); }
.dm-amend-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.dm-amend-input { flex: 1; min-width: 220px; }

/* the invoice card */
.dm-icard-num { font-size: 14px; margin-right: 8px; }
.dm-icard-overdue { color: var(--dm-red); font-weight: 600; }
.dm-ilines { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 13px; }
.dm-ilines th {
    text-align: left;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--dm-muted);
    padding: 6px 8px;
    border-bottom: 1px solid var(--dm-line);
}
/* The colour is stated, not inherited: the night layout (business-S17) paints
   the page's text near-white, and a cell with no colour of its own drew the
   invoice lines invisible on the white card (found by looking, S18). */
.dm-ilines td { padding: 8px; border-bottom: 1px solid var(--dm-line); vertical-align: top; color: var(--dm-ink); }
.dm-ilines-n { width: 28px; color: var(--dm-muted); font-family: ui-monospace, monospace; }
/* ⭐ demo-S36 — THE TARIFF CODE, as the first column of his own legacy grid
   (891IN, RETPC, TRAIN). It is what a biller recognises a charge by before he
   has read the sentence beside it. */
.dm-ilines-code {
    width: 76px;
    white-space: nowrap;
    font-family: ui-monospace, monospace;
    font-size: 11px;
    color: var(--dm-navy);
}
.dm-ilines-amt { text-align: right; white-space: nowrap; font-weight: 600; }
/* 🚨 demo-S32 — THE HEADING GOES WHERE ITS NUMBERS ARE (Al, on the ageing):
   *"the columns — Current, 31-60 days — are not lined up with the amounts. The
   column names should go to the right to line up properly."*
   `.dm-ilines th` is (0,1,1) and `.dm-ilines-amt` is (0,1,0), so the left-align
   on the row of headings BEAT the right-align on the amount class and every
   money heading on this seat sat over the left edge of a right-aligned column.
   It reads as a table whose headings belong to the column before. */
.dm-ilines th.dm-ilines-amt { text-align: right; }
.dm-itotals { display: grid; gap: 4px; margin-top: 10px; margin-left: auto; max-width: 320px; }
.dm-itotal { display: flex; justify-content: space-between; gap: 16px; padding: 4px 8px; font-size: 13px; color: var(--dm-muted); }
.dm-itotal--due {
    margin-top: 4px;
    padding: 10px 12px;
    background: var(--dm-navy);
    color: #fff;
    border-radius: 10px;
    font-weight: 600;
}
.dm-itotal--due span:last-child { color: var(--dm-teal-light); }
.dm-icard-remarks { margin-top: 12px; }
a.dm-btn { text-decoration: none; display: inline-block; }
.dm-invoice-total {
    display: flex;
    justify-content: space-between;
    margin-top: 14px;
    padding: 12px 14px;
    background: var(--dm-navy);
    color: #fff;
    border-radius: 10px;
    font-weight: 600;
}
.dm-invoice-amount { color: var(--dm-teal-light); }

/* ── the rail: chat + email ──────────────────────────────────────────── */

.dm-rail { display: grid; gap: 12px; }

.dm-chat { background: #fff; border-radius: 16px; box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35); overflow: hidden; display: flex; flex-direction: column; max-height: 70vh; }
/* `center`, not `baseline` (S21): the head carries a real BUTTON now, and a
   button baseline-aligned to a heading sits low and looks dropped in. */
.dm-chat-head { padding: 12px 16px; border-bottom: 1px solid var(--dm-line); display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.dm-chat-title { font-weight: 700; color: var(--dm-navy); }
.dm-chat-sub { font-size: 12px; color: var(--dm-muted); }

/* The way back to voice — the mirror of the panel's "Type instead" (S171). */
.dm-chat-alt {
    border: none;
    background: none;
    padding: 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--dm-teal-dark);
    cursor: pointer;
    text-decoration: underline;
}
.dm-chat-alt:hover { color: var(--dm-teal); }

/* ⭐ THE WAY BACK TO HER IS A BUTTON (Al, S21) — "like our send or new job
   button". Same weight as Send, one size down from the board's buttons so it
   sits inside the chat head rather than dominating it. */
.dm-chat-talk { padding: 7px 14px; font-size: 13px; white-space: nowrap; }

/* §7 — the audio promise they never saw, because they consented in text. */
.dm-voice-offer {
    margin: 0 16px;
    padding: 11px 13px;
    background: #f0fdfa;
    border: 1px solid #99f6e4;
    border-radius: 10px;
    font-size: 13px;
    line-height: 1.45;
    color: var(--dm-ink);
}
.dm-voice-offer p { margin: 0 0 9px; }

.dm-thread { flex: 1; overflow-y: auto; padding: 14px 16px; display: grid; gap: 8px; align-content: start; }
.dm-bubble { max-width: 88%; padding: 9px 13px; border-radius: 14px; font-size: 14px; line-height: 1.45; white-space: pre-wrap; }
.dm-bubble--assistant { background: #f1f5f9; color: var(--dm-ink); border-bottom-left-radius: 4px; justify-self: start; }
.dm-bubble--user { background: var(--dm-teal); color: #fff; border-bottom-right-radius: 4px; justify-self: end; }

.dm-degraded { margin: 0 16px 10px; padding: 10px 12px; background: #fffbeb; border: 1px solid #fde68a; border-radius: 10px; font-size: 13px; color: #92400e; }

.dm-composer { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--dm-line); }
.dm-composer textarea {
    flex: 1;
    font: inherit;
    font-size: 14px;
    resize: none;
    border: 1px solid var(--dm-line);
    border-radius: 10px;
    padding: 9px 12px;
    color: var(--dm-ink);
}
.dm-composer textarea:focus { outline: 2px solid var(--dm-teal-light); border-color: var(--dm-teal); }
.dm-send { border: none; background: var(--dm-navy); color: #fff; font: inherit; font-weight: 600; border-radius: 10px; padding: 0 16px; cursor: pointer; }
.dm-send:disabled { opacity: 0.5; }

/* §5 — the DICTATE button, text mode only. Quiet until it is listening, and
   absent entirely on a browser with no SpeechRecognition. */
.dm-dictate {
    border: 1px solid var(--dm-line);
    background: #fff;
    border-radius: 10px;
    padding: 0 11px;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}
.dm-dictate:hover { border-color: var(--dm-teal); background: #ecfdf5; }
.dm-dictate--on {
    border-color: var(--dm-red);
    background: #fef2f2;
    animation: dm-dictate-pulse 1.4s ease-in-out infinite;
}
@keyframes dm-dictate-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.35); }
    50%      { box-shadow: 0 0 0 5px rgba(220, 38, 38, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .dm-dictate--on { animation: none; }
}

/* email (§8) */
.dm-email { background: #fff; border-radius: 16px; padding: 14px 16px; box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35); }
.dm-email-title { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--dm-navy); }
.dm-email-form { display: flex; gap: 8px; }
.dm-email-form input { flex: 1; font: inherit; font-size: 13px; border: 1px solid var(--dm-line); border-radius: 10px; padding: 8px 12px; }
.dm-email-form .dm-btn { padding: 8px 14px; }
.dm-email-err { margin: 8px 0 0; font-size: 12px; color: var(--dm-red); }
.dm-email-note { margin: 8px 0 0; font-size: 12px; color: var(--dm-muted); }
.dm-email-ok { margin: 0; font-size: 13px; color: var(--dm-teal-dark); font-weight: 600; }

/* done card */
.dm-card--done { max-width: 560px; margin: 0 auto; text-align: center; }
.dm-card--done .dm-lead { margin-bottom: 24px; }

.dm-offline { opacity: 0.6; pointer-events: none; }

/* ── the voice panel (dv-*) ──────────────────────────────────────────── */

[x-cloak] { display: none !important; }

.dv-wrap {
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
    padding: 16px;
    display: grid;
    gap: 12px;
}

.dv-status-row { display: flex; align-items: center; gap: 8px; }
.dv-dot { width: 9px; height: 9px; border-radius: 50%; background: #94a3b8; }
.dv-dot--on { background: var(--dm-teal-light); box-shadow: 0 0 0 4px rgba(45, 212, 191, 0.25); }
.dv-status { font-size: 13px; font-weight: 600; color: var(--dm-navy); }

.dv-thread {
    min-height: 180px;
    max-height: 44vh;
    overflow-y: auto;
    display: grid;
    gap: 8px;
    align-content: start;
    padding: 4px 2px;
}

/* The voice call has no chat — just proof she is listening. Occupies the same
   height the thread used to, so the panel does not jump when a call starts. */
.dv-live {
    min-height: 180px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 4px 2px;
}
.dv-pulse { display: flex; align-items: center; gap: 7px; height: 34px; }
.dv-pulse span {
    width: 7px;
    border-radius: 4px;
    background: var(--dm-accent, #0f8f80);
    animation: dv-bounce 1.15s ease-in-out infinite;
}
.dv-pulse span:nth-child(2) { animation-delay: 0.16s; }
.dv-pulse span:nth-child(3) { animation-delay: 0.32s; }
.dv-pulse--listening span { height: 12px; opacity: 0.55; }
.dv-pulse--speaking  span { height: 26px; opacity: 1; }
.dv-live-hint { margin: 0; font-size: 13px; color: var(--dm-muted, #5a6b74); }

@keyframes dv-bounce {
    0%, 100% { transform: scaleY(0.55); }
    50%      { transform: scaleY(1.35); }
}
@media (prefers-reduced-motion: reduce) {
    .dv-pulse span { animation: none; }
}

.dv-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.dv-call {
    border: none;
    background: var(--dm-teal);
    color: #fff;
    font: inherit;
    font-weight: 700;
    border-radius: 999px;
    padding: 10px 20px;
    cursor: pointer;
}
.dv-call--live { background: var(--dm-red); }
.dv-connecting { font-size: 13px; color: var(--dm-muted); }
.dv-ended { margin: 0 0 10px; font-size: 13px; line-height: 1.45; color: var(--dm-muted); }
.dv-alt { border: none; background: none; color: var(--dm-teal-dark); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; text-decoration: underline; }

.dv-error { background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 12px 14px; }
.dv-error p { margin: 0 0 8px; font-size: 13px; color: #991b1b; }

/* In voice mode on a phone, the stage IS the screen (§10) — hide the rail. */
@media (max-width: 900px) {
    .dm-rail--voice { display: none; }
}

/* ── the dispatcher board (§6.3, rebuilt S171) ────────────────────────────
   The legacy STANDARD board from Al's reference: VESSEL MOVEMENTS as a wide
   traffic grid, PILOTS ON ROTATION beside it. Mira is the layout's rail, so
   this is a two-panel row, not three. */

/* The dispatcher is the one demo built for a wide screen (§6.3): give the
   traffic grid the room and hold Mira's rail to its minimum. The other three
   personas keep the 1080px shell. */
/* 🚨 THE WIDE SHELL IS PER PERSONA, AND THE RIVER NEEDS IT MORE THAN ANYONE.
   The river board puts THREE panels in the width the standard grid has to
   itself, so on the 1080px default its own responsive rule fired and stacked
   them — three lists down the page, which is the one thing the layout must
   never become. Found by looking: every vessel name clipped and every row
   "misaligned", because the panels were no longer side by side. */
/* 🚨 THE CLASS IS THE PERSONA KEY — `dm-wrap--{{ $persona->key() }}` (S14).
   Renaming the keys to `standard-dispatch` / `river-dispatch` silently unhooked
   this rule, and the symptom was on the OTHER board: the standard dispatcher's
   1760px shell reverted to the 1080px default, so its fifteen-column grid was
   squeezed with nothing to say why. A selector built from a value in PHP breaks
   without an error when that value moves. */
.dm-wrap--standard-dispatch { max-width: 1760px; }
/* ⭐ THE RIVER BOARD IS THE SAME SHELL AS THE STANDARD ONE (Al, S15): *"vessel
   name and pilot name have room to adjust the size, so we will have more room
   for talking to Mira. The standard dispatch board is smaller."*
   It used to take 1880px and give Mira a 280px rail, on the argument that three
   panels of ten columns need the width. They do not: the two columns that are
   TEXT — the ship's name and the pilot's — were sized for names nobody has, and
   giving those back is worth more than a wider shell, because the rail is where
   the demo actually happens. Same max-width and the same split as the standard
   board now, so a visitor moving between the two demos sees one product. */
.dm-wrap--river-dispatch { max-width: 1760px; }
/* ⭐ THE BILLING SEAT TAKES THE SAME SHELL (Al, 13 Sep 2026 — S27): *"I would
   like the billing, accounts receivable and the tariffs to be BIGGER on the
   screen. This is not a mobile demo, this is a desktop demo for billing."*
   `desktop only` had been built as "not responsive down to a phone" and not
   as "uses the width": the seat sat in the phone seats' 1080px shell with an
   empty dark field either side of a narrow card. Found by Al walking it —
   the fourth time a seat's truth came from a walker, not from us. */
.dm-wrap--billing { max-width: 1760px; }

/* 🚨 INSIDE THE BREAKPOINT, NOT ABOVE IT (Al, 10 Sep 2026 — measured on a
   phone: the board was SEVEN PIXELS WIDE).

   These two lines used to sit here unqualified. `.dm-layout` collapses to one
   column at `max-width: 900px` (line ~263), but a bare `.dm-layout` loses to
   `.dm-wrap--river-dispatch .dm-layout` on specificity AND comes earlier in the
   file — so on a 375px screen the two dispatch boards kept a two-column grid
   and resolved to `7px 320px`: the rail took its 320px minimum and the stage
   got what was left. Both boards were unusable on a phone, silently, and spec
   §10 says *"all four open on mobile. None is ever blocked."*

   A media query cannot be outranked by specificity, so the wide split now only
   exists where it is true. Gate [31] asserts both selectors stay inside it. */
@media (min-width: 901px) {
    .dm-wrap--standard-dispatch .dm-layout { grid-template-columns: minmax(0, 2.6fr) minmax(320px, 0.72fr); }
    .dm-wrap--river-dispatch .dm-layout { grid-template-columns: minmax(0, 2.6fr) minmax(320px, 0.72fr); }
    /* S27 — the billing seat's split joins the two boards': same stage/rail
       proportion, inside the SAME guard, or the 7-pixel phone trap (S20)
       comes back wearing a third selector. Gate [31] walks all three. */
    .dm-wrap--billing .dm-layout { grid-template-columns: minmax(0, 2.6fr) minmax(320px, 0.72fr); }
}

/* ⭐ A CONTROL THAT ONLY A NARROW SCREEN NEEDS (Al, 10 Sep 2026).

   *"The guided tour button is for phones. Then I think we need to be smarter:
   if we identify that this is a smaller device, then show the guided tour."*

   On a wide screen the rail sits beside the board with **Start the tour** at
   the top of it, and a second button under the board is clutter — his word was
   redundant. Under 900px the rail moves BELOW the stage, and a dispatch board
   is thousands of pixels tall, so that button is somewhere nobody will ever
   scroll to. Same control, opposite verdict, decided by the viewport rather
   than by a device string: a narrow desktop window gets it too, and a phone
   that lies about its user agent cannot lose it. */
.dm-onlynarrow { display: none; }
@media (max-width: 900px) {
    .dm-onlynarrow { display: inline-flex; }
}

.dm-boardwrap {
    display: grid;
    /* The roster column is AS WIDE AS ITS TABLE, never narrower (Al, S18: the
       LIST TIME column was cut off — "LIST TIM", "1 Sep 030"). It used to be a
       fixed 272px under a panel with overflow hidden, so four nowrap columns
       at their natural width were clipped at the right edge, silently. The
       traffic grid beside it already scrolls in its own wrapper, so it is the
       one that gives. */
    grid-template-columns: minmax(0, 1fr) minmax(272px, max-content);
    gap: 12px;
    align-items: start;
}

.dm-panel {
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    overflow: hidden;
    background: #fff;
    min-width: 0;
}
/* ⭐ THE SCREEN'S OWN HEADER (Al, S15): the board's name and what it is for on
   the left, the jump to the other board on the right, full size — *"the place
   for it is in that header where we describe what our board is all about... not
   that small, same size like you have Add location."* It is a way of LOOKING at
   the port, so it belongs beside the port's name rather than in the row of
   things you do to it. */
.dm-screen-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
}
.dm-screen-head .dm-screen-sub { margin-bottom: 14px; }
.dm-screen-head .dm-btn--jump { flex: none; }
@media (max-width: 700px) {
    .dm-screen-head { flex-direction: column; align-items: stretch; }
}

.dm-panel-head {
    margin: 0;
    background: #eef2f7;
    border-bottom: 1px solid var(--dm-line);
    padding: 7px 11px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--dm-navy);
}

/* ⭐⭐ demo-S33 — THE PANEL HEAD CARRIES THE LIST'S OWN CONTROLS. Al pointed
   at the top right of the ledger's heading bar and said *"here's the search
   box where it's supposed to be"* — so the heading becomes a row rather than
   a label, and nothing above the panel moves. */
.dm-panel-head--filter {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    /* The strip is taller than a label; the picker inside it must not be
       clipped by the panel's own overflow. */
    position: relative;
    padding: 5px 8px 5px 11px;
}
.dm-panel-head-name { flex: 1 1 auto; min-width: 0; }

.dm-gfilter {
    position: relative;
    display: flex;
    align-items: center;
    gap: 6px;
    flex: none;
    text-transform: none;
    letter-spacing: 0;
    font-weight: 500;
}
.dm-gfilter-count {
    font-size: 10px;
    color: var(--dm-muted);
    font-weight: 600;
    white-space: nowrap;
    max-width: 320px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.dm-gfilter-search {
    font: inherit;
    font-size: 11px;
    font-weight: 500;
    width: 170px;
    padding: 3px 8px;
    border: 1px solid var(--dm-line);
    border-radius: 5px;
    background: #fff;
    color: var(--dm-ink);
}
.dm-gfilter-search:focus { outline: 2px solid var(--dm-teal); outline-offset: -1px; }
.dm-gfilter-btn {
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    padding: 3px 9px;
    border: 1px solid var(--dm-line);
    border-radius: 5px;
    background: #fff;
    color: var(--dm-navy);
    cursor: pointer;
}
.dm-gfilter-btn:hover      { border-color: var(--dm-teal); color: var(--dm-teal); }
.dm-gfilter-btn--on        { border-color: var(--dm-teal); color: var(--dm-teal); }
.dm-gfilter-btn--clear     { color: var(--dm-red); }

/* The popup: Al's *"like we have on Filament — you click, choose your
   filtration, click OK"*. It hangs off the Filter button and closes on a click
   outside, which is the one gesture nobody has to be taught.

   ⭐⭐ demo-S36 — IT IS FIXED, NOT ABSOLUTE, BECAUSE THE PANEL CLIPS (Al:
   *"if you get to the grid where there is a minimum records on a grid, the
   filter goes inside... can we make it pop out outside of the grid?"*).
   `.dm-panel` carries `overflow: hidden` for its rounded corners, so an
   ABSOLUTE popup can never be taller than the panel it hangs in: on a board
   filtered down to one row the picker was cut off mid-select and the Done
   button did not exist. The panel's overflow is load-bearing (S18: the roster
   columns), so the popup leaves the clipping ancestor instead — `position:
   fixed`, placed from the button's own rect by `place()` in the partial, which
   also flips it ABOVE the button when the viewport has no room below.
   ⚠ Nothing may be anchored on `top`/`right` here: the coordinates are
   written by JS and a CSS offset would fight it. */
.dm-gfilter-pop {
    position: fixed;
    top: 0;
    left: 0;
    /* ⚠ HIDDEN UNTIL IT IS PLACED (demo-S36). A fixed box with no coordinates
       yet IS the top-left corner, so `demo-screen-follows.js` reveals it in the
       same pass that writes its left/top. */
    visibility: hidden;
    z-index: 50;
    width: 270px;
    display: flex;
    flex-direction: column;
    gap: 7px;
    padding: 11px;
    background: #fff;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
    box-shadow: 0 12px 30px rgba(15, 23, 42, 0.16);
}
.dm-gfilter-row { display: flex; align-items: center; gap: 8px; }
.dm-gfilter-label {
    flex: none;
    width: 78px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--dm-muted);
}
.dm-gfilter-input {
    flex: 1 1 auto;
    min-width: 0;
    font: inherit;
    font-size: 11px;
    padding: 3px 6px;
    border: 1px solid var(--dm-line);
    border-radius: 5px;
    background: #fff;
    color: var(--dm-ink);
}
.dm-gfilter-foot { display: flex; justify-content: flex-end; gap: 6px; padding-top: 3px; }

@media (max-width: 860px) {
    .dm-panel-head--filter { flex-wrap: wrap; }
    .dm-gfilter-search { width: 120px; }
    .dm-gfilter-count { max-width: 100%; }
}

/* The grid scrolls sideways rather than squashing — fifteen columns on a
   phone is §10's "it runs, it is never blocked", not a squeeze. */
.dm-board-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.dm-board,
.dm-roster {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-variant-numeric: tabular-nums;
    font-size: 12px;
}
.dm-board { min-width: 900px; }

/* S9 — THE PADDING IS A COLUMN. Fifteen columns at 8px a side spend 240px of
   an 825px panel on whitespace; at 5px they spend 150. That difference is the
   Pilot column, which is the one the whole dispatcher demo is about. The
   roster keeps its own roomier padding below — it has four columns and room. */
.dm-th {
    background: #f8fafc;
    color: var(--dm-muted);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 6px 5px;
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--dm-line);
}
.dm-th--num { text-align: right; }

.dm-td {
    padding: 7px 5px;
    white-space: nowrap;
    color: var(--dm-navy);
    border-bottom: 1px solid #f1f5f9;
}
.dm-td--num { text-align: right; }

/* 🚨 A CLOCK IS NOT A QUANTITY (Al, S13). *"The ETA/ETD column, I think it's
   aligned right. Make it centre or left."* Right alignment exists so magnitudes
   line up at the decimal — 190 above 244 above 9.8. A time is a fixed four
   characters and always the same width, so right-aligning it only pushes it away
   from its own heading and leaves a hole where the eye looks first. Centred
   keeps the column reading as one block under its label. Dates and every other
   clock follow, or the board would align two kinds of the same thing two ways. */
.dm-th--job_date, .dm-td--job_date,
.dm-th--schedule_time, .dm-td--schedule_time,
.dm-th--order_time, .dm-td--order_time,
.dm-th--disp_time, .dm-td--disp_time,
.dm-th--boarding, .dm-td--boarding,
.dm-th--off_time, .dm-td--off_time { text-align: center; }

/* The identity columns carry the weight; the rest read as data. */
.dm-td--id     { color: var(--dm-muted); font-weight: 700; }
.dm-td--vessel { font-weight: 600; }
.dm-td--notes  { max-width: 130px; overflow: hidden; text-overflow: ellipsis; color: var(--dm-muted); }

/* A field the visitor just moved, on the grid AND on the card — the same
   highlight from one field list, so they cannot disagree. */
.dm-td--fresh {
    background: #fffbeb;
    color: var(--dm-amber);
    font-weight: 700;
}

.dm-board-row { cursor: pointer; transition: background 0.15s; }
.dm-board-row:hover td { background: #f0fdfa; }
.dm-board-row:hover .dm-td--fresh { background: #fef3c7; }

/* PILOTS ON ROTATION — #, Name, Certificate, List time. Exactly four
   columns, Al's call: the rank, who, their ticket, and when they came back on
   the list. Tighter cells than the traffic grid so all four fit unclipped. */
.dm-roster .dm-th,
.dm-roster .dm-td { padding: 7px 4px; }
.dm-roster-rank { color: var(--dm-muted); font-weight: 700; width: 1%; padding-left: 4px !important; }

/* 🚨 THE TWO GRIDS READ AS ONE INSTRUMENT (Al, S11).
   *"The pilot rotation, each pilot starts like from one, two, and then already
   four, five and six, they are out of alignment to the job record."* They were
   drifting because the two tables set their own cell padding and their own font
   size, so every row compounded a couple of pixels of difference. Rows now
   declare one HEIGHT and both tables use it, which is what keeps the eye
   travelling straight across.

   This is alignment of the BASELINE, never of the meaning: pilot row N is NOT
   the pilot on job N, and with ten jobs against six pilots it could not be.
   Al's own PPA board is the authority — #2 there takes no job at all.

   MEASURED, not eyeballed: the board's rows were 33.13px and the roster's
   31.13px, so row 6 sat 8px high of job 6 and the two headers ended 2px apart.
   Vertical padding is what produced the difference, so it is taken out of the
   equation entirely and the height is stated once, here, for both. */
.dm-board .dm-th,
.dm-roster .dm-th {
    height: 26px;
    padding-top: 0;
    padding-bottom: 0;
    line-height: 26px;
    box-sizing: border-box;
}
.dm-board .dm-td,
.dm-roster .dm-td {
    height: 33px;
    padding-top: 0;
    padding-bottom: 0;
    line-height: 33px;
    box-sizing: border-box;
}
/* The status and type pills are inline-blocks INSIDE those cells, so they
   inherited the 33px line-height and added their own 2px of padding on top —
   37px tall, which pushed the row to 38 and put the drift back, bigger. An
   inline-block's height feeds the line box, so it needs its own. */
.dm-board .dm-status,
.dm-board .dm-kind {
    line-height: 15px;
    vertical-align: middle;
}

/* The status glyph — PPA's leftmost icon column, which is the thing that
   explains a pilot's position without a sentence anywhere. */
.dm-th--glyph, .dm-td--glyph { width: 14px; padding-left: 8px !important; padding-right: 0 !important; }
.dm-pstate {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    vertical-align: middle;
}
/* ⭐ Q70 — a pilot's state is a MEANING on the board too, so it takes swatches
   like the rest. `available` stays hollow whatever is picked: "nothing is
   happening to him" is carried by the ABSENCE of fill, and a swatch there would
   turn the one state that reads at a glance into another coloured dot. */
/* Out on a ship — solid, and the darkest thing in the column. */
.dm-pstate--aboard    { background: var(--dmc-pstate-aboard-bg, var(--dm-navy)); box-shadow: 0 0 0 2px #cbd5e1; }
/* Dispatched, on his way — the same amber the grid uses for "this moved". */
.dm-pstate--committed { background: var(--dmc-pstate-committed-bg, var(--dm-amber)); }
/* Has not had his rest — the same red as his list time, so they agree. */
.dm-pstate--resting   { background: var(--dmc-pstate-resting-bg, var(--dm-red)); }
/* On the list — hollow, because "nothing is happening to him" is the point. */
/* ⚠ NOT tokenised, and [49] enforces that: "available" is the one state read by
   the ABSENCE of fill, so it is not on the picker — and a token the picker does
   not offer is a colour nobody can reach. The gate caught this line the day it
   was written, which is the whole point of asserting the sweep both ways. */
.dm-pstate--available { background: transparent; border: 1.5px solid #94a3b8; }
.dm-roster-name { font-weight: 600; }
.dm-roster-cert { color: var(--dm-muted); font-size: 11px; letter-spacing: 0.02em; }
.dm-roster-list { font-weight: 600; font-size: 11px; padding-right: 10px !important; }
/* A pilot committed this session: their list time is now the job they were
   given, so it reads as the amber "this moved" the grid uses. */
.dm-roster-row--committed .dm-roster-list { color: var(--dm-amber); }
.dm-roster-row--committed .dm-roster-name { color: var(--dm-muted); }

/* The wheel's controls — a footer under the panel, not a column: moving a
   pilot is one of the two things this demo commits (§6.3), and Al's reference
   panel has no button column. */
.dm-wheel-moves-block { padding: 8px 10px 10px; border-top: 1px solid var(--dm-line); background: #f8fafc; }
.dm-wheel-moves-block::before {
    content: 'Move on rotation';
    display: block;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--dm-muted);
    margin-bottom: 5px;
}
.dm-wheel-move {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 0;
}
.dm-wheel-move-name { flex: 1; font-size: 11px; color: var(--dm-navy); }
.dm-wheel-btn {
    border: 1px solid var(--dm-line);
    background: #fff;
    color: var(--dm-teal-dark);
    cursor: pointer;
    font-size: 8px;
    line-height: 1;
    padding: 4px 6px;
    border-radius: 5px;
}
.dm-wheel-btn:hover { background: #ecfdf5; border-color: var(--dm-teal); }

/* ── S9: THE BOARD IS READ BY COLOUR (Al's review) ──────────────────────
   *"Dispatchers can by colour see: this is my prospect, this is already
   confirmed, this was dispatched, this is sailing, this is completed."*
   The palette is OURS — Al: *"colors we can pick for ourselves is not
   important. It's just they are colors that present each different status."*
   So the only requirement is that the eight read apart at a glance, and they
   are ordered here the way a job moves through them. */
.dm-status {
    display: inline-block;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    border-radius: 3px;
    padding: 2px 6px;
    white-space: nowrap;
}
/* ⭐ Q70 — tokenised, same rule as the kinds above: the comment names the
   MEANING, which is what the picker offers a swatch for. */
.dm-status--new-prospect    { background: var(--dmc-status-new-prospect-bg,    #f1f5f9); color: var(--dmc-status-new-prospect-fg,    #475569); }   /* grey — a maybe */
.dm-status--pending         { background: var(--dmc-status-pending-bg,         #fef9c3); color: var(--dmc-status-pending-fg,         #854d0e); }   /* straw — asked, not answered */
.dm-status--order-confirmed { background: var(--dmc-status-order-confirmed-bg, #cffafe); color: var(--dmc-status-order-confirmed-fg, #155e75); }   /* cyan — agreed */
.dm-status--dispatched      { background: var(--dmc-status-dispatched-bg,      #d1fae5); color: var(--dmc-status-dispatched-fg,      #065f46); }   /* green — a pilot is on it */
.dm-status--vessel-delayed  { background: var(--dmc-status-vessel-delayed-bg,  #ffedd5); color: var(--dmc-status-vessel-delayed-fg,  #9a3412); }   /* orange — she is late */
.dm-status--sailing         { background: var(--dmc-status-sailing-bg,         #dbeafe); color: var(--dmc-status-sailing-fg,         #1e40af); }   /* blue — under way */
.dm-status--cancel-sail     { background: var(--dmc-status-cancel-sail-bg,     #fee2e2); color: var(--dmc-status-cancel-sail-fg,     #b91c1c); }   /* red — off */
.dm-status--completed       { background: var(--dmc-status-completed-bg,       #e2e8f0); color: var(--dmc-status-completed-fg,       #334155); }   /* slate — done */
.dm-status--unknown         { background: #f1f5f9; color: var(--dm-muted); }

/* The Type column carries the SAME three chips the agent's traffic list uses
   (.dm-kind--inbound / --shift / --outbound). Al asked for exactly those
   colours; reusing the classes is the point, so this only trims the badge for
   a table cell. */
.dm-kind--cell { margin-right: 0; padding: 2px 5px; font-size: 10px; }

/* A TIME THAT HAS GONE, WITH NOTHING BEHIND IT — "waiting" (Al, S9).
   Measured against the port's own clock, never the server's. */
.dm-td--gone {
    color: var(--dm-red);
    font-weight: 700;
    background: #fef2f2;
}
.dm-board-row:hover .dm-td--gone { background: #fee2e2; }

/* AN ACTUAL DRAFT DEEPER THAN THE SHIP'S SUMMER MARKS (Al, Q45 — S13).
   Recorded, never refused; the cell says which number is the conflict. */
.dm-td--over {
    color: var(--dm-red);
    font-weight: 700;
    background: #fef2f2;
}
.dm-board-row:hover .dm-td--over { background: #fee2e2; }

/* THE CONFLICT — one honest comparison against the berth's own max LOA. */
.dm-x { color: var(--dm-red); font-weight: 800; margin-right: 4px; }
/* S11 — the cross moved off the first column and onto the SHIP's name, which
   is what the conflict is actually about. It trails the name, so its margin
   goes the other way. */
.dm-x--vessel { margin-right: 0; margin-left: 5px; }
/* First cell, whichever column that is — pinning it to .dm-td--id meant the
   stripe left the board with the Job # column and said nothing on its way. */
.dm-board-row--conflict .dm-td:first-child { box-shadow: inset 3px 0 0 var(--dm-red); }

/* §1.7 — the licence flag, deliberately fake and always explained as
   organization configuration. */
.dm-pilot-warn { color: var(--dm-red); font-weight: 700; }

/* The rotation panel's rest window: green rested, red not. Eight hours, Al's
   number for the demo, and an organization rule like every other one here. */
.dm-rest--ok    { color: #047857; }
.dm-rest--short { color: var(--dm-red); }

/* the board behind the open card — visible, unclickable, slightly receded.

   ⭐ demo-S39 — THE DIM IS THE WRAPPER'S, NOT THE BOARD'S (Al: *"it should be
   everywhere the same"*). It sat on `.dm-boardwrap--behind`, a class only the
   standard grid and the billing grid carry, so the river board behind its card
   stayed at full strength while the other two receded. Every composition that
   lays a card over a board wraps the copy in `.dm-board-behind` ([55] counts
   them), so one rule here reaches every seat and a new board cannot miss it. */
.dm-board-behind { pointer-events: none; opacity: 0.55; }

/* ⭐⭐ THE CARD OPENS HIGH, OVER ITS BOARD (Al, demo-S38, three screenshots).
   *"The job card, when it's open, opens at the bottom. So we have this big gap
   ... make the job card open up above all the dispatch board, leaving only the
   narrow window ... You still see some jobs, but not a lot of them."*

   The card follows the copy of the board in the flow, so it landed after EVERY
   row: 360 px down the standard board, 468 px down the river, 564 px down the
   invoice list (which then scrolled the page 621 px to reach it, demo-S32's
   answer to the same complaint). Clipping the copy to a strip puts the card
   where he looks, on every seat, with no script: the card's own -34 px margin
   (.dm-jobcard, .dm-record-over) laps over the strip's foot, so the cut edge is
   always under the card and a row is left half showing, which is what makes it
   read as a card laid over a board rather than a new screen.

   The peek is two rows and the top of a third: 140 px on a grid (a 53 px
   heading, 34 px rows), 180 px on the river, whose scoreboard strip comes first.

   🚨 STILL INERT, DELIBERATELY (Al, same message, reversing 20 Sep): *"when card
   is open, nothing should be clicked on the board ... we are in the editing
   mode ... finish the job card and then go back to the board."* */
.dm-board-behind {
    --dm-peek: 140px;
    max-height: calc(var(--dm-peek) + 34px);
    overflow: hidden;
}
.dm-board-behind:has(.dm-riverboard) { --dm-peek: 180px; }

/* One column on a narrow screen: the grid keeps its own sideways scroll and
   the rotation panel drops beneath it rather than shrinking to nothing. */
@media (max-width: 900px) {
    .dm-boardwrap { grid-template-columns: minmax(0, 1fr); }
}

/* ── S27: THE BILLING SEAT'S TABS AND GRID ──────────────────────────────
   Al, 13 Sep 2026, with a drawing: four tabs over one full-width grid —
   *"the same grid and the presentation, minus rotation on the side."* The
   strip highlights off the current stage's screen (never its own state), so
   a hand and Mira move the same thing. The grid is the standard board's own
   table in one panel: no roster flank, so the wrap is one column. */

/* ⭐ ONE PANEL, FULL WIDTH — `dm-boardwrap`'s second track is the rotation
   flank this seat does not have, and an empty minmax(272px, max-content)
   track still takes its 272px. */
.dm-boardwrap--bill { grid-template-columns: minmax(0, 1fr); }

.dm-tabs {
    display: flex;
    align-items: stretch;
    gap: 2px;
    border-bottom: 1px solid var(--dm-line);
    margin: 0 0 16px;
    overflow-x: auto;
}
.dm-tab {
    border: none;
    background: none;
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    color: var(--dm-muted);
    padding: 9px 14px 10px;
    cursor: pointer;
    white-space: nowrap;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.dm-tab:hover { color: var(--dm-navy); }
/* The current tab: the family's teal, the same accent the solid buttons and
   the ask-chip carry — one accent, many seats, never a billing-only colour. */
.dm-tab--on {
    color: var(--dm-teal-dark);
    border-bottom-color: var(--dm-teal);
}
.dm-tab--on[aria-current] { cursor: default; }

/* The invoice grid's own chips — the standard board's status language, on
   the states a ledger actually has. */
.dm-status--draft       { background: #ecfdf5; color: #047857; }  /* teal — NO LONGER PRODUCED (demo-S36: that row says Open now); kept for a cached page */
.dm-status--outstanding { background: #cffafe; color: #155e75; }  /* cyan — owed, not late */
.dm-status--overdue     { background: #fee2e2; color: #b91c1c; }  /* red — late */
/* ⭐ demo-S32 — PAID, NOT SETTLED (Al): *"I don't like statuses to be settled.
   Status is always open or posted; maximum on an invoice we can have status
   paid. Paid is much better than settled."* `settled` was a word this seat
   invented; every legacy screen of his says paid. The old class is KEPT beside
   it for one release so a stale cached page does not draw an unstyled chip.

   ⭐ AND THE FOUR COLOURS ARE HIS (Al, demo-S32, reading the A/R tab): *"paid =
   green, open as is, posted grey and overdue = red."* They are a SCALE, which is
   why he named them together: green is done with, cyan is live and fine, red is
   live and late, grey is a row that is neither — a receipt, which is a fact
   rather than a balance. Give posted the cyan it had and "nothing is owed here"
   and "money is owed here" wear the same chip. */
.dm-status--paid        { background: #d1fae5; color: #065f46; }  /* green — nothing left on it */
.dm-status--settled     { background: #d1fae5; color: #065f46; }
/* ⭐⭐ demo-S36 — OPEN IS THE INVOICE NOBODY HAS POSTED YET (Al): calculated,
   sitting for the accounting clerk to check the tariffs, the events and the
   times, with no document number and no due date until it is posted. It used
   to mean "a debit with money on it, not yet late" — which is what POSTED
   means, one line down. Cyan stays: Al's *"open as is"*. */
.dm-status--open        { background: #cffafe; color: #155e75; }
/* ⭐⭐ demo-S34 — A PAYMENT THAT DID NOT CLEAR. Red, and it is the only CREDIT
   word on the red end of the scale: every other one says where live money is,
   and this one says there is none. The invoices it had paid are owed again,
   which is the red thing about it. */
.dm-status--reversed    { background: #fee2e2; color: #b91c1c; }
/* The tariff's period — the legacy's Current/Expired/Future radio group, as
   a chip (S27): green while the rule is live, grey when it has served, straw
   when it has not started. */
.dm-status--current     { background: #d1fae5; color: #065f46; }
.dm-status--expired     { background: #e2e8f0; color: #475569; }
.dm-status--future      { background: #fef9c3; color: #854d0e; }
/* The AR ledger's own status (S28, wave 2). `posted` is what a live
   transaction is on the legacy's AR grid — not "outstanding", which is a
   statement about a balance rather than about the row.
   ⭐ GREY because Al named it grey (demo-S32).
   ⭐⭐ demo-S36 — AND IT IS THE LIVE DEBIT WORD AGAIN. demo-S32 retired it
   because "posted" was true of every issued invoice and so told a biller
   nothing; what was actually wrong was that OPEN was being spent on the same
   rows. With open moved to the unposted invoice, posted is the document an
   agency is holding and has not yet paid — the state between the clerk's
   review and the due date. */
.dm-status--posted      { background: #e2e8f0; color: #475569; }
/* ⭐⭐ demo-S33 — AND A CREDIT SAYS WHERE ITS MONEY WENT (Al): *"on the payments
   we should have status — if applied, if the amount remaining is zero."*
   `Posted` was true of every credit the moment it was banked and stayed true
   forever, which is what was wrong with `posted` on a DEBIT in demo-S32.

   They ride the SAME THREE-COLOUR SCALE the debit words ride, deliberately —
   one language across the ledger, so the eye does not have to learn the row's
   type before it can read its colour:
     green  = done with        (Applied, as Paid is green)
     cyan   = live and fine    (Part applied, as Open is cyan)
     straw  = waiting on YOU   (On account — money nobody has placed yet, which
                                is the one row on this ledger that is a job) */
.dm-status--applied     { background: #d1fae5; color: #065f46; }
.dm-status--partapplied { background: #cffafe; color: #155e75; }
.dm-status--onaccount   { background: #fef9c3; color: #854d0e; }

/* ── THE AGEING (S28, wave 2) ─────────────────────────────────────────────
   The A/R tab's headline: who owes the port and for how long. A late bucket
   is marked in the column, because the whole reason the table exists is to
   make the eye land on the right-hand side of it. */
/* A ledger row with no single record behind it is not a door, and must not
   invite a press: the board's rows carry a pointer cursor and a hover tint. */
.dm-board-row--nodoor { cursor: default; }
.dm-board-row--nodoor:hover .dm-td { background: inherit; }

.dm-ageing { margin-top: 4px; }
.dm-ageing-late  { color: var(--dm-red); font-weight: 700; }
.dm-ageing-row td:first-child { font-weight: 600; color: var(--dm-navy); }
.dm-ageing-total td { border-top: 2px solid var(--dm-line); font-weight: 700; color: var(--dm-navy); padding-top: 8px; }
/* The statement doors sit under the ageing as named buttons rather than as
   clickable summary rows: a row of numbers gives a visitor nothing to aim at. */
.dm-ageing-doors { flex-wrap: wrap; row-gap: 6px; margin-bottom: 18px; }
/* demo-S32 — the row IS the statement door now, so it reads as one. */
.dm-ageing-row--door { cursor: pointer; }
.dm-ageing-row--door:hover td { background: #f8fafc; }
.dm-ageing-row--door:hover td:first-child { text-decoration: underline; }
.dm-ageing-hint { margin: 6px 0 18px; }
/* demo-S32 — money on account, and the panel that spends it. */
.dm-onaccount { margin-top: 14px; }
/* demo-S35 — a BLOCK, because what is inside it is a field grid now rather than
   an inline sentence (see `.dm-receipt-fields`). */
.dm-onaccount-form { display: block; width: 100%; }
.dm-apply td { vertical-align: middle; }
.dm-apply-row--late td { background: #fffbfb; }

/* ⭐⭐ demo-S33 — THE INSTRUCTION, READ BACK BEFORE IT IS ACTED ON. The panel
   sits between "where the money has gone" and "where it could go", which is
   where the decision is being made. */
.dm-instructed {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin: 14px 0 4px;
    padding: 11px 13px;
    background: #f0fdfa;
    border: 1px solid #99f6e4;
    border-radius: 8px;
}
.dm-instructed-read { margin: 0; flex: 1 1 320px; font-size: 13px; color: var(--dm-ink); }
.dm-instructed-read strong { font-family: ui-monospace, monospace; }
.dm-instructed-read--none { color: var(--dm-muted); }
.dm-instructed-miss { flex: 1 1 100%; margin: 0; }
/* The rows the instruction named, in the list it narrows. */
.dm-apply-row--named td { background: #f0fdfa; }
.dm-open-toggle { margin-left: 10px; font-size: 11px; font-weight: 500; text-transform: none; letter-spacing: 0; }

/* 🔴 demo-S34 — THE INSTRUCTION NAMES SOMEBODY ELSE'S INVOICE (Al, §8.2). The
   one line in this panel that is a WARNING and not a note, so it is the one
   thing in it that is red — inside the same block, because it is still part of
   what the machine read. */
.dm-foreign {
    flex: 1 1 100%;
    margin: 0;
    padding: 9px 11px;
    font-size: 13px;
    line-height: 1.5;
    color: #7f1d1d;
    background: #fef2f2;
    border: 1px solid #fecaca;
    border-left: 3px solid var(--dm-red);
    border-radius: 6px;
}
.dm-foreign strong { color: #b91c1c; }
.dm-foreign .dm-irow-num { color: #b91c1c; font-weight: 700; }
/* Each named invoice on its own line — a list of them run together as prose is
   unreadable at exactly the moment it matters. */
.dm-foreign-one { display: block; margin: 3px 0; }

/* demo-S34 — the bounced cheque's banner reuses the red block, at the top of
   the card rather than inside the instruction panel. */
.dm-reversed { margin: 0 0 14px; }

/* demo-S34 — the invoice reversal's disclosure block. Shut by default: it is
   the one destructive verb on this card, and a form that is always open beside
   Receive payment is a form that gets filled in by accident. */
.dm-reverse { margin-top: 14px; }
.dm-reverse > * { width: 100%; }
/* demo-S35 — the opener is a BUTTON in the action row now (Al), so it wears the
   row's own size; what marks it is the danger colour, and a pressed-open state
   that says the form below belongs to it. */
.dm-reverse-form { margin-top: 10px; }
.dm-reverse-form .dm-rule-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
.dm-reverse-form .dm-clabel { margin: 0; }

/* demo-S34 — the one destructive button on this seat. Outlined rather than
   solid: it is pressed once in a hundred visits and must not compete with the
   Apply buttons beside it. */
.dm-btn--danger { color: #b91c1c; border-color: #fecaca; }
.dm-btn--danger:hover { background: #fef2f2; border-color: var(--dm-red); }
.dm-btn--danger-on { background: #fef2f2; border-color: var(--dm-red); }

/* demo-S35 — the card's foot: the action row, the one line that says why a
   verb is missing, and the reversal form the row's own button opens. One Alpine
   island, so the button and the form it owns cannot morph apart. */
.dm-icard-foot { margin-top: 4px; }
.dm-icard-why  { margin: 10px 0 0; max-width: 760px; }

/* ⭐ demo-S34 — APPLY A CUSTOM AMOUNT. The box sits against its own button so
   the pair reads as one control; the number is right-aligned because every
   other money figure in this table is. */
.dm-applycell { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; }
.dm-applycell-amt {
    font: inherit;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    text-align: right;
    width: 92px;
    color: var(--dm-navy);
    background: #f8fafc;
    border: 1px solid var(--dm-line);
    border-bottom: 2px solid var(--dm-teal);
    border-radius: 6px;
    padding: 3px 7px;
}
.dm-applycell-amt:focus { outline: 2px solid var(--dm-teal); outline-offset: 1px; }

/* The receipt form — the tariff sentence's own row, on money coming IN. Its
   button row breaks to its own line so the sentence stays a sentence. */
.dm-receipt .dm-stage-actions { margin-top: 12px; width: 100%; }

/* ⭐⭐ demo-S35 — AND THE FIELDS ARE AN EDITOR NOW (Al: *"small, compact, but
   editor-like"*). A grid rather than a flex line, so every box carries its own
   label above it and the widest one can take the whole row — which is the
   defect he was looking at: ". Instructions" ended line one and its input
   wrapped to line two.

   ⚠ THE CONTAINER RULE BEATS THE MODIFIERS ON PURPOSE. `.dm-rfield
   .dm-rule-input` is (0,2,0) against `.dm-rule-input--money`'s (0,1,0), so the
   money box fills its cell here and keeps its 110px everywhere else — the
   inverse of the S32 defect where a modifier lost to an element rule and no
   money heading was ever right-aligned. */
.dm-receipt-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px 14px;
    width: 100%;
    /* ⚠ COMPACT IS HALF THE ASK (Al: *"small, compact, but editor-like"*). On
       the 1760px shell a four-column auto-fit gave every box 260px, so a date
       and a cheque number were drawn at the width of a sentence. The cap makes
       the fields read as a block of keyed values; below it the grid reflows
       to two columns and then to one on a phone. */
    max-width: 780px;
}
.dm-rfield { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.dm-rfield .dm-clabel { margin: 0; }
.dm-rfield .dm-rule-input { width: 100%; max-width: none; flex: none; }
/* The one field that holds a phrase rather than a value. */
.dm-rfield--wide { grid-column: 1 / -1; }

/* ⭐ demo-S39 — COPY TARIFF: two purposes as two cards to choose between, then
   only the fields the chosen one needs. `.dm-rule` is a flex row, so every
   child claims the full width, as the reversal form's do. */
/* ⚠ `.dm-rule` is drawn for a SENTENCE (16px at line-height 2); a form in it
   inherits that and its buttons came out 54px tall. */
.dm-tcopy { margin-top: 14px; font-size: 14px; line-height: 1.45; }
.dm-tcopy > * { width: 100%; }
.dm-tcopy-choices {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 10px;
    max-width: 780px;
    margin-bottom: 12px;
}
.dm-tcopy-choice {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding: 10px 12px;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
    background: #fff;
    cursor: pointer;
}
.dm-tcopy-choice--on { border-color: var(--dm-teal); box-shadow: inset 0 0 0 1px var(--dm-teal); }
.dm-tcopy-choice input { margin-top: 3px; }
.dm-tcopy-body { display: flex; flex-direction: column; gap: 3px; }
.dm-tcopy-body strong { color: var(--dm-navy); font-size: 13px; }
.dm-tcopy .dm-receipt-fields { margin-bottom: 4px; }

/* The credit note (wave 4) — a table of tickable lines plus a reason, inside
   the same bordered block as the receipt form. ⚠ The block is a flex ROW by
   inheritance from `.dm-rule`, so everything in it has to claim the full width
   or the table and the sentence sit side by side and both are unreadable. */
.dm-credit > * { width: 100%; }
.dm-credit .dm-ilines { margin: 6px 0 14px; }
.dm-credit .dm-ilines th:first-child,
.dm-credit .dm-ilines td:first-child { text-align: center; }
.dm-credit-tick { width: 15px; height: 15px; accent-color: var(--dm-teal); cursor: pointer; }
/* The reason row is the sentence, so it keeps the sentence's own baseline. */
.dm-credit .dm-rule-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.dm-rule-input--wide { flex: 1 1 260px; min-width: 0; }
.dm-credit .dm-stage-actions { margin-top: 14px; width: 100%; }

/* The tariff editor's field block (S27) — the legacy dialog's labelled rows
   under the sentence. The date inputs sit in card cells, so they want the
   cell's own width rather than the sentence row's inline width. */
.dm-tariff-editor { margin-top: 14px; }
/* ⚠ demo-S32 — DIRECT CHILDREN OF A CELL ONLY. The formula moved INSIDE this
   block (Al's layout), and a blanket `.dm-tariff-editor .dm-rule-input` made
   every box in the sentence full-width — one control per line, which is the
   opposite of a formula you can read. The labelled cells still want the cell's
   width; the sentence wants its own inline widths. */
.dm-tariff-editor .dm-cell > .dm-rule-input { width: 100%; margin-top: 4px; }
.dm-tariff-date   { max-width: 180px; }
.dm-tariff-gl     { max-width: 180px; font-family: ui-monospace, monospace; }
/* demo-S35 — the description is a sentence, so it takes the cell's full width
   (the rule two lines up already gives it that); the open-ended note under the
   expiry is the placeholder a date picker has nowhere to put. */
.dm-tariff-desc   { max-width: none; }
.dm-tariff-open   { display: block; margin-top: 4px; }
/* ⭐ demo-S32 — THE FORMULA IS TWO LINES (Al): the sum on one, and the floor
   and the per-pilot multiplier underneath, because those are not terms of the
   sum and on one line they read as though they were. The box loses its own
   frame here: it is a cell of the card now, not a panel floating above it. */
.dm-cell--rule { padding-bottom: 12px; }
.dm-rule--stacked {
    display: block;
    margin: 4px 0 0;
    padding: 0;
    border: none;
    background: none;
    line-height: 1.9;
}
.dm-rule-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.dm-rule-line--second { margin-top: 8px; }
.dm-rule-word--gap { margin-left: 18px; }
.dm-tariff-period-note { margin: 8px 0 0; }

/* The second term of a two-term invoice line: the quantity/rate cells stack
   their pairs row-aligned; the second row is muted so the pair reads as one
   working, not two charges the visitor must reconcile himself (S27). */
.dm-term2 { display: block; color: var(--dm-muted); font-size: 12px; }

/* Row tints: quieter than chips, the same inset mark a conflict row wears. */
.dm-board-row--draft    .dm-td:first-child { box-shadow: inset 3px 0 0 var(--dm-teal); }
.dm-board-row--overdue  .dm-td:first-child { box-shadow: inset 3px 0 0 var(--dm-red); }
.dm-board-row--settled  .dm-td,
.dm-board-row--paid     .dm-td { color: var(--dm-muted); }
.dm-board-row--settled  .dm-td:first-child,
.dm-board-row--paid     .dm-td:first-child { box-shadow: inset 3px 0 0 #cbd5e1; }
.dm-board-row--edited   .dm-td:first-child { box-shadow: inset 3px 0 0 var(--dm-teal); }

/* The two-line cell: a tariff's name with its sentence riding underneath —
   the grid keeps §10d's columns without hiding the sentence the tour reads.
   The --was variant carries the italic the tariff list always gave the
   port's own rule beside a visitor's edit (.dm-tariff-was): the two lines
   must differ at a glance, or the grid quietly claims his rate is hers. */
.dm-cell-main { display: block; font-weight: 600; }
.dm-cell-sub  { display: block; font-size: 11px; color: var(--dm-muted); margin-top: 1px; }
.dm-cell-sub--was { font-style: italic; }

/* ═══ THE RIVER BOARD ═════════════════════════════════════════════════════
   Al, demo S11: "pilots in the middle, arrivals on the right, departures and
   shifts on the left." A second ARRANGEMENT of the board above — same rows,
   same rules, same job card underneath.

   🚨 THE FIXED ROW HEIGHT IS LOAD-BEARING, not tidiness. The three panels are
   three separate tables that agree by ROW INDEX and nothing else, so line i is
   a line only while every row on all three is exactly the same height. That is
   also why a cell clips instead of wrapping: one long vessel name growing its
   own row would shear the alignment of everything below it, on that panel
   only. The app-track board syncs three scrollers in JS to hold this; one
   scroller around all three cannot desync at all. */
/* ⭐⭐ THE SCOREBOARD (Al, S14) — "it's all about KPI, the manpower". The
   out-and-in count is the number the whole layout exists to move, so it is the
   only one that carries colour. */
.dm-rv-kpi {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px 16px;
    margin: 0 0 9px;
    padding: 7px 12px;
    border: 1px solid var(--dm-line);
    border-radius: 10px;
    background: #f8fafc;
    font-size: 12px;
    color: var(--dm-muted);
}
.dm-rv-kpi-item b { font-size: 15px; color: var(--dm-navy); margin-right: 3px; }
.dm-rv-kpi-item--win { cursor: help; color: #92400e; }
.dm-rv-kpi-item--win b { color: #b45309; }
.dm-rv-kpi-note { margin-left: auto; cursor: help; font-size: 11px; }

.dm-riverboard { display: block; }
.dm-riverwrap {
    display: grid;
    /* ⚠ THE TWO FLANKS ARE NOT THE SAME WIDTH, because they do not carry the
       same columns: a sailing needs TRAVEL and FROM, an arrival needs neither.
       Splitting the remainder evenly gave the arrivals 90px it had no use for
       while the departures scrolled sideways by 26 — the ST column half off the
       edge, on a panel whose own overflow made it look deliberate. */
    grid-template-columns: minmax(0, 1.25fr) 280px minmax(0, 1fr);
    gap: 10px;
    /* 🚨 ONE BOTTOM EDGE (QA #12). This was `start`, which sizes each panel to
       its own content — and the three tables do not end at the same height,
       because the rotation carries neither the flanks' last border nor their
       trailing row. Measured at 1440x900: 467 / 456 / 467, all three
       top-aligned, so the middle panel stopped 11px short of its neighbours
       and its tint made the gap read as a mistake rather than as a scroll.
       `stretch` makes the row's own height the panel's height, which is what
       the three-panel spine has always claimed to be. */
    align-items: stretch;
}
.dm-panel--river { display: flex; flex-direction: column; }
/* The rotation reads as the spine it is: the two flanks are the traffic, the
   middle is the men, and the eye should know which is which without a label. */
.dm-panel--river-mid { background: #fbfcfe; }

.dm-rv-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.dm-rv {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    table-layout: fixed;
}

/* 🚨 THE ROW HEIGHT MUST NOT DEPEND ON THE CONTENT — the three panels are three
   separate tables that agree by ROW INDEX and nothing else, so line i is a line
   only while every row on all three is the same integer height.
   `height` on a table row is a MINIMUM: the used height still comes from the
   cells' own line boxes plus padding. So the vertical padding is zero and a
   fixed `line-height` does the centring.
   And `::after` forces a line box into a cell that has NO CONTENT — the blank
   half of a line was collapsing to 15px, which lifted every row below it on
   that panel out of step with the other two. */
.dm-rv thead th { height: 26px; box-sizing: border-box; }
.dm-rv-row { height: 30px; }
.dm-rv-td {
    max-width: 0;
    height: 30px;
    box-sizing: border-box;
    padding: 0 5px;
    line-height: 29px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    border-bottom: 1px solid #eef2f7;
}
.dm-rv-td::after {
    content: '';
    display: inline-block;
    width: 0;
    height: 29px;
    vertical-align: middle;
}
/* 🚨 `table-layout: fixed` DIVIDES EVENLY WHEN NOTHING NAMES A WIDTH — it does
   not fall back to the content. Every column came out the same size and clipped
   whatever was longest, on a panel that had room for all of them. So the widths
   are named per column; only Vessel is left to absorb the remainder, which is
   the one column a dispatcher actually reads across. */
/* 🚨 EVERY TABLE NEEDS A FLOOR, or `width: 100%` squeezes it below its own
   columns. Vessel is the only column with no width — it absorbs the remainder —
   so on a narrower window the remainder went to ZERO and the ship's name, the
   one thing a dispatcher reads across, vanished while every other column stayed
   put. Found by looking at a 1500px screenshot; the panel's own `overflow-x`
   now has something to scroll. */
/* ⭐ SIZED FOR THE NAMES THIS PORT ACTUALLY HAS (Al, S15). The longest ship on
   the board is "Coastline Voyager" and the longest man is "McBride, E." — the
   two text columns were carrying 146px and 108px for names that need ~118 and
   ~84. The 70px that came back went to Mira's rail, not to the board. */
.dm-rv--out { min-width: 544px; }
.dm-rv--in  { min-width: 422px; }
.dm-rv--rot { min-width: 274px; }
.dm-rvr--rank { width: 28px; }
.dm-rvr--name { width: 90px; }
/* LIST and BACK carry the DATE now (Al, S14) — the projection runs past
   midnight, so a bare clock in it is unreadable. The width came from the
   arrivals panel giving up Travel, Ride and From. */
.dm-rvr--list { width: 76px; }
.dm-rvr--back { width: 76px; }

/* THE FLANKS ASK DIFFERENT QUESTIONS, so they no longer carry the same set:
   a sailing needs TRAVEL (the clock he starts out at), an arrival needs
   neither that nor FROM, because she is always from the sea. */
/* ⚠ 44, NOT 34 — and this is the second time. The cell's 5px padding either
   side is INSIDE the width (border-box), so a bold three-letter code needs ~36
   and 34 clipped every OUT to "O…" while SHF happened to fit, which reads as a
   rendering fault rather than a narrow column.
   🚨 Caught by LOOKING at the screenshot: the `scrollWidth > clientWidth` probe
   said nothing was clipped, because a cell with `text-overflow: ellipsis` can
   report them equal. Verify a rendering by looking at the rendering. */
.dm-rvc--type_code { width: 44px; }
.dm-rvc--date      { width: 44px; }
.dm-rvc--travel    { width: 50px; }
.dm-rvc--time      { width: 44px; }
.dm-rvc--from_code { width: 66px; }
.dm-rvc--to_code   { width: 66px; }
.dm-rvc--jl        { width: 30px; }
.dm-rvc--off       { width: 42px; }
.dm-rvc--status    { width: 40px; }

/* The status is the row's own colour first; these four letters are the label a
   dispatcher can point at. All traffic here is at least CONFIRMED (Al, S14) —
   a river board is the working day, not the order book. */
/* ⭐ Q70 — THESE FOUR LETTERS ARE THE SAME FOUR MEANINGS AS THE STATUS CHIPS,
   so they read the SAME tokens. This is the half that makes a picked palette
   look finished rather than half-applied: recolour "sailing" on the standard
   board and the river's SAIL follows it, because there is one token for the
   meaning and not one per surface. Each keeps its OWN present colour as the
   fallback, so nothing moves for a port that has picked nothing. */
.dm-rv-st { font-size: 9px; font-weight: 700; letter-spacing: 0.04em; }
.dm-rv-st--ordr { color: var(--dmc-status-order-confirmed-fg, #64748b); }
.dm-rv-st--disp { color: var(--dmc-status-dispatched-fg,      #b45309); }
.dm-rv-st--sail { color: var(--dmc-status-sailing-fg,         #1d4ed8); }
.dm-rv-st--done { color: var(--dmc-status-completed-fg,       #047857); }

/* A REPEAT in the rotation column — the same man, come round again at the slot
   his own finish time earned. It carries which turn this is for him. */
.dm-rv-name--repeat { font-weight: 700; }
/* 🚨 A PLAIN INLINE SPAN, and every property here is chosen so it CANNOT grow
   the line box. The first cut was `display:inline-block` with its own
   `line-height` and `vertical-align: middle`: an inline-block participates in
   line-box height whatever its size, so every row carrying a repeat badge came
   out 32px against its neighbours' 31 — and the rotation panel drifted five
   pixels out of step with the flanks by the bottom of the board. On a layout
   whose three panels agree by row index alone, one pixel per row is a bug. */
.dm-rv-turn {
    margin-left: 5px;
    padding: 1px 4px;
    border-radius: 6px;
    background: #e0e7ff;
    color: #3730a3;
    font-size: 9px;
    font-weight: 800;
}

/* A man somebody actually SENT, as against the man the rotation proposes for
   that line. The whole middle column is a projection until this mark appears
   on a row — after which that line keeps him however the board re-derives. */
.dm-rv-sent {
    margin-left: 5px;
    color: #047857;
    font-weight: 800;
}

/* ⭐ THE PILOT'S NAME IS THE DOOR INTO HIS OWN SEAT (Q66, S15). A glyph beside
   it was tried and Al had it removed — *"it's just unnecessary"* — because the
   name is the thing you point at.

   ⚠ AND IT HAS TO LOOK LIKE A BUTTON WITHOUT BEING HOVERED (Al, S15): *"there
   is no visible confirmation — you have to point your mouse over the pilot name
   for it to be clickable. If we box it, just like putting the pilot name in a
   box, it will become button, button, button."* A door that only announces
   itself under the cursor is a door nobody finds on a touch screen at all. */
.dm-seat-name {
    padding: 1px 6px;
    border: 1px solid #cbd5e1;
    border-radius: 5px;
    background: #f8fafc;
    font: inherit;
    color: inherit;
    line-height: 1.35;
    cursor: pointer;
    text-align: left;
}
.dm-seat-name:hover {
    border-color: var(--dm-teal, #0f766e);
    background: #ecfdf5;
    color: var(--dm-teal-dark, #115e59);
}

/* The question itself — one per board, centred over it. */
.dm-seat-ask {
    position: fixed;
    z-index: 60;
    top: 96px;
    left: 50%;
    transform: translateX(-50%);
    width: min(420px, calc(100vw - 32px));
    padding: 16px 18px;
    border: 1px solid var(--dm-line, #e2e8f0);
    border-radius: 12px;
    background: #fff;
    box-shadow: 0 18px 44px rgba(15, 23, 42, 0.22);
}
.dm-seat-ask-q    { margin: 0 0 6px; font-size: 14px; line-height: 1.45; }
.dm-seat-ask-note { margin: 0 0 12px; font-size: 12px; color: #64748b; line-height: 1.45; }
.dm-seat-ask .dm-stage-actions { margin: 0; }

/* ⭐ THE BOARD IS READ BY COLOUR, and it is the SAME colour language the chips
   and the agent's traffic list already use (.dm-kind--*). A river board's
   signature is that you read the hue before the words; inventing a second
   palette for it would mean one job wearing two colours on two screens. */
/* ⭐ demo-S32 — AND IT READS THE KIND'S OWN TOKEN, which it did not until now.
   These six rules were the literal hexes the chips default to, so the river was
   the one board where picking a kind colour changed nothing: the picker offered
   it, the standard board followed, and the river sat at the port's green. That
   is exactly the half-applied palette [49] exists to prevent — it was invisible
   because both halves rendered correctly on their own screen.
   The row TINT is the same colour washed into white, so one swatch moves the
   letter and the row together and a port never has to pick two. */
.dm-rv-row--outbound .dm-rv-td { background: color-mix(in srgb, var(--dmc-kind-outbound-bg, #dbeafe) 55%, #fff); }
.dm-rv-row--shift    .dm-rv-td { background: color-mix(in srgb, var(--dmc-kind-shift-bg,    #ede9fe) 55%, #fff); }
.dm-rv-row--inbound  .dm-rv-td { background: color-mix(in srgb, var(--dmc-kind-inbound-bg,  #d1fae5) 55%, #fff); }
.dm-rv-row--outbound .dm-rvc--type_code { color: var(--dmc-kind-outbound-fg, #1e40af); font-weight: 700; }
.dm-rv-row--shift    .dm-rvc--type_code { color: var(--dmc-kind-shift-fg,    #5b21b6); font-weight: 700; }
.dm-rv-row--inbound  .dm-rvc--type_code { color: var(--dmc-kind-inbound-fg,  #065f46); font-weight: 700; }

/* A BOAT RUN IS NOT A JOB — a Light Trip or a Relief, fabricated by the board
   so the triple lines up. The legacy marks these with a yellow square and
   nothing else; here they are drawn in the port's own neutral so they read as
   a cost rather than as traffic. Each one is a pilot with ONE job instead of
   two, which is the number the board wants smaller. */
.dm-rv-row--boat .dm-rv-td {
    background: repeating-linear-gradient(135deg, #fffbeb, #fffbeb 5px, #fef3c7 5px, #fef3c7 10px);
    color: #92400e;
    font-style: italic;
}
.dm-rv-row--boat .dm-rvc--type_code { color: #92400e; font-weight: 700; font-style: normal; }
.dm-rv-row--empty .dm-rv-td { background: repeating-linear-gradient(135deg, #fcfdfe, #fcfdfe 6px, #f6f8fb 6px, #f6f8fb 12px); }

.dm-rv-row--conflict .dm-rv-td:first-child { box-shadow: inset 3px 0 0 var(--dm-red); }

/* ⭐⭐ THE LINE. One class, set by Alpine on all three panels from one piece of
   state, so a sailing, the pilot the rotation puts against it and the arrival
   he can go on to take light together — which is the whole reason the three
   columns share a row set. It never waits for the server. */
.dm-rv-row.is-line > .dm-rv-td {
    background: #fef3c7;
    border-bottom-color: #fcd34d;
}
.dm-rv-row.is-line > .dm-rv-td:first-child { box-shadow: inset 3px 0 0 var(--dm-amber); }
.dm-rv-row:not(.dm-rv-row--empty) { cursor: pointer; }

/* The certificate CLASS on the man's name — the demo port's own rule (S13), in
   the place SFBP puts a pilot's river certifications. Class A is the mark that
   matters: those are the men cleared for anything on the board. */
.dm-rv-name { font-weight: 600; }
.dm-rv-name--a { color: #b45309; font-weight: 800; }
.dm-rv-cert { color: var(--dm-muted); font-size: 11px; }
.dm-rv-window { font-size: 11px; color: #334155; cursor: help; }

/* ⭐⭐ THE OUT-AND-IN (Al, S14) — one shift, two jobs: he takes her out to the
   sea buoy and brings the next one in. It is marked on ALL THREE panels of the
   line, because the claim is about the line, not about either row. The amber
   edge is the same amber the scoreboard counts in. */
.dm-rv-row--double > .dm-rv-td:first-child { box-shadow: inset 3px 0 0 var(--dm-amber); }
.dm-rv-j--double { color: #b45309; font-weight: 800; cursor: help; }

/* the job card off the river board, and the reasoning under it */
.dm-rv-card {
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    background: #fff;
    padding: 4px 14px;
    margin-bottom: 12px;
}
.dm-rv-card-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 7px 0;
    border-bottom: 1px solid #f1f5f9;
    font-size: 13px;
}
.dm-rv-card-row:last-child { border-bottom: 0; }
.dm-rv-card-row span { min-width: 96px; color: var(--dm-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
.dm-rv-card-row em { color: var(--dm-muted); font-style: normal; font-size: 11px; }
.dm-rv-why {
    border: 1px solid #fcd34d;
    border-radius: 12px;
    background: #fffbeb;
    padding: 4px 14px 14px;
}
.dm-rv-why p { margin: 0; font-size: 13px; line-height: 1.55; color: var(--dm-navy); }

@media (max-width: 1100px) {
    /* The rotation drops beneath the traffic rather than squeezing to nothing —
       and the line highlight goes with it, because it is one island. */
    .dm-riverboard { display: block; }
.dm-riverwrap { grid-template-columns: minmax(0, 1fr); }
}

/* the card itself, laid over the board's foot — the changes land behind it */
.dm-jobcard {
    position: relative;
    z-index: 2;
    margin-top: -34px;
    background: #fff;
    border: 1px solid var(--dm-line);
    border-radius: 14px;
    padding: 18px 20px 20px;
    box-shadow: 0 18px 44px rgba(11, 37, 69, 0.18);
}
/* ⭐ A RECORD DRAWN WHERE THE JOB CARD WAS (Al, S15) — the vessel, the agency,
   the berth. Same card, same board behind it, so opening one out of the job
   editor stops reading as a jump to another screen: *"it will be more elegant
   and right there... more natural."*

   The card inside is a shared blade that brings its own `.dm-screen` padding,
   so the frame supplies the shell and takes that padding back rather than each
   of three blades learning about a dispatch board they also render on a phone. */
.dm-record-frame { padding-bottom: 24px; }
.dm-record-over {
    position: relative;
    z-index: 2;
    margin-top: -34px;
    background: #fff;
    border: 1px solid var(--dm-line);
    border-radius: 14px;
    box-shadow: 0 18px 44px rgba(11, 37, 69, 0.18);
}
.dm-record-over > .dm-screen { padding: 18px 20px 20px; }

/* ⭐ THE CARD'S OWN BACK BUTTON (Al, S15), top right of whatever is open. It
   floats out of the flow so it does not push the title down — the title is what
   the eye lands on, and a button above it would make every card look like it
   began with a control. */
.dm-card-back {
    float: right;
    margin: 0 0 4px 12px;
    position: relative;
    z-index: 3;
}
.dm-record-over > .dm-card-back { margin: 14px 18px 0 0; }

.dm-jobcard-dispatch { margin-top: 16px; }
.dm-jobcard-split { margin-top: 10px; }
.dm-btn--pilot { padding: 8px 14px; font-size: 13px; }

/* ── S12: THE NEW-JOB CARD ─────────────────────────────────────────────
   The same card as edit, empty. The mandatory rows carry a mark and, while
   they are still empty, a tinted well — so the field set and what the port
   demands read at a glance, which is what Al asked the New job door to show. */
.dm-jobcard--new .dm-cvalue--derived { color: var(--dm-muted); }
.dm-req { color: var(--dm-red); font-weight: 700; margin-left: 3px; }
/* 🚨 THE ASTERISK IS THE MARK, NOT THE CELL (Al, S13).

   *"I don't like the coloured background on the agency editor. I know you're
   trying to show that it's a mandatory field, but the red asterisk is what
   it's for. It looks very unprofessional."*

   A tinted cell per required field turned a form into a patchwork — and it
   marked the wrong thing anyway, since "required" is a property of the FIELD,
   not a state of the row. The label carries it now, and the cell is left alone.
   The form's own grey ground (below) is what makes it read as an editor. */
.dm-cell--needs .dm-clabel { color: #9a3412; }
.dm-reads { display: block; font-size: 11px; color: var(--dm-muted); margin-top: 2px; }
.dm-newjob-actions { margin-top: 14px; }
.dm-jobcard--new .dm-picker-input { width: 100%; }

/* ── S12: THE JOB'S LIFE, ON THE CARD ──────────────────────────────────
   The walk New Prospect → Order Confirmed → Dispatched → Sailing → Completed,
   with the step it is standing on lit and the ones behind it faded. The chips
   are the SAME .dm-status the grid prints, so a step reads on the card exactly
   as it reads on the board once he takes it. */
.dm-jobcard-life { margin-top: 10px; }
.dm-life { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.dm-life-step { opacity: 0.35; }
.dm-life-step--done { opacity: 0.55; }
.dm-life-step--now { opacity: 1; box-shadow: 0 0 0 2px var(--dm-navy); border-radius: 999px; }
.dm-life-step + .dm-life-step::before {
    content: '\203A';
    color: var(--dm-muted);
    margin-right: 4px;
}

/* ── S9: THE CARD IN LINES, NOT A COLUMN ───────────────────────────────
   Al's review: fifteen stacked rows ran off the bottom of the screen, so his
   layout is two to four fields per LINE. The line count comes off the field
   list's `row` grouping, so the card cannot drift from what jobFields() says.
   Cells share a line evenly and wrap on a narrow screen rather than clipping. */
.dm-cardgrid {
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    overflow: hidden;
}
.dm-cardline {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    border-bottom: 1px solid var(--dm-line);
}
.dm-cardline:last-child { border-bottom: none; }
/* Exactly two cells, kept side by side down to phone width — an auto-fit row
   of two silently becomes one column the moment either cell wants more than
   150px, which on the tariff editor split "Active on" from "Expired on". */
.dm-cardline--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dm-cell {
    padding: 8px 12px;
    border-right: 1px solid #f1f5f9;
    min-width: 0;
}
.dm-cell:last-child { border-right: none; }
.dm-clabel {
    display: block;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--dm-muted);
    margin-bottom: 2px;
}
.dm-cvalue {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: var(--dm-navy);
    overflow: hidden;
    text-overflow: ellipsis;
}
.dm-cell--fresh { background: #ecfdf5; }
.dm-cell--fresh .dm-cvalue { color: var(--dm-amber); }
.dm-cell--gone .dm-cvalue { color: var(--dm-red); }

/* A DRAFT DEEPER THAN THE SHIP'S OWN MARKS (Al, Q45 — S13). *"It's a conflict
   on the board. We just make the actual draft red. That's all."* The cell, not
   the row: the row may already be wearing a berth-limit ✗, and the point of
   colouring this one is that it names WHICH number is wrong. */
.dm-cell--over { background: #fef2f2; }
.dm-cell--over .dm-clabel { color: var(--dm-red); }
.dm-cell--over .dm-picker-input { border-color: var(--dm-red); color: var(--dm-red); font-weight: 700; }

/* THE CARD IS AN EDITOR (S13) — every row carries a control now, so the
   controls have to read as data at a glance and only look like inputs when
   they are reached for. A card of seventeen boxed fields reads as a form to
   fill in; a dispatcher's card is a thing he scans. */
.dm-cardgrid .dm-cvalue .dm-picker-input {
    width: 100%;
    font-size: 13px;
    font-weight: 600;
    color: var(--dm-navy);
}
.dm-cardgrid .dm-cvalue .dm-picker-input:focus { border-color: var(--dm-teal); }
/* A cell that is NOT an editor keeps the panel's ground — it is data, not a
   box you can type in, and it should not look like one. */
.dm-cardgrid .dm-cvalue--derived { padding: 5px 0; }

/* A value that is a DOOR — the ship, either agency, the From and the To all
   open as records of their own (§3). It has to look pressable without
   becoming a button in a grid of data. */
.dm-linkish {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: var(--dm-teal-dark);
    cursor: pointer;
    text-align: left;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 2px;
}
.dm-linkish:hover { color: var(--dm-navy); }
.dm-screen-title .dm-linkish { text-decoration-thickness: 1px; }

/* the conflict sentence, on the card the ✗ belongs to */
.dm-conflict {
    margin: 10px 0 0;
    padding: 9px 12px;
    background: #fef2f2;
    border: 1px solid #fecaca;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 600;
    color: #991b1b;
}
.dm-conflict--soft { background: #fffbeb; border-color: #fde68a; color: #92400e; }

/* ── PICKERS, NEVER TEXT BOXES (Al, S9) ────────────────────────────────
   A time opens a clock, a place offers the port's own list. Seventeen
   locations is few enough to show, which is what he meant by "how many
   locations is there, so it will not overwhelm the screen". */
.dm-pickers {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px dashed var(--dm-line);
}
.dm-picker { display: grid; gap: 3px; }
.dm-picker-label {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--dm-muted);
}
.dm-picker-input {
    font: inherit;
    font-size: 13px;
    padding: 6px 8px;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
    background: #fff;
    color: var(--dm-navy);
    min-width: 120px;
}
.dm-picker-input:focus { outline: 2px solid var(--dm-teal); outline-offset: 1px; }
.dm-picker-input--inline { min-width: 0; padding: 3px 6px; font-size: 13px; font-weight: 600; }

.dm-inline-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px;
    width: 100%;
}
.dm-board-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }

/* §3c — the Columns control. Under the board, wrapping, so it can carry
   fifteen chips without pushing the grid off the screen. */
.dm-cols { width: 100%; display: grid; gap: 9px; }
.dm-cols-label {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--dm-muted);
}
.dm-cols-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.dm-cols-item {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 3px 4px 3px 5px;
    border: 1px solid var(--dm-line);
    border-radius: 999px;
    background: #fff;
    /* Q74 — the chip is the drag handle, so it says so before it is touched:
       a grab cursor, no text selection to fight the gesture, and `relative`
       for the insertion bar the drop indicator draws on its own edge. */
    position: relative;
    cursor: grab;
    user-select: none;
}
.dm-cols-item:active { cursor: grabbing; }
/* The grip is drawn, not typed — a braille or box-drawing glyph is a bet on
   the visitor's font stack, and this chip is 12px tall. */
.dm-cols-grip {
    width: 7px;
    height: 11px;
    margin-right: 2px;
    background-image: radial-gradient(circle, var(--dm-muted) 0.9px, transparent 1.1px);
    background-size: 3.5px 3.5px;
    background-position: 0 1px;
    opacity: 0.55;
}
/* The chip being carried, and the gap it would land in. The bar sits in the
   6px list gap, so nothing reflows while he drags — a list that re-wraps under
   the pointer moves the target he is aiming at. */
.dm-cols-item--held { opacity: 0.35; }
.dm-cols-item--before::before,
.dm-cols-item--after::after {
    content: '';
    position: absolute;
    top: -1px;
    bottom: -1px;
    width: 3px;
    border-radius: 2px;
    background: var(--dm-teal);
}
.dm-cols-item--before::before { left: -5px; }
.dm-cols-item--after::after { right: -5px; }
.dm-cols-name { font-size: 12px; font-weight: 600; color: var(--dm-navy); margin-right: 3px; }
.dm-cols-btn {
    border: none;
    background: transparent;
    color: var(--dm-muted);
    font-size: 10px;
    line-height: 1;
    padding: 4px 4px;
    border-radius: 6px;
    cursor: pointer;
}
.dm-cols-btn:hover:not(:disabled) { background: #e2e8f0; color: var(--dm-navy); }
.dm-cols-btn:disabled { opacity: 0.25; cursor: default; }
.dm-cols-btn--drop:hover:not(:disabled) { background: #fee2e2; color: var(--dm-red); }

/* Q74 — THE ARROWS ARE THE KEYBOARD PATH AND THEY STAY, demoted to hover and
   focus: a demo that can only be worked with a mouse is a worse story than a
   slightly old-fashioned one, and a keyboard visitor has no other route.

   🚨 ONLY WHERE A POINTER CAN HOVER. Native HTML5 drag does not exist on touch
   — no amount of markup gives a phone the gesture — so on a coarse pointer
   these buttons are the ONLY way to move a column and they keep full strength.
   Keyed to the CAPABILITY, never to a device string (S20's rule).

   Hidden by opacity rather than by width: the chip keeps its size, so the list
   does not re-wrap under the pointer the moment it is hovered. `pointer-events`
   goes with it so an invisible control is never clickable — focus is unaffected
   by that, which is what lets Tab reach it and `:focus-within` reveal it.

   ⚠ AND IT IS WRITTEN AT TWO CLASSES DEEP ON PURPOSE. `.dm-cols-btn--move`
   alone is one class; `.dm-cols-btn:disabled` above is two, so it OUTRANKED the
   demotion and the board showed exactly the wrong half — the greyed-out arrow on
   the first chip, which cannot be pressed, while the working ones were invisible.
   A media query cannot be outranked (S20); a selector inside one still can. */
@media (hover: hover) and (pointer: fine) {
    .dm-cols-item .dm-cols-btn--move { opacity: 0; pointer-events: none; transition: opacity 0.12s ease; }
    .dm-cols-item:hover .dm-cols-btn--move,
    .dm-cols-item:focus-within .dm-cols-btn--move { opacity: 1; pointer-events: auto; }
    .dm-cols-item:hover .dm-cols-btn--move:disabled,
    .dm-cols-item:focus-within .dm-cols-btn--move:disabled { opacity: 0.25; }
}
.dm-cols-foot { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; }

/* the jobs headed for a berth, on the berth's own card */
.dm-here { display: grid; gap: 5px; width: 100%; margin-top: 6px; }
.dm-here-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    width: 100%;
    text-align: left;
    font: inherit;
    font-size: 12px;
    padding: 7px 10px;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
    background: #fff;
    cursor: pointer;
}
.dm-here-row:hover { background: #f0fdfa; }
.dm-here-row--conflict { border-color: #fecaca; background: #fef2f2; }
.dm-here-vessel { font-weight: 700; color: var(--dm-navy); }
.dm-here-note { color: var(--dm-muted); }

/* the people at an agency */
.dm-people { display: grid; gap: 5px; width: 100%; margin-top: 6px; }
.dm-person {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: baseline;
    font-size: 12px;
    padding: 7px 10px;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
}
.dm-person-name { font-weight: 700; color: var(--dm-navy); }
.dm-person-title { color: var(--dm-muted); }
.dm-person-email { color: var(--dm-teal-dark); }

/* a refused click (a tight pilot) says so on the stage, in Mira's words */
.dm-stage-err {
    margin: 0;
    padding: 12px 22px 16px;
    font-size: 13px;
    font-weight: 600;
    color: #92400e;
    background: #fffbeb;
    border-top: 1px solid #fde68a;
}

/* ...and a write that CHANGED what he typed — feet stored as metres, a rate
   rounded to what an invoice line prints. Not a warning: it worked, and this
   is what it came out as. */
.dm-stage-note {
    margin: 0;
    padding: 12px 22px 16px;
    font-size: 13px;
    color: var(--dm-teal-dark);
    background: #f0fdfa;
    border-top: 1px solid #99f6e4;
}



/* ── wave 4: the admin canvas + field editor (§6.4) ───────────────────── */

/* the canvas: a few widgets, already placed, over the capped world */
.dm-canvas-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}
.dm-widget {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 16px 18px;
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    background: #f8fafc;
}
.dm-widget--wide { grid-column: 1 / -1; }
.dm-widget-num { font-size: 28px; font-weight: 700; color: var(--dm-navy); line-height: 1.1; }
.dm-widget-label { font-size: 13px; font-weight: 600; color: var(--dm-ink); }
.dm-widget-sub { font-size: 12px; color: var(--dm-muted); }
.dm-widget-open {
    align-self: flex-start;
    margin-top: 6px;
    border: none;
    background: none;
    padding: 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--dm-teal-dark);
    cursor: pointer;
}
.dm-widget-open:hover { text-decoration: underline; }

/* suggested questions, on screen (the script asks for them) */
.dm-ask { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }
.dm-ask-title { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dm-muted); }
.dm-ask-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.dm-ask-chip {
    border: 1px solid var(--dm-line);
    background: #fff;
    color: var(--dm-navy-2);
    font-size: 13px;
    font-weight: 600;
    padding: 7px 12px;
    border-radius: 999px;
    cursor: pointer;
}
.dm-ask-chip:hover { border-color: var(--dm-teal); background: #ecfdf5; }
.dm-ask-chip--editor { border-color: var(--dm-teal); background: #ecfdf5; color: var(--dm-teal-dark); }
/* QA #18 (demo-S38) — what Mira answers, shown to a visitor with no code: a
   quotation, not a control. No border and no hover, so it cannot be mistaken
   for the button beside it that does work. */
.dm-ask-said { font-size: 13px; font-style: italic; color: var(--dm-muted); padding: 6px 2px; }

/* the vessels-alongside list */
.dm-vlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.dm-vrow {
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) auto;
    gap: 10px;
    align-items: center;
    padding: 12px 16px;
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    background: #fff;
}
.dm-vrow-name { font-size: 14px; font-weight: 700; color: var(--dm-navy); }
.dm-vrow-meta { font-size: 12px; color: var(--dm-muted); }
.dm-vrow-berth { font-size: 13px; font-weight: 600; color: var(--dm-teal-dark); }

/* the field editor: the drafting panel beside the agency record it changes */
.dm-editor {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    gap: 14px;
    align-items: start;
}
.dm-editor-panel {
    border: 1px solid var(--dm-line);
    border-radius: 12px;
    padding: 16px 18px 18px;
    background: #fff;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.dm-editor-panel--form { background: #f8fafc; }
.dm-editor-head { margin: 0; font-size: 15px; font-weight: 700; color: var(--dm-navy); }
.dm-editor-agency { margin: 0; font-size: 12px; color: var(--dm-muted); }
.dm-editor-add { align-self: flex-start; }
.dm-editor-note { margin: 0; font-size: 12px; color: var(--dm-muted); }
.dm-editor-empty { margin: 0; font-size: 13px; color: var(--dm-muted); font-style: italic; }

/* form rows shared by both panels */
.dm-frow { display: flex; flex-direction: column; gap: 5px; }
.dm-flabel { font-size: 12px; font-weight: 600; color: var(--dm-muted); }
.dm-finput {
    width: 100%;
    border: 1px solid var(--dm-line);
    border-radius: 8px;
    padding: 9px 11px;
    font-size: 14px;
    font-family: var(--dm-font);
    color: var(--dm-ink);
    background: #fff;
}
.dm-finput--fixed { background: #f1f5f9; color: var(--dm-muted); }

/* the dropdown's list, building itself while they speak */
.dm-valuelist { margin: 0; padding: 0 0 0 22px; display: flex; flex-direction: column; gap: 3px; }
.dm-valuelist-item { font-size: 13px; font-weight: 600; color: var(--dm-navy-2); }

/* custom fields on the agency record — the thing they watched appear */
.dm-fcustom-sep { margin-top: 4px; border-top: 1px dashed var(--dm-line); padding-top: 10px; }
.dm-fcustom-title { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--dm-teal-dark); }
.dm-fcustom-chip {
    display: inline-block;
    margin-left: 6px;
    padding: 2px 7px;
    border-radius: 999px;
    background: #ecfdf5;
    color: var(--dm-teal-dark);
    font-size: 11px;
    font-weight: 700;
    text-transform: none;
}
.dm-frow--custom { gap: 7px; }
.dm-fremove {
    align-self: flex-start;
    border: none;
    background: none;
    padding: 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--dm-red);
    cursor: pointer;
}
.dm-fremove:hover { text-decoration: underline; }

/* §10 — narrow: the canvas keeps one widget per row, the editor stacks,
   nothing is ever blocked. */
@media (max-width: 900px) {
    .dm-canvas-grid { grid-template-columns: 1fr; }
    .dm-editor { grid-template-columns: 1fr; }
    .dm-vrow { grid-template-columns: minmax(0, 1fr) auto; }
    .dm-vrow-meta { grid-column: 1 / -1; }
}

/* ── §3b — THE INSTRUCTIONS PAGE, BELOW THE BOARD (Al, S9) ────────────────
   "Below the board, scrollable, out of the way; the visible screen stays the
   traffic board." So it sits outside .dm-layout, on the page's dark ground,
   and nothing about it competes with the grid above. */
/* 🚨 IT SHARES THE STAGE'S BOX (Al, S11). *"The instructions are not aligned
   to the dispatch grid — they're off. Let's make it symmetrical, or align it
   by the right side."* It was centring itself at 1080px inside a 1553px wrap,
   so it sat 237px in from the left edge of the board above it and overhung the
   right — a panel that belongs to this screen, floating free of it. Same
   left edge, same right edge, same object. */
.dm-instructions {
    /* 🚨 THE WIDTH OF THE GRID, NOT OF THE PAGE (Al, S13): *"that portion on
       the right is Mira's chart, so it will grow — the About this demo should
       be the same length as the grid itself."* It is a grid item in
       `.dm-layout` pinned to column 1, so it takes the board's own width by
       construction and cannot drift from it. */
    grid-column: 1;
    margin: 40px 0 0;
    padding: 26px 28px 32px;
    max-width: none;
    background: rgba(255, 255, 255, 0.94);
    border-radius: 16px;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
    color: var(--dm-navy);
}
.dm-instructions-title {
    margin: 0 0 18px;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--dm-muted);
}
/* 🚨 THREE BUCKETS, THEN FULL-WIDTH SECTIONS (Al, S13).

   One `auto-fit` grid held everything, so five blocks of very different heights
   sat side by side: the tallest set the row height and the rest left holes —
   *"you see like a big hole inside that page"* — while the colour legend, in a
   300 px column, wrapped between its own chips. Prose that ends level goes
   across the top; anything with a legend or a list in it gets the whole width.

   `.dm-instructions-grid` is kept as an alias so nothing that still names it
   breaks. */
.dm-instructions-top,
.dm-instructions-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 22px 32px;
    align-items: start;
}
@media (max-width: 780px) {
    .dm-instructions-top,
    .dm-instructions-grid { grid-template-columns: 1fr; }
}

/* The stacked half. A rule between sections, because they are read one at a
   time rather than compared. */
.dm-instructions-rows { display: grid; gap: 0; margin-top: 26px; }
.dm-instructions-rows > .dm-instructions-block {
    padding-top: 20px;
    border-top: 1px solid var(--dm-line);
}

/* ONE LINE PER IDEA: the chips in their own column, the sentence beside them.
   A legend entry can then never wrap between its own swatches, which is what
   made "Reading the board by colour" unreadable. */
.dm-legend {
    display: grid;
    grid-template-columns: minmax(150px, 260px) 1fr;
    gap: 8px 18px;
    align-items: baseline;
    margin: 0;
    padding-left: 0;
    list-style: none;
}
.dm-legend dt { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.dm-legend dd {
    margin: 0;
    font-size: 13px;
    line-height: 1.55;
    color: #334155;
}
@media (max-width: 620px) {
    .dm-legend { grid-template-columns: 1fr; gap: 4px; }
    .dm-legend dd { margin-bottom: 10px; }
}

/* ⭐ Q70 — THE PALETTE PICKER, under the legend it recolours.
   Deliberately the legend's own grid shape (a name column and a row of chips),
   so the two blocks read as one thing: what the colours mean, then what they
   could be. */
.dm-palette { margin-top: 22px; padding-top: 18px; border-top: 1px solid #e2e8f0; }
.dm-palette h3 { margin: 0 0 6px; }
.dm-palette-lead { font-size: 13px; line-height: 1.55; color: #334155; margin: 0 0 14px; }
.dm-palette-row {
    display: grid;
    grid-template-columns: minmax(120px, 160px) 1fr;
    gap: 8px 18px;
    align-items: center;
    margin-bottom: 6px;
}
/* ⭐ demo-S32 — the name on the left IS the chip, so the row shows the thing
   being recoloured rather than describing it. The inner span carries the
   board's own class (see DemoPalette::SLOTS['css']); this wrapper only stops
   the chip's own right margin from pulling the column out of line. */
.dm-palette-name { font-size: 12px; font-weight: 600; color: #475569; }
.dm-palette-name > .dm-kind, .dm-palette-name > .dm-status { margin-right: 0; }
/* A pilot's state has no text on the board, so the dot stands beside the words. */
.dm-palette-name--dot { display: inline-flex; align-items: center; gap: 7px; }
.dm-palette-swatches { display: flex; flex-wrap: wrap; gap: 5px; }
/* A swatch shows the chip it WOULD make: the two letters carry the ink colour
   over the background colour, so a visitor sees the pair he is choosing rather
   than a square he has to imagine text on. */
.dm-swatch {
    width: 30px;
    height: 22px;
    padding: 0;
    border: 1px solid rgba(15, 23, 42, 0.12);
    border-radius: 3px;
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    transition: transform 0.08s ease, box-shadow 0.08s ease;
}
.dm-swatch:hover { transform: translateY(-1px); }
/* A pilot's state is a DOT, so its swatch is one too. */
.dm-swatch--dot { width: 22px; border-radius: 50%; }
.dm-swatch--on { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--dm-teal); }
.dm-palette-reset {
    margin-top: 12px;
    padding: 5px 11px;
    font-size: 12px;
    font-weight: 600;
    color: var(--dm-teal-dark);
    background: #fff;
    border: 1px solid #cbd5e1;
    border-radius: 6px;
    cursor: pointer;
}
.dm-palette-reset:hover { border-color: var(--dm-teal); background: #ecfdf5; }
@media (max-width: 620px) {
    .dm-palette-row { grid-template-columns: 1fr; gap: 3px; margin-bottom: 12px; }
}

/* The example lists read as two columns of short lines on a wide panel — a
   single column of eleven one-liners is a lot of white space to the right. */
.dm-asklist, .dm-conflict-list {
    columns: 2;
    column-gap: 32px;
    margin: 0;
    padding-left: 18px;
}
.dm-asklist li, .dm-conflict-list li { break-inside: avoid; }
@media (max-width: 780px) {
    .dm-asklist, .dm-conflict-list { columns: 1; }
}
.dm-instructions-block h3 {
    margin: 0 0 8px;
    font-size: 15px;
    font-weight: 700;
}
.dm-instructions-block p,
.dm-instructions-block li {
    margin: 0 0 9px;
    font-size: 13px;
    line-height: 1.55;
    color: #334155;
}
.dm-instructions-block ul { margin: 0; padding-left: 18px; }
.dm-legend .dm-status { margin-right: 2px; }
.dm-legend-swatch {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-variant-numeric: tabular-nums;
    font-size: 11px;
}

@media (max-width: 900px) {
    .dm-instructions { margin: 24px 0 0; padding: 20px 18px 24px; border-radius: 12px; }
}

/* ANOTHER SEAT, WITHOUT LOSING THIS ONE (Al, S13).
   The chain used to END the demo and redirect the tab; he agreed to the pilot
   demo mid-dispatcher-walk and could not get back. It is a link in a new tab
   now, under the board, and it appears only once the server says this demo has
   actually been walked. Quiet on purpose: it is an offer, not a call to
   action, and the board above it is still the thing he is doing. */
.dm-seat-offer {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 14px 0 0;
    padding: 10px 14px;
    background: #f0fdfa;
    border: 1px solid #99f6e4;
    border-radius: 8px;
}
.dm-seat-offer .dm-btn { text-decoration: none; }

/* THE RECORD FORMS — vessel, agency, location (Al, S13).
   *"Make the fields editable visible, just like we have on a job card."* The
   job card's cells only look like data because they are FULL of data; an empty
   form of the same cells reads as a wall of labels with nothing to type into,
   which is exactly what he hit. So on a form — and only on a form — the control
   is bordered from the start. */
/* 🚨 THE PANEL IS GREY, THE FIELDS ARE WHITE (Al, S13). *"Make the background
   like a greyish, like the About this demo one, so it will stand out as the
   editor, while the edit fields would be white."* One ground for the whole
   form says "this is a form" far better than a colour per cell did, and it
   leaves the white boxes reading as the only things you type into. */
.dm-cardgrid--form,
.dm-jobcard .dm-cardgrid {
    background: #f1f5f9;
    border: 1px solid var(--dm-line);
    border-radius: 10px;
    overflow: hidden;
}
.dm-cardgrid--form .dm-cvalue .dm-picker-input,
.dm-jobcard .dm-cardgrid .dm-cvalue .dm-picker-input {
    border: 1px solid var(--dm-line);
    background: #fff;
    border-radius: 6px;
    padding: 5px 8px;
}
.dm-cardgrid--form .dm-cell { padding: 10px 12px; }
.dm-picker-input--area { min-height: 44px; resize: vertical; font-family: inherit; }

/* A DATE AND A TIME, SIDE BY SIDE (Al, S13) — the port's own component shape.
   *"All time boxes should be with a date and a time, two fields, including
   boarding and off time. Because if the demo would show us on the edge of two
   days, that will be terrible experience."* */
.dm-datetime { display: inline-flex; gap: 4px; align-items: center; width: 100%; }
.dm-picker-input--date { flex: 1 1 58%; min-width: 0; }
.dm-picker-input--time { flex: 1 1 42%; min-width: 0; }

/* THE PILOT ALREADY ON THE JOB (Al, S13): *"whoever dispatched needs to be
   highlighted."* Solid, not merely bordered — it is a state, not an option. */
.dm-btn--pilot-on {
    background: var(--dm-teal);
    color: #fff;
    border-color: var(--dm-teal-dark);
}
.dm-btn--pilot-on:hover { background: var(--dm-teal-dark); }
.dm-hint--tight { color: var(--dm-amber); }

/* THE PILOT'S CLASS ON HIS BUTTON (Al, S13). The certificate's last letter is
   the licence class, and a ship over 200 m needs an A — so the choice can be
   informed before the objection instead of after it. */
.dm-pclass {
    display: inline-block;
    margin-left: 4px;
    padding: 0 4px;
    border-radius: 3px;
    background: #e2e8f0;
    color: var(--dm-navy);
    font-size: 10px;
    font-weight: 800;
}
.dm-pclass--no { background: #fee2e2; color: var(--dm-red); }
.dm-btn--pilot-on .dm-pclass { background: rgba(255, 255, 255, 0.25); color: #fff; }

/* THE JUMP BETWEEN THE TWO DISPATCH BOARDS (Al, S14). An <a>, not a button:
   it navigates and finishes nothing, so neither session is lost. Styled as a
   button because that is what it is to the eye. */
.dm-btn--jump {
    text-decoration: none;
    border-color: #94a3b8;
    font-weight: 700;
}
.dm-btn--jump:hover { border-color: var(--dm-teal); }

/* THE AMBER REST BAND — he ends past his working window but starts inside it.
   Red is the one that starts past it. Both only ever appear on a man's SECOND
   turn (Al, S14): the first pass through the wheel is the port's own handover
   and there is nothing there to warn about. */
.dm-rest--warn { color: #b45309; font-weight: 700; }

/* ⭐ THE ALERT — invisible until the board needs him (Al, S14). It sits at the
   right of the scoreboard, where his eye already goes for the KPI, and it is
   bound to the same ticking clock as the red cells, so it cannot claim a
   number the rows do not show. */
.dm-rv-alert {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    padding: 3px 10px;
    border-radius: 999px;
    background: #fff1f2;
    border: 1px solid #fecdd3;
    /* Dark, not red — the WORDS are read, the name is spotted. */
    color: var(--dm-navy);
    font-size: 11px;
    font-weight: 600;
    cursor: help;
}
.dm-rv-alert-who   { color: #b91c1c; font-weight: 800; }
.dm-rv-alert-arrow { color: #94a3b8; margin: 0 2px; }
.dm-rv-alert-clock { font-variant-numeric: tabular-nums; color: var(--dm-muted); }
.dm-rv-alert + .dm-rv-kpi-note { margin-left: 14px; }
.dm-rv-alert-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #dc2626;
    animation: dm-rv-pulse 1.6s ease-in-out infinite;
}
@keyframes dm-rv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) { .dm-rv-alert-dot { animation: none; } }
.dm-rv-alert-more { opacity: 0.8; font-weight: 700; }

/* The river job editor's own inputs — the four facts the placement reads. */
.dm-rv-input {
    width: 68px;
    padding: 4px 7px;
    border: 1px solid var(--dm-line);
    border-radius: 7px;
    font: inherit;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    background: #fff;
}
.dm-rv-input--wide { width: 118px; }
.dm-rv-input:focus { outline: 2px solid var(--dm-teal); outline-offset: 1px; }
.dm-rv-card-actions { margin: 10px 0 4px; padding: 0; }
.dm-rv-note { margin-top: 8px; font-size: 12px; color: var(--dm-muted); }

/* The working window, changeable from the board — configuration he can press. */
.dm-rv-window-pick { display: inline-flex; align-items: baseline; gap: 4px; cursor: pointer; }
.dm-rv-window-sel {
    border: 1px solid var(--dm-line);
    border-radius: 6px;
    background: #fff;
    padding: 1px 3px;
    font: inherit;
    font-size: 11px;
    font-weight: 800;
    color: var(--dm-navy);
    cursor: pointer;
}
.dm-rv-kpi-item--over b { color: #b45309; }

/* ── demo-S37 — "Edit Invoice / Adjustment" (spec §11) ──────────────────────
   Al's legacy carries Add Charge, Modify Charge, Delete Charge and Re-Calculate
   in ONE dialog, so this is one screen rather than four controls on a card.

   🚨 THE MODIFIERS ARE SCOPED TO THE TABLE, not left to out-rank `.dm-ilines
   th`. demo-S32: `.dm-ilines th` (0,1,1) beat `.dm-ilines-amt` (0,1,0), so no
   money heading on this seat was EVER right-aligned and nobody noticed for
   nine sessions. Anything here that has to win says `.dm-charge-edit` first. */
.dm-charge-edit td { vertical-align: middle; }
.dm-charge-edit .dm-ilines-code {
    display: inline-block;
    margin-left: 6px;
    font-size: 11px;
    color: var(--dm-muted);
}
.dm-cinput {
    width: 100%;
    max-width: 110px;
    padding: 3px 6px;
    border: 1px solid var(--dm-line);
    border-radius: 4px;
    font: inherit;
    font-size: 12px;
    text-align: right;
    background: #fff;
    color: inherit;
}
.dm-cadd .dm-cinput { max-width: none; text-align: left; }
.dm-charge-edit .dm-cactions { white-space: nowrap; }
.dm-btn--sm { padding: 3px 8px; font-size: 12px; }
.dm-charge-edit .dm-cactions .dm-btn--sm + .dm-btn--sm { margin-left: 5px; }
.dm-cadd {
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--dm-line);
}
.dm-csub { margin: 0 0 8px; font-size: 13px; font-weight: 600; }
.dm-rfield em { font-style: normal; font-weight: 400; color: var(--dm-muted); }
/* demo-S42 — the Add-a-charge row on ONE line (Al: "line up our add charges").
   A select and a text input draw at different heights from the same padding,
   so the height is stated; the hint sits under its box, never in the label. */
.dm-cadd-fields { align-items: start; }
.dm-cadd-fields .dm-rule-input { height: 34px; box-sizing: border-box; }
.dm-cadd-fields em { font-size: 11px; line-height: 1.3; }
/* ⚠ Amber, not red. A wiped hand edit is the DESIGNED behaviour (§13.3) — the
   clerk is being told what Recalculate will do, not warned off an error. */
.dm-cwarn {
    margin-top: 12px;
    padding: 8px 10px;
    border-left: 3px solid #d97706;
    background: #fffbeb;
    color: #92400e;
}

/* demo-S37 — the agreed hours, beside what the pilots did (spec §11 + §13.1).
   The override is a SECOND fact, so it reads as an annotation on the derived
   figure rather than replacing it: a biller has to be able to see both when the
   agency rings back. */
.dm-hours-agreed {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 3px;
    background: #fffbeb;
    color: #92400e;
    font-size: 11px;
    font-weight: 600;
}
.dm-hours-edit { display: flex; gap: 5px; align-items: center; margin-top: 4px; }
.dm-hours-edit .dm-cinput { max-width: 72px; }
