/* --- Page header ---
   One header for every page, index and detail alike. Pairs with the
   `pageHeader` / `pageCrumbs` partials (templates/_page_header.html).
   Caller renders the title block via a partial and any actions (search
   input, primary button, sort chips) as a sibling on the right.

   No bottom margin on purpose — every adopting page wraps its content
   in `.p-6.space-y-6`, so vertical rhythm is the parent's job. Adding
   our own mb would double-up against space-y. */
.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}

/* .page-title — the ONE h1 style in the app. Every page template's <h1>
   carries this class and nothing else that changes typography;
   templates_lint_test.go pins that. */
.page-title {
    font-size: var(--fs-2xl);
    line-height: 2rem;
    font-weight: 700;
    letter-spacing: -0.025em;
    color: rgb(var(--text-primary));
}

/* .page-subtitle — the one-line sentence under the title. */
.page-subtitle {
    font-size: var(--fs-sm);
    color: rgb(var(--text-muted));
}

/* .page-title-row — for the pages that sit a status badge, a count or an
   action beside the h1. Wraps rather than overflows: two non-shrinking
   buttons beside a long lot number must drop to a second line on a narrow
   laptop. */
.page-title-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
}

/* .page-crumbs — the trail above the title, naming the page's parent.
   Replaces the back arrows the detail pages used to hand-roll: a crumb
   says WHERE back goes, an arrow does not. It owns its bottom margin (the
   only margin in this family) because the gap between trail and title is
   part of the header, not the call site's rhythm. */
.page-crumbs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.375rem;
    margin-bottom: 0.5rem;
    font-size: var(--fs-xs);
    color: rgb(var(--text-faint));
}
.page-crumbs a {
    color: inherit;
}
.page-crumbs a:hover {
    color: rgb(var(--text-secondary));
}

/* --- Section headers ---
   One named family for the labels that title a block of content inside a
   card, replacing the hand-rolled inline variants that made the app
   "feel uneven across pages". The outline is fixed (DESIGN.md "Section
   header"): the page title is the only <h1>, a section is an
   <h2 class="section-h">, a group inside one is an
   <h3 class="subsection-h">, and there is no further level.
   templates_lint_test.go enforces it.

   .section-h     the canonical eyebrow — small, uppercase, muted. The
                  label sitting above a group of fields or rows. Most
                  sections use this.
   .subsection-h  a Title-case block title — same weight, full size,
                  primary ink. More prominent than the eyebrow.
   .rail-h        the dense eyebrow — one tier smaller (2xs), bold,
                  secondary ink (defined below).
   .alert-h       the title of an alert banner: it keeps the banner's own
                  colour instead of taking the muted eyebrow ink.

   All deliberately set no margin. Preflight zeroes heading margins and
   this file loads after the utility layer, so a margin here would
   override the margin utilities (mb-3, mt-6, and so on) each call site
   sets for its own rhythm. Spacing stays the call site's job;
   typography is ours. */
.section-h {
    font-size: var(--fs-sm);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgb(var(--text-muted));
}
.subsection-h {
    font-size: var(--fs-base);
    font-weight: 600;
    color: rgb(var(--text-primary));
}
/* .rail-h — the eyebrow for a group inside a narrow column: the lot-detail
   editor rail, and the booking notify parties, which stack a name and an
   address under each label in a three-across grid.
   One tier smaller and one shade darker than .section-h: a narrow column
   packs many groups, so its headers drop to 2xs but keep bold + secondary
   ink to stay legible at that size. Margins stay the call site's job, same
   as the rest of this family. */
.rail-h {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgb(var(--text-secondary));
}
/* .alert-h — the title of an alert banner (.alert-warning and friends).
   Deliberately sets no colour: the banner tints its own text, and the muted
   eyebrow ink would read as a disabled line on the amber band. */
.alert-h {
    font-size: var(--fs-sm);
    font-weight: 600;
}

/* --- Section help ---
   Under a section heading sits at most one line of help
   (`text-xs text-content-muted`, a plain utility pair). Anything longer
   folds into this disclosure at the END of the section, so the controls
   stay the first thing read. */
.section-help-more > summary {
    cursor: pointer;
    font-size: var(--fs-xs);
    color: rgb(var(--text-muted));
}
.section-help-more > summary:hover {
    color: rgb(var(--text-primary));
}
.section-help-more > p {
    margin-top: 0.5rem;
    font-size: var(--fs-xs);
    color: rgb(var(--text-muted));
}

/* --- Table column headers ---
   The eyebrow that titles a column on a data grid (the lots and bookings
   lists, the booking-detail sub-tables, the buyers list). It is the
   table-row analog of `.section-h`: small, uppercase, muted, lightly
   tracked, so a column label reads as the same tier as a section label.

   Typography only. Padding (px-4, py-2, py-3) is row density and alignment
   (text-left, text-right, text-center) is per column, so both stay the
   call site's job as utilities. As with `.section-h`, this file loads after
   the utility layer, so anything set here would win over those utilities.
   font-weight is 500 rather than the 600 of `.section-h`: a grid shows many
   column labels at once, and the lighter weight keeps the header row from
   shouting over the data beneath it. */
.table-head {
    font-size: var(--fs-xs);
    line-height: 1rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgb(var(--text-muted));
}

/* --- Data cell rhythm ---
   A date, an identifier, a count or a vessel/voyage is ONE fact and reads as
   one line: left to wrap, "15 Jul 2026" broke into three stacked lines and
   the row grew to match, while the free-text columns beside it sat half
   empty. Mark those cells `.cell-data`; supplier, buyer and route carry no
   marker and take the slack. Figures line up because the numerals are
   tabular. Column padding and alignment stay utilities at the call site. */
.cell-data {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* --- Filter row ---
   The one filter idiom for every list page: the search box first, then
   selects whose EMPTY option carries the label ("All programmes", "All
   statuses"), so no separate label text and no italic placeholder is needed.
   A chip row sits below only where the page splits by status. Controls keep
   the shared `.form-input` / `.form-select` look; a select that sits
   directly in the row shrinks to its content instead of the field default
   of full width. */
.filter-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    font-size: var(--fs-sm);
}
.filter-row > .form-select {
    width: auto;
}

/* --- Summary strip ---
   Period figures ride one strip under the page title. A row of hero tiles
   spends a card and a screenful of height on one number each and pushes the
   table the operator came for below the fold, so figures live inline:
   label, figure, next. The breakdowns sit under it as .mix-table
   tables, never as strip items. */
.summary-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 20px;
    font-size: var(--fs-sm);
}
.summary-strip > div {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}
/* The breakdowns under the strip (By carrier / By destination / Top shippers):
   three small tables side by side, one row per item, figures right-aligned and
   tabular. Never sentences: forty numbers in running prose is a block nobody
   can scan. Columns collapse to one on a narrow viewport. */
.mix-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    align-items: start; /* a stretched table pads its rows to fill the track */
    gap: 8px 40px;
    margin-top: 10px;
}
.mix-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-xs);
    table-layout: fixed;
}
.mix-table caption {
    caption-side: top;
    text-align: left;
    padding-bottom: 4px;
}
.mix-table th,
.mix-table td {
    padding: 2px 0 2px 10px;
    text-align: right;
    white-space: nowrap;
}
.mix-table th:first-child,
.mix-table td:first-child {
    text-align: left;
    padding-left: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mix-table th:not(:first-child),
.mix-table td:not(:first-child) {
    width: 4.5rem;
}
.mix-table td {
    color: rgb(var(--text-secondary));
    border-top: 1px solid rgb(var(--border-subtle));
}
.mix-table td.fig {
    color: rgb(var(--text-primary));
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.mix-table .mix-more td {
    text-align: left;
    color: rgb(var(--text-muted));
}
.summary-strip dt {
    color: rgb(var(--text-muted));
    white-space: nowrap;
}
.summary-strip dd {
    margin: 0;
    color: rgb(var(--text-secondary));
}
.summary-strip .fig {
    color: rgb(var(--text-primary));
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* --- Status badge classes ---
   .badge is the canonical lifecycle pill primitive per DESIGN.md:
   11px sans, weight 600, uppercase, letter-spacing 0.05em, pill-shaped,
   with a leading 6px dot in currentColor at 0.7 opacity carrying hue
   redundantly. Color comes from the badge-<state> variants below. */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}
.badge::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: currentColor;
    opacity: 0.7;
    flex-shrink: 0;
}
[class*="badge-"]:not(.save-badge-saved):not(.save-badge-error) {
    letter-spacing: 0.05em;
    text-transform: uppercase;
}
.badge-booked     { background: rgb(var(--status-booked-bg));     color: rgb(var(--status-booked-text)); }
.badge-released   { background: rgb(var(--status-released-bg));   color: rgb(var(--status-released-text)); }
.badge-docs_in    { background: rgb(var(--status-docs_in-bg));    color: rgb(var(--status-docs_in-text)); }

.badge-stuffed    { background: rgb(var(--status-stuffed-bg));    color: rgb(var(--status-stuffed-text)); }
.badge-bl_ready   { background: rgb(var(--status-bl_ready-bg));   color: rgb(var(--status-bl_ready-text)); }
.badge-shipped    { background: rgb(var(--status-shipped-bg));    color: rgb(var(--status-shipped-text)); }
.badge-unknown    { background: rgb(var(--status-unknown-bg));    color: rgb(var(--status-unknown-text)); }
.badge-on_hold    { background: rgb(var(--status-on_hold-bg));    color: rgb(var(--status-on_hold-text)); }
.badge-cancelled  { background: rgb(var(--status-cancelled-bg));  color: rgb(var(--status-cancelled-text)); }

/* ICS2 filing-state variants of the same .badge primitive, so the worklist's
   Status column reads like every other status column in the app. Hues reuse
   existing token families by meaning, not by lifecycle: neutral while the
   operators still have it, amber when one signature (or a refile decision) is
   pending, terracotta signal when it is the filer's move, green once filed. */
.badge-ics2-processing { background: rgb(var(--status-booked-bg));     color: rgb(var(--status-booked-text)); }
.badge-ics2-awaiting   { background: rgb(var(--accent-amber-soft-bg)); color: rgb(var(--accent-amber-soft-text)); }
.badge-ics2-ready      { background: rgb(var(--accent-blue-soft-bg));  color: rgb(var(--accent-blue-soft-text)); }
.badge-ics2-submitted  { background: rgb(var(--status-shipped-bg));    color: rgb(var(--status-shipped-text)); }
.badge-ics2-stale      { background: rgb(var(--accent-amber-soft-bg)); color: rgb(var(--accent-amber-soft-text)); }

/* Two neutral-severity variants of the same primitive, for a state with no
   lifecycle of its own ("Unknown type", "Already asked"). */
.badge-warn { background: rgb(var(--accent-amber-soft-bg)); color: rgb(var(--accent-amber-soft-text)); }
.badge-info { background: rgb(var(--accent-blue-soft-bg));  color: rgb(var(--accent-blue-soft-text)); }

/* --- Identifier tag ---
   The second of the three marker kinds (DESIGN.md "Badge / pill"). A tag names
   a thing: document type, programme, destination, a filing name like ICS2. Mono
   uppercase on a neutral outline, because a tag is an identifier and colour on
   these pages means state. State on a tag is a leading .status-dot, never a
   fill: a filled category chip and a filled status pill read alike at a glance,
   which is how eight pill families grew here in the first place. */
.tag {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 7px;
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-sm);
    background-color: rgb(var(--surface-card));
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    line-height: 1.4;
    color: rgb(var(--text-muted));
    white-space: nowrap;
}
.tag .status-dot { width: 6px; height: 6px; flex-shrink: 0; }

/* --- Count ---
   The third kind, and the one with no chrome at all: a cardinality is a number,
   so it takes tabular figures and nothing else. A number in a filled circle
   reads as a status pill that happens to hold a digit. */
.count {
    font-family: 'Geist', -apple-system, system-ui, sans-serif; /* a count is sans even inside a mono parent such as .tag */
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

/* --- Inline HTML alert classes (all token-backed; dark mode flips via
   tokens.css, matching the .alert-warning / .alert-info variants below) --- */
.alert-error {
    background-color: rgb(var(--accent-red-soft-bg));
    color: rgb(var(--accent-red-soft-text));
}
.alert-success {
    background-color: rgb(var(--accent-green-soft-bg));
    color: rgb(var(--accent-green-soft-text));
}
/* Title and body inherit the container color. Icon and link use the accent
   TEXT color, not --accent-green-strong: "strong" is the fill that white button
   text sits on (darkened in dark mode for AA), so it would be illegible as text
   on the dark soft-bg here. The inline action button takes the strong fill with
   white text at rest and hover — soft-text on the soft-border chip fell to
   4.05:1 in dark mode (below the 4.5:1 small-text bar), white-on-strong clears
   AA in both themes. */
.alert-success .alert-icon,
.alert-success .alert-link {
    color: rgb(var(--accent-green-soft-text));
}
.alert-success .alert-link:hover {
    color: rgb(var(--accent-green-soft-text));
}

/* Token-backed alert variants — pair with rounded-* / p-* / text-* on the
   wrapper. */
.alert-warning {
    background-color: rgb(var(--accent-amber-soft-bg));
    color: rgb(var(--accent-amber-soft-text));
    border: 1px solid rgb(var(--accent-amber-soft-border));
}
/* Info uses the ink-navy ("violet") family, not the terracotta "blue" accent:
   terracotta is reserved for primary action, and an info alert in the action
   hue reads as an error next to .alert-error. Ink-navy keeps info calm and
   visually distinct from both error (red) and the primary button. */
.alert-info {
    background-color: rgb(var(--accent-violet-soft-bg));
    color: rgb(var(--accent-violet-soft-text));
    border: 1px solid rgb(var(--accent-violet-soft-border));
}

/* Status indicator dot — 0.5rem circle. Pair with an inline-flex parent
   and a label sibling. Variants map to the accent palette so dark mode
   swaps via tokens.css. */
.status-dot {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 9999px;
    background-color: rgb(var(--text-muted));
}
.status-dot--ok      { background-color: rgb(var(--accent-green-strong)); }
.status-dot--neutral { background-color: rgb(var(--text-muted)); }
.status-dot--warn    { background-color: rgb(var(--accent-amber-strong)); }
.status-dot--error   { background-color: rgb(var(--accent-red-strong)); }

/* ── Pipeline Stage Bar ── */
.pipeline { display: flex; align-items: center; gap: 0; height: 24px; }
.pipeline.compact { height: 20px; }
.pipeline .stage {
    display: flex; align-items: center; justify-content: center;
    height: 100%; flex: 1;
    font-family: 'Geist Mono', ui-monospace, monospace;
    /* sub-scale: chevron stages pack below the --fs-3xs (10px) floor; the
       compact variant drops to 8px. Intentionally off the named type scale. */
    font-size: 9px; font-weight: 600; letter-spacing: 0.3px;
    white-space: nowrap; transition: all 0.2s ease;
}
.pipeline.compact .stage { font-size: 8px; }
.pipeline .stage:not(:last-child) {
    clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 50%, calc(100% - 6px) 100%, 0 100%, 6px 50%);
    padding-left: 8px; padding-right: 10px;
}
.pipeline .stage:first-child {
    clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 50%, calc(100% - 6px) 100%, 0 100%);
    border-radius: var(--radius-sm) 0 0 var(--radius-sm); padding-left: 6px;
}
.pipeline .stage:last-child {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 6px 50%);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0; padding-left: 8px;
}
/* Tokens auto-swap light/dark at the :root and .dark boundary.
   .current is the soft signal tier, not the solid fill: a sailing in
   mid-sprint renders one .current chevron per lot row, and ~130 solid
   terracotta blocks would drown the accent that marks the row's one
   primary action (the terracotta budget is ~5% of a screen). */
.pipeline .stage.done    { background: rgb(var(--accent-green-strong)); color: #fff; }
.pipeline .stage.current { background: rgb(var(--accent-blue-soft-bg)); color: rgb(var(--accent-blue-soft-text)); }
.pipeline .stage.blocked { background: rgb(var(--accent-red-strong));   color: #fff; animation: blocked-pulse 1.5s ease-in-out infinite; }
.pipeline .stage.todo    { background: rgb(var(--surface-inset));       color: rgb(var(--text-faint)); }
@keyframes blocked-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }

/* ── Vessel Card urgency indicator ──
   Elevation upgrade: layered ring + shadow-sm instead of the flat
   border + shadow-sm combo the template uses. Rises to shadow-md on
   hover for a subtle parallax feel. */
.vessel-card {
    position: relative;
    box-shadow: var(--shadow-ring), var(--shadow-sm);
    transition: box-shadow 0.15s ease;
}
.vessel-card:hover {
    box-shadow: var(--shadow-ring), var(--shadow-md);
}
.vessel-card::before {
    content: ''; position: absolute; left: 0; top: 12px; bottom: 12px;
    width: 3px; border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
}
.vessel-card.urgent::before      { background: rgb(var(--accent-red-strong)); }
.vessel-card.departed::before    { background: rgb(var(--text-muted)); }

/* ── Booking generation bar buttons ── */
.gen-btn {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs); font-weight: 600;
    padding: 2px 10px; border-radius: var(--radius-sm);
    border: 1px solid rgb(var(--border-default));
    background: rgb(var(--surface-card)); color: rgb(var(--text-secondary));
    cursor: pointer; transition: all 0.15s;
}
.gen-btn:hover  { border-color: rgb(var(--accent-blue-strong)); color: rgb(var(--accent-blue-strong)); }
.gen-btn.active {
    border-color: rgb(var(--accent-blue-strong));
    background: rgb(var(--accent-blue-soft-bg));
    color: rgb(var(--accent-blue-soft-text));
}
.gen-btn.disabled {
    color: rgb(var(--text-faint)); background: rgb(var(--surface-inset));
    border-color: transparent; cursor: not-allowed;
}

/* ── Generate document buttons ──
   Base visuals come from .btn .btn-primary; .gen-doc-btn only
   owns the gen-label/gen-spin swap for HTMX requests. */
.gen-doc-btn.htmx-request {
    opacity: 0.7; pointer-events: none;
}
.gen-doc-btn .gen-label { display: inline; }
.gen-doc-btn .gen-spin { display: none; }
.gen-doc-btn.htmx-request .gen-label { display: none; }
.gen-doc-btn.htmx-request .gen-spin { display: inline-flex; align-items: center; gap: 4px; }

/* ── Universal HTMX loading spinner — any button triggering HTMX gets a spinner ── */
@keyframes htmx-spin {
    to { transform: rotate(360deg); }
}
button.htmx-request, a.htmx-request {
    position: relative;
    pointer-events: none;
    opacity: 0.85;
}
button.htmx-request::after, a.htmx-request::after {
    content: "";
    display: inline-block;
    width: 12px; height: 12px;
    margin-left: 6px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: htmx-spin 0.6s linear infinite;
    vertical-align: -2px;
}
/* Skip the auto-spinner for buttons that already manage their own (gen-doc-btn) */
button.gen-doc-btn.htmx-request::after { display: none; }

/* ── Lot row action buttons ── */
.action-btn {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs); font-weight: 600;
    padding: 3px 10px; border-radius: var(--radius-sm); border: none; cursor: pointer;
}
.action-btn.generate { background: rgb(var(--accent-blue-strong)); color: #fff; }
.action-btn.generate:hover { background: rgb(var(--accent-blue-strong-hover)); }
.action-btn.blocked  { background: rgb(var(--accent-red-soft-bg));   color: rgb(var(--accent-red-soft-text));   border: 1px solid rgb(var(--accent-red-soft-border));   cursor: default; }
.action-btn.approve  { background: rgb(var(--accent-green-soft-bg)); color: rgb(var(--accent-green-soft-text)); border: 1px solid rgb(var(--accent-green-soft-border)); }
/* Hover rise: nudge the border color darker; bg stays put to avoid
   reusing a border-purpose token as a fill. */
.action-btn.approve:hover { border-color: rgb(var(--accent-green-strong)); }

/* ── Progress bar segments ──
   Tokens auto-swap light/dark at the :root and .dark boundary. */
.seg-shipped  { background: rgb(var(--seg-shipped)); }
.seg-bl-ready { background: rgb(var(--seg-bl_ready)); }
.seg-stuffed  { background: rgb(var(--seg-stuffed)); }
.seg-docs-in  { background: rgb(var(--seg-docs_in)); }
.seg-released { background: rgb(var(--seg-released)); }
.seg-booked   { background: rgb(var(--seg-booked)); }

/* ── Phase timeline strip ── */
.phase-step {
    flex: 1; text-align: center; padding: 6px 0;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs); font-weight: 600; letter-spacing: 0.3px;
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-sm);
    color: rgb(var(--text-faint));
    position: relative;
}
.phase-step.done {
    background: rgb(var(--surface-subtle));
    color: rgb(var(--text-muted));
    text-decoration: line-through;
    text-decoration-color: rgb(var(--text-faint));
}
.phase-step.current {
    border: 2px solid rgb(var(--text-link));
    background: rgba(var(--accent-violet-strong) / 0.05);
    color: rgb(var(--text-primary));
}
.dark .phase-step.current {
    background: rgba(var(--accent-violet-strong) / 0.14);
}
.phase-arrow {
    color: rgb(var(--text-faint));
    font-size: var(--fs-3xs); flex-shrink: 0;
    width: 16px; text-align: center;
}

/* ── Tab strip ── */
.doc-tab {
    padding: 10px 16px; font-size: var(--fs-sm); font-weight: 500;
    color: rgb(var(--text-muted));
    border-bottom: 2px solid transparent;
    transition: all 0.15s;
    white-space: nowrap;
}
.doc-tab:hover:not(:disabled) { color: rgb(var(--text-primary)); }
.doc-tab.active {
    color: rgb(var(--text-primary));
    border-bottom-color: rgb(var(--text-link));
    background: rgb(var(--surface-subtle));
}
.doc-tab:disabled {
    color: rgb(var(--text-faint));
    cursor: not-allowed; opacity: 0.5;
}
/* Link-styled tab: navigates to the lot's batch Shipping Order page instead
   of switching a panel — link-colored so it reads as navigation, not a
   fifth panel tab. */
.doc-tab.doc-tab-link { color: rgb(var(--text-link)); display: inline-flex; align-items: center; gap: 3px; }
.doc-tab.doc-tab-link:hover { color: rgb(var(--text-link)); text-decoration: underline; }

/* ── Unified form controls ──
   Single canonical styling for text inputs, selects, and textareas.
   Replaces ad-hoc .lot-field-input and per-template `px-3 py-2` /
   `px-2 py-1 text-xs` duplication. */
.form-input, .form-select, .form-textarea {
    display: block;
    width: 100%;
    padding: 8px 12px;
    font-size: var(--fs-sm);
    line-height: 1.4;
    color: rgb(var(--text-primary));
    background: rgb(var(--surface-card));
    border: 1px solid rgb(var(--border-input));
    border-radius: var(--radius-lg);
    transition: border-color 0.15s, box-shadow 0.15s;
}
/* --text-faint is deliberately held at the same RGB as muted text (see
   tokens.css) so a placeholder still clears WCAG AA contrast — color alone
   therefore can't distinguish a suggested default from an operator-set
   value. Italics carry that distinction structurally instead. */
.form-input::placeholder,
.form-textarea::placeholder {
    color: rgb(var(--text-faint));
    font-style: italic;
}
/* A select pre-showing its render-time default (nothing persisted yet — see
   the enum branch in _lot_rail_section.html) must not read as an
   operator-set value either; the shared change listener in layout.html
   removes this class the instant the operator actually picks something. */
.form-select.is-default {
    color: rgb(var(--text-muted));
    font-style: italic;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
    outline: none;
    border-color: rgb(var(--accent-blue-strong));
    box-shadow: 0 0 0 var(--ring-focus-width)
                rgb(var(--accent-blue-strong) / var(--ring-focus-alpha));
}
.form-input:disabled, .form-select:disabled, .form-textarea:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: rgb(var(--surface-inset));
}
/* Readonly fields are still focusable (for copy/select) but must not
   advertise themselves as editable — suppress the focus ring, and drop the
   text caret. The cursor rule covers the settings cards, which render
   read-only until Edit, and the producer-invoice link box. */
.form-input[readonly]:focus,
.form-select[readonly]:focus,
.form-textarea[readonly]:focus {
    border-color: rgb(var(--border-input));
    box-shadow: none;
}
.form-input[readonly],
.form-select[readonly],
.form-textarea[readonly] {
    cursor: default;
}
/* Currency-prefixed inputs (the rate-card form) carry a `pl-12` utility to clear
   the absolutely-positioned BDT/USD prefix, but `.form-input`'s own
   `padding: 8px 12px` is loaded after Tailwind and wins on equal specificity —
   collapsing the left pad so the value renders on top of the prefix
   ("BDT" + "1500.00" → garbled "B5OO.OO"). Restore the gap with two-class
   specificity, the same trick `.form-select.is-default` above relies on. */
.form-input.pl-12 {
    padding-left: 3rem;
}
/* Same trick for the icon-prefixed list-filter inputs (Lots/Bookings/Fleet). */
.form-input.pl-8 {
    padding-left: 2rem;
}
/* Size modifier: smaller dense variant (e.g. inline-editable cells) */
.form-input.form-input-sm,
.form-select.form-input-sm,
.form-textarea.form-input-sm {
    padding: 4px 8px;
    font-size: var(--fs-xs);
}
/* Content-tier widths for the dense container table. Each compact cell is as
   wide as its data type needs — and no wider — so the row reads as one tidy
   set instead of inputs at eyeballed pixel widths (the old w-20..w-36 mix).
   A new column picks a tier by what it HOLDS (code / size / number / date /
   short text), never a one-off width. This mirrors how the HBL's own packed
   cargo table sizes its cells. Width-only; pair with .form-input.form-input-sm
   for the control style. The compound .form-input.fw-* selector outranks
   .form-input's own width:100% (0,2,0 vs 0,1,0), so the tier always wins. */
.form-input.fw-size { width: 4.5rem; }   /* size code: 40HC, 20GP */
.form-input.fw-code,
.form-select.fw-code { width: 8.5rem; }   /* mono identifiers: seal, container no., mode */
.form-input.fw-num  { width: 6rem; }     /* quantities: cartons, weight, CBM, VGM */
.form-input.fw-date { width: 9.5rem; }   /* native date control needs picker room */
.form-input.fw-text { width: 9rem; }     /* short free text: depot name */
/* HBL signatory picker (tab-tools chrome): size to the selected name rather
   than the form-field default width:100%. It is inline chrome, not a full-width
   field, and the name length varies, so auto avoids both truncation and the
   circular flex sizing the 100% default would otherwise lean on. */
.form-select.sig-select { width: auto; }
/* Deep-link landing highlight: a field reached via its id in the URL hash
   (e.g. the lot page's "Set onboard date" chip → /bookings/<id>#bk-atd)
   wears the focus ring so it's unmistakable on arrival, instead of being
   lost among its siblings. Same treatment as :focus, so it reads as
   "this is the field you came for". */
.form-input:target,
.form-select:target,
.form-textarea:target {
    border-color: rgb(var(--accent-blue-strong));
    box-shadow: 0 0 0 var(--ring-focus-width)
                rgb(var(--accent-blue-strong) / var(--ring-focus-alpha));
}
/* Save / error affordances used by the inline editor */
.form-input.is-saved {
    border-color: rgb(var(--accent-green-strong));
    box-shadow: 0 0 0 var(--ring-focus-width)
                rgb(var(--accent-green-strong) / var(--ring-focus-alpha));
}
.form-input.is-error {
    border-color: rgb(var(--accent-red-strong));
    box-shadow: 0 0 0 var(--ring-focus-width)
                rgb(var(--accent-red-strong) / var(--ring-focus-alpha));
}

/* Transient save/error badge pinned to the top-right of a form
   field's wrapper. JS drops one in after HTMX auto-save requests. */
.save-badge {
    position: absolute;
    top: 0;
    right: 0;
    font-size: var(--fs-3xs);
    font-weight: 600;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    pointer-events: none;
    transition: opacity 0.25s ease-out;
    opacity: 1;
}
.save-badge.fade-out { opacity: 0; }
.save-badge-saved {
    background: rgb(var(--accent-green-soft-bg));
    color: rgb(var(--accent-green-soft-text));
}
.save-badge-error {
    background: rgb(var(--accent-red-soft-bg));
    color: rgb(var(--accent-red-soft-text));
}

/* ── Unified button component ──
   .btn is the base; add one .btn-{primary,secondary,danger,ghost}
   variant and an optional .btn-{sm,lg} size modifier. Default size
   is medium (px-4 py-2 text-sm). Disabled state is handled once
   here so all buttons share the same treatment. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 8px 16px;               /* md default */
    font-size: var(--fs-sm);
    font-weight: 500;
    line-height: 1.25;
    border-radius: var(--radius-lg);
    border: 1px solid transparent;
    cursor: pointer;
    transition: background-color 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s;
    white-space: nowrap;
    user-select: none;
}
/* Visual disabled state. :disabled adds pointer-events:none (native
   buttons shouldn't respond); .is-disabled is visual-only so spans
   and links using btn styling can keep their title/tooltip on hover. */
.btn:disabled, .btn.is-disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
.btn:disabled {
    pointer-events: none;
}
/* Size modifiers */
.btn-sm { padding: 4px 12px; font-size: var(--fs-xs); }
.btn-lg { padding: 10px 20px; font-size: var(--fs-base); }

/* Variants */
.btn-primary {
    background: rgb(var(--accent-blue-strong));
    color: #fff;
}
.btn-primary:hover:not(:disabled):not(.is-disabled) {
    background: rgb(var(--accent-blue-strong-hover));
}
.btn-secondary {
    background: rgb(var(--surface-card));
    color: rgb(var(--text-secondary));
    /* line-strong, not the paler line: a secondary button sat almost
       borderless on the warm paper background and read as plain text. */
    border-color: rgb(var(--border-input));
}
.btn-secondary:hover:not(:disabled):not(.is-disabled) {
    background: rgb(var(--surface-hover));
    color: rgb(var(--text-primary));
}
/* Tonal middle tier between the one solid primary and the quiet secondary:
   a soft terracotta fill for a genuine action button that needs to be found
   but must not compete with the screen's single primary (e.g. "+ Add
   document" / "+ Add container", where Generate is the primary). Hover
   commits to the solid signal. Uses the signal-50/200/700 chip tokens, so it
   flips for dark mode automatically. */
.btn-soft {
    background: rgb(var(--accent-blue-soft-bg));
    color: rgb(var(--accent-blue-soft-text));
    border-color: rgb(var(--accent-blue-soft-border));
}
.btn-soft:hover:not(:disabled):not(.is-disabled) {
    background: rgb(var(--accent-blue-strong));
    color: #fff;
    border-color: rgb(var(--accent-blue-strong));
}
.btn-ghost {
    background: transparent;
    color: rgb(var(--text-secondary));
}
.btn-ghost:hover:not(:disabled):not(.is-disabled) {
    background: rgb(var(--surface-hover));
    color: rgb(var(--text-primary));
}
.btn-danger {
    background: rgb(var(--accent-red-strong));
    color: #fff;
}
.btn-danger:hover:not(:disabled):not(.is-disabled) {
    background: rgb(var(--accent-red-strong-hover));
}
.btn-success {
    background: rgb(var(--accent-green-strong));
    color: #fff;
}
.btn-success:hover:not(:disabled):not(.is-disabled) {
    background: rgb(var(--accent-green-strong-hover));
}

/* ── Card surfaces ──
   Layered elevation for the app's main content cards. The inner ring
   (--shadow-ring) supplies a crisp 1px halo that replaces a heavy
   border; the outer shadow-sm gives subtle lift. Use instead of
   `border border-line shadow-sm`, not on top of it. */
.card-surface {
    background: rgb(var(--surface-card));
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-ring), var(--shadow-sm);
}
.card-surface-hover {
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.card-surface-hover:hover {
    box-shadow: var(--shadow-ring), var(--shadow-md);
}

/* ── Empty-state component ──
   A composed zero-state surface: centered icon + heading + description
   + optional action. Used by dashboard, inbox, bookings, billing when
   there is no data yet. Templates opt in by wrapping content in
   .empty-state; no rewrite required. */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 48px 24px;
    gap: 12px;
}
.empty-state .empty-icon {
    width: 48px; height: 48px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 999px;
    background: rgb(var(--surface-inset));
    color: rgb(var(--text-faint));
    margin-bottom: 4px;
}
.empty-state .empty-icon svg { width: 24px; height: 24px; }
.empty-state .empty-title {
    font-size: var(--fs-base); font-weight: 600;
    color: rgb(var(--text-primary));
    letter-spacing: -0.01em;
}
.empty-state .empty-description {
    font-size: var(--fs-sm); line-height: 1.5;
    color: rgb(var(--text-muted));
    max-width: 42ch;
}
.empty-state .empty-actions {
    display: flex; gap: 8px; margin-top: 8px;
}

/* ── App sidebar: static rail on desktop, off-canvas drawer on mobile ──
   #app-sidebar is a normal in-flow flex child at lg (1024px) and up — the
   laptop/desktop target, unchanged. Below lg it becomes a fixed drawer that
   slides in from the left over the content, toggled by the hamburger in the
   mobile top bar (layout.html). The transition is neutralized by the global
   prefers-reduced-motion rule in
   base.css. The backdrop is display:none on desktop so a stale .open class
   (e.g. after a resize from mobile) can never dim the desktop layout. */
#app-sidebar-backdrop { display: none; }
@media (max-width: 1023px) {
    #app-sidebar {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 50;
        transform: translateX(-100%);
        /* visibility:hidden takes the closed drawer out of the tab order and
           the accessibility tree (translate + pointer-events alone leave its
           links keyboard/SR-reachable off-screen). The delayed visibility
           transition keeps it on screen for the full slide-out before hiding. */
        visibility: hidden;
        transition: transform 0.2s ease-out, visibility 0s linear 0.2s;
    }
    #app-sidebar.open {
        transform: translateX(0);
        visibility: visible;
        box-shadow: 4px 0 24px oklch(0 0 0 / 0.18);
        transition: transform 0.2s ease-out;
    }
    #app-sidebar-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 40;
        background: oklch(0 0 0 / 0.35);
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.2s ease-out;
    }
    #app-sidebar-backdrop.open {
        opacity: 1;
        pointer-events: auto;
    }
}

/* Sidebar search — keyboard-focused result row. A slightly darker
   background distinguishes it from mouse hover so users can see which
   row Enter will open without a separate stripe or border accent. */
#nav-search-results .palette-row-active {
    background: rgb(var(--surface-active));
}

/* Sidebar search dropdown width. On desktop the dropdown deliberately extends
   ~196px past the 224px rail (left-3 anchor + this fixed width) so result lines
   don't truncate identifiers. Scoped to lg+ on purpose: below lg the rail is a
   fixed ~224px drawer, so without this the dropdown falls back to its
   left-3/right-3 stretch and stays inside the drawer instead of overflowing the
   phone viewport. */
@media (min-width: 1024px) {
    #nav-search-dropdown { width: 420px; }
}

/* In-flight HTMX mutation marker. Applied by the global double-click guard
   in layout.html on POST/PUT/PATCH/DELETE — the button is also `disabled`,
   the visual treatment just makes the wait obvious. Forms get the same
   class but stay clickable so input edits aren't blocked while a sibling
   request is in flight. */
.cf-busy {
    opacity: 0.6;
    cursor: wait !important;
}
button.cf-busy::after {
    content: " …";
}

/* ── Per-field source line ──
   Renders under every editable HBL field. Three visual states:
   - Locked (manual edit): amber tint + lock icon + unlock affordance.
   - Parser-set: muted neutral + doc icon + relative time.
   - Empty: invisible div kept for HTMX swap target stability.
   The line is small, single-row, and intentionally low-contrast — it's
   reference info, not a primary action. Provenance and lock state are
   the meaning; visual weight stays on the input itself. */
.field-src {
    min-height: 1.125rem;
    margin-top: 4px;
}
.field-src-line {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-2xs);
    line-height: 1.3;
    color: rgb(var(--text-muted));
}
.field-src-parser { color: rgb(var(--text-muted)); }
.field-src-err {
    color: rgb(var(--accent-red-soft-text));
    font-weight: 500;
}
/* Attribution-only treatment: same muted typography as the parser line
   so a locked row doesn't appear visually heavier than its unlocked
   neighbour. The lock icon next to the label is the primary "this is
   locked" signal; the line below is attribution and shouldn't compete
   with section dividers for the eye's attention. The earlier amber
   background tint made adjacent rows look like they belonged to
   different sections — fix is to drop the chip styling entirely. */
.field-src-locked {
    color: rgb(var(--text-muted));
}
.field-src-icon {
    width: 11px;
    height: 11px;
    flex-shrink: 0;
    opacity: 0.8;
}
/* Attribution, not a call to action: the document link inherits the muted
   line colour so dozens of "from <doc>" lines don't read as scattered blue
   download links (the header's Documents drawer is the one place to browse
   files). The dotted underline keeps the click affordance discoverable;
   hover restores full link colour. */
.field-src-link {
    color: inherit;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-decoration-color: rgb(var(--border-input));
    text-underline-offset: 2px;
}
.field-src-link:hover {
    color: rgb(var(--text-link-hover));
    text-decoration-style: solid;
    text-decoration-color: currentColor;
}
.field-src-by {
    color: rgb(var(--text-secondary));
    border-bottom: 1px dotted rgb(var(--border-default) / 0.7);
    cursor: help;
}
.field-src-action {
    display: inline;
    margin-left: 2px;
}
.field-src-btn {
    background: transparent;
    border: 0;
    padding: 0;
    font-size: var(--fs-2xs);
    line-height: 1.3;
    color: rgb(var(--text-link));
    cursor: pointer;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 2px;
}
.field-src-btn:hover { color: rgb(var(--text-link-hover)); }

/* Container cell manual-edit marker. The container table is dense and its
   default state is "no edits", so the marker only appears on a manually
   edited cell — and when it does it must read clearly. Medium weight + the
   secondary (not muted) ink make an operator override stand out against the
   row's origin line and the parser-set neighbours, while reusing the
   .field-src-line layout/icon. Gap before the unlock button keeps it tappable. */
.lc-edit {
    flex-wrap: wrap;
    gap: 4px;
    font-weight: 500;
    color: rgb(var(--text-secondary));
}
.lc-edit .field-src-icon { opacity: 1; }

/* ────────────────────────────────────────────────────────── */
/* lot_detail Pass 1 chrome: booking strip, tab-tools,         */
/* missing-fields strip, sources drawer, scope badges          */
/* ────────────────────────────────────────────────────────── */

/* One-line booking strip above the workspace card. Replaces the
   pre-PR free-form booking section. The change-MBL picker (still
   the existing form) opens from the "change MBL" affordance. */
.lot-booking-strip {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    font-size: var(--fs-sm);
    margin-bottom: 16px;
    color: rgb(var(--text-muted));
}
.lot-booking-strip .booking-label {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: rgb(var(--text-faint));
    margin-right: 2px;
}
.lot-booking-strip .booking-mbl {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-weight: 600;
    color: rgb(var(--text-link));
    text-decoration: none;
    border-bottom: 1px dotted rgb(var(--border-default));
    padding-bottom: 1px;
}
.lot-booking-strip .booking-mbl:hover {
    color: rgb(var(--text-link-hover));
    border-bottom-color: rgb(var(--text-link-hover));
}
.lot-booking-strip .booking-sep {
    color: rgb(var(--text-faint));
}
.lot-booking-strip .booking-fact {
    font-size: var(--fs-xs);
    color: rgb(var(--text-secondary));
}
/* Inline carrier picker — reads like a booking-fact but is editable. A quiet
   bordered chip that gains a link-coloured edge on hover/focus so the
   affordance is discoverable without shouting in the summary strip. */
.lot-booking-strip .booking-carrier-select {
    font-size: var(--fs-xs);
    color: rgb(var(--text-secondary));
    background-color: transparent;
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-md);
    padding: 1px 6px;
    max-width: 22ch;
    cursor: pointer;
}
.lot-booking-strip .booking-carrier-select:hover {
    border-color: rgb(var(--text-link));
    color: rgb(var(--text-primary));
}
.lot-booking-strip .booking-carrier-select:focus-visible {
    outline: 2px solid rgb(var(--text-link));
    outline-offset: 1px;
}

/* Tab-tools row: its own slim row under the document tabs, holding the
   watermark toggle, the FI Revised tick, Generate/XLS/Originals, and the
   "More" overflow menu (signatory, Email, Sources, Activity — the
   rarely-used tools live behind that one door). It must never share a row
   with the tabs — the old ~880px of controls crushed the strip to ~126px
   at 1280 and hid three of the four tabs. Right-aligned (Generate keeps
   its corner), wrapping when even a full row is too narrow. */
.tab-tools {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 8px;
    padding: 6px 12px;
}
/* Left group of the same row: Documents, Add document, Split into parts. The
   inputs a shipment was built from sit left; its outputs (Generate and the
   downloads) keep the right corner. The dropdown panels open leftward-anchored
   here, or they would run off the card's left edge. */
.tab-tools-sources {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-right: auto;
}
.tab-tools-sources .lot-add-doc-panel {
    right: auto;
    left: 0;
}
/* The HBL confirmation slot rides this row too (pill plus one button). Its one
   band, the refusal of a confirm on a moved bill, arrives by swap and lists
   what changed, so it takes a full line of its own under the tools. */
.tab-tools .state-prompt {
    flex-basis: 100%;
    border: 1px solid rgb(var(--border-subtle));
    border-radius: var(--radius-md);
    padding: 8px 12px;
}

/* "More" overflow-menu summary — same visual family as the other small
   bordered chips in the tab-tools row. (Originally the Sources/Activity
   toggle buttons; those now live as rows inside the menu this chip opens.) */
.src-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-2xs);
    color: rgb(var(--text-secondary));
    /* paper-2, not the card colour: the toggle sat on a same-coloured
       container and only its pale border showed. A gentle fill makes it
       read as a button at rest. */
    background: rgb(var(--surface-subtle));
    border: 1px solid rgb(var(--border-input));
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.src-btn:hover {
    color: rgb(var(--text-primary));
    background: rgb(var(--surface-inset));
}

/* Brief flash when a missing field is jumped-to. JS adds the
   class for ~1.2s, then removes it. */
.is-jump-target {
    animation: lot-jump-pulse 1200ms ease-out;
}
@keyframes lot-jump-pulse {
    0%   { background: rgb(var(--accent-amber-soft-bg)); }
    100% { background: transparent; }
}

/* Sources drawer: right-edge overlay inside the workspace card.
   Toggled from the header's "Documents · N" button. The Activity drawer
   (.activity-drawer) is its sibling and shares the same shell, head,
   close button, sub-line, and backdrop. */
.sources-drawer,
.activity-drawer {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 320px;
    max-width: 90%;
    background: rgb(var(--surface-card));
    border-left: 1px solid rgb(var(--border-default));
    box-shadow: -10px 0 30px rgb(0 0 0 / 0.06);
    padding: 18px 20px 22px;
    display: flex;
    flex-direction: column;
    z-index: 25;
    transform: translateX(100%);
    transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.sources-drawer.is-open,
.activity-drawer.is-open {
    transform: translateX(0);
}
.sources-drawer .sd-head,
.activity-drawer .sd-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 6px;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgb(var(--text-muted));
}
.sources-drawer .sd-close,
.activity-drawer .sd-close {
    margin-left: auto;
    background: transparent;
    border: 0;
    color: rgb(var(--text-muted));
    font-size: var(--fs-base);
    cursor: pointer;
    padding: 2px 6px;
    border-radius: var(--radius-sm);
}
.sources-drawer .sd-close:hover,
.activity-drawer .sd-close:hover {
    color: rgb(var(--text-primary));
    background: rgb(var(--surface-hover));
}
.sources-drawer .sd-sub,
.activity-drawer .sd-sub {
    font-size: var(--fs-xs);
    color: rgb(var(--text-muted));
    margin-bottom: 12px;
}
.sources-drawer .sd-scroll {
    overflow-y: auto;
    flex: 1;
}
.sources-drawer .sd-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
/* One smallcaps heading per document kind, so the operator hunting "the CFS
   report" scans headings instead of the whole upload history. */
.sources-drawer .sd-group-head {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgb(var(--text-link));
    margin: 14px 0 2px;
}
.sources-drawer .sd-group-head:first-child {
    margin-top: 2px;
}
/* The fields a document filled in — sans, wrapping, quiet. */
.sources-drawer .sd-fields {
    font-size: var(--fs-2xs);
    color: rgb(var(--text-secondary));
}
.sources-drawer .sd-row {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 10px 0;
    border-bottom: 1px solid rgb(var(--border-subtle));
    text-decoration: none;
    color: inherit;
}
.sources-drawer .sd-row:last-child {
    border-bottom: 0;
}
/* One file's row: the name/date block stays the download link and grows, the
   Remove / Put back button sits beside it without stealing the click target.
   The border override keeps the separator behaviour the drawer already had —
   .sd-row stops being :last-child the moment a button joins it. */
.sources-drawer .sd-item {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}
.sources-drawer .sd-item .sd-row {
    flex: 1;
    min-width: 0;
    border-bottom: 0;
}
.sources-drawer .sd-act {
    padding: 10px 0;
    flex: none;
}
/* On a confirmed bill the removal asks why first, so the action takes a full
   row of its own under the file rather than squeezing a text box in beside
   the name. */
.sources-drawer .sd-item {
    flex-wrap: wrap;
}
.sources-drawer .sd-act-reason {
    flex: 1 0 100%;
    display: flex;
    align-items: center;
    gap: 6px;
    padding-top: 0;
}
.sources-drawer .sd-reason {
    flex: 1;
    min-width: 0;
    font-family: inherit;
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
    padding: 6px 8px;
    border: 1px solid rgb(var(--border-input));
    border-radius: var(--radius-sm);
    background: rgb(var(--surface-card));
}
/* Remove is a rare correction on a reference surface, so it sits quiet
   (ghost, red ink) until hovered, and only its armed "click again" state
   turns loud - the same warm-red arm treatment the freeze buttons use. */
.sources-drawer .sd-remove {
    color: rgb(var(--accent-red-strong));
}
.sources-drawer .sd-act .sd-remove:hover:not(.is-armed) {
    background: rgb(var(--surface-hover));
    color: rgb(var(--accent-red-strong-hover));
}
.sources-drawer .sd-act .btn.is-armed {
    background: var(--armed-bg);
    color: var(--armed-ink);
    border-color: var(--armed-border);
    animation: lot-arm-pulse 1s ease-in-out infinite;
}
/* Removed files sit last and read quieter than the live sources above them. */
.sources-drawer .sd-removed-head {
    color: rgb(var(--text-muted));
    margin-top: 20px;
}
.sources-drawer .sd-removed-sub {
    margin-bottom: 0;
}
.sources-drawer .sd-removed-head ~ .sd-list .sd-name {
    color: rgb(var(--text-muted));
}
.sources-drawer .sd-row:hover .sd-name {
    color: rgb(var(--text-link));
}
.sources-drawer .sd-name {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
    font-weight: 500;
    word-break: break-all;
    transition: color 0.15s;
}
.sources-drawer .sd-meta {
    font-size: var(--fs-2xs);
    color: rgb(var(--text-muted));
}

/* "Add document" header control — the fallback for when AI auto-linking can't
   land the right lot. A <details> dropdown in the lot header (this used to be
   buried in the Sources drawer): the operator uploads a producer invoice /
   off-dock report directly to this lot with a required justification. The
   summary is the button; the panel is the dropdown form. */
.lot-add-doc {
    position: relative;
}
/* Soft terracotta tonal, matching .btn-soft: the add-document fallback was an
   outline button that disappeared into the paper background. It's the action
   in the header (Generate, the primary, lives in the doc toolbar), so it earns
   a tonal fill that hover/[open] commits to the solid signal. */
.lot-add-doc-summary {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-xs);
    font-weight: 600;
    /* Geometry mirrors .btn.btn-sm — this summary sits beside the
       "Documents · N" button (a real .btn btn-sm) in the header, and the
       pair must read as one group, not two vocabularies. */
    line-height: 1.25;
    color: rgb(var(--accent-blue-soft-text));
    background: rgb(var(--accent-blue-soft-bg));
    border: 1px solid rgb(var(--accent-blue-soft-border));
    border-radius: var(--radius-lg);
    padding: 4px 12px;
    cursor: pointer;
    white-space: nowrap;
    list-style: none;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.lot-add-doc-summary::-webkit-details-marker {
    display: none;
}
.lot-add-doc-summary:hover,
.lot-add-doc[open] .lot-add-doc-summary {
    background: rgb(var(--accent-blue-strong));
    border-color: rgb(var(--accent-blue-strong));
    color: #fff;
}
.lot-add-doc-panel {
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    z-index: 30;
    width: 24rem;
    max-width: 90vw;
    padding: 14px;
    background: rgb(var(--surface-card));
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
}
/* The Email panel: several addresses per box, so wide enough to read them
   (owner, 2026-09-15). .email-box is the textarea shown without scripting
   (chips.js hides it behind the chip box, which wraps): it grows with its
   text, rows= the floor where field-sizing is not supported. */
.lot-add-doc-panel--wide { width: 36rem; }
.email-box { field-sizing: content; min-height: 2.5rem; max-height: 10rem; }
/* The offer to send the shipper's other bills on the sailing together: the
   signal family, an invitation rather than a warning. */
/* The Email panel's stage line: which email this is. */
.email-stage {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
    font-size: var(--fs-xs);
    color: rgb(var(--text-secondary));
}

/* The Email panel's read-only preview of the email as the reader gets it. */
.email-preview-box:not(:empty) {
    margin-top: 0.5rem;
    padding: 0.75rem;
    border: 1px solid rgb(var(--border-subtle));
    border-radius: var(--radius-md);
    background: rgb(var(--surface-subtle));
}

.email-preview-subject {
    margin: 0 0 0.5rem;
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
}

.email-preview-subject span {
    color: rgb(var(--text-muted));
    margin-right: 0.25rem;
}

.email-preview-body {
    margin: 0;
    white-space: pre-wrap;
    font-family: inherit;
    font-size: var(--fs-xs);
    line-height: 1.5;
    color: rgb(var(--text-primary));
}

.email-nudge {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border-radius: var(--radius-lg);
    background: rgb(var(--accent-blue-soft-bg));
    border: 1px solid rgb(var(--accent-blue-soft-border));
    color: rgb(var(--accent-blue-soft-text));
    font-size: var(--fs-xs);
}
.lot-add-doc-hint {
    font-size: var(--fs-2xs);
    line-height: 1.5;
    color: rgb(var(--text-muted));
    margin: 0 0 10px;
}
/* Pin-upload form internals — shared by the header control. Unscoped (the form
   moved out of the Sources drawer to the header dropdown). */
.sd-pin-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.sd-pin-label {
    font-size: var(--fs-2xs);
    font-weight: 500;
    color: rgb(var(--text-secondary));
}
.sd-pin-req {
    color: rgb(var(--text-muted));
    font-weight: 400;
}
.sd-pin-types {
    display: flex;
    flex-direction: column;
    gap: 5px;
    margin: 0;
    padding: 0;
    border: 0;
}
.sd-pin-types .sd-pin-label {
    margin-bottom: 2px;
}
.sd-pin-type {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
    cursor: pointer;
}
.sd-pin-type.is-disabled {
    color: rgb(var(--text-muted));
    cursor: not-allowed;
}
.sd-pin-order-note {
    font-size: var(--fs-2xs);
    line-height: 1.5;
    color: rgb(var(--accent-amber-soft-text));
    margin: 2px 0 0;
}
.sd-pin-just {
    width: 100%;
    font-family: inherit;
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
    padding: 6px 8px;
    border: 1px solid rgb(var(--border-input));
    border-radius: var(--radius-sm);
    background: rgb(var(--surface-card));
    resize: vertical;
}
.sd-pin-file {
    font-size: var(--fs-2xs);
    color: rgb(var(--text-secondary));
}
.sd-pin-btn {
    align-self: flex-start;
    font-size: var(--fs-xs);
    font-weight: 600;
    color: #fff;
    padding: 7px 12px;
    border: 0;
    border-radius: var(--radius-sm);
    background: rgb(var(--accent-blue-strong));
    cursor: pointer;
    transition: background 0.15s;
}
.sd-pin-btn:hover {
    background: rgb(var(--accent-blue-strong-hover));
}
.sd-pin-btn:disabled {
    opacity: 0.6;
    cursor: progress;
}
.sd-pin-ok {
    font-size: var(--fs-2xs);
    line-height: 1.5;
    color: rgb(var(--text-secondary));
    padding: 8px 10px;
    border: 1px solid rgb(var(--border-subtle));
    border-radius: var(--radius-sm);
    background: rgb(var(--surface-subtle));
}
.sd-pin-refresh {
    display: inline-block;
    margin-top: 6px;
    font-size: var(--fs-2xs);
    font-weight: 600;
    color: rgb(var(--text-link));
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
}
.sources-drawer-backdrop,
.activity-drawer-backdrop {
    position: absolute;
    inset: 0;
    background: rgb(0 0 0 / 0.10);
    z-index: 24;
    opacity: 0;
    pointer-events: none;
    transition: opacity 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.sources-drawer-backdrop.is-open,
.activity-drawer-backdrop.is-open {
    opacity: 1;
    pointer-events: auto;
}

/* Activity drawer rows: who/what/when. Mirrors .sd-row spacing/dividers,
   but the description is a full sentence (body font, not mono) above a
   mono meta line that leads with the actor. */
.activity-drawer .ad-list {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    flex: 1;
}
.activity-drawer .ad-row {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 10px 0;
    border-bottom: 1px solid rgb(var(--border-subtle));
}
.activity-drawer .ad-row:last-child {
    border-bottom: 0;
}
.activity-drawer .ad-desc {
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
    line-height: 1.4;
}
.activity-drawer .ad-meta {
    font-size: var(--fs-2xs);
    color: rgb(var(--text-muted));
}
.activity-drawer .ad-actor {
    color: rgb(var(--text-secondary));
    font-weight: 600;
}
.activity-drawer .ad-empty {
    font-size: var(--fs-xs);
    color: rgb(var(--text-muted));
}

/* =========================================================================
   Editor-primary layout on lot_detail
   ========================================================================= */

/* The lot workspace uses the standard global content cap
   (max-w-screen-2xl, 1536px) like every other page — the previous
   full-bleed escape hatch existed to give the sticky-right preview
   column enough horizontal room, but the preview is now stacked at
   the top so the editor rail benefits from the same readable width
   as bookings / lots / settings. */


/* Workspace card containment. The sources and activity drawers
   (.sources-drawer / .activity-drawer) are position:absolute with a
   transform that parks them 320px outside the card's right edge while
   closed — that overflow used to bubble up to the page and create a
   horizontal scrollbar. Clip the card on the X axis to contain it.
   `overflow-x: clip` (not `hidden`) keeps the preview pane's
   `position: sticky` behaviour working — `clip` doesn't establish a
   scroll port. */
#lot-workspace {
    overflow-x: clip;
}

/* Preview-on-top layout. Operators mostly *review* docs the inbox already
   prepared; edits are the exception. So the preview gets the visible
   real estate, the editor sits below as the escape hatch. Sized for
   small-laptop screens (≈1366×768) where the preview needs ~65vh to be
   readable at A4 scale. */
.lot-split {
    display: flex;
    flex-direction: column;
    position: relative;
}
.lot-editor-rail {
    background: rgb(var(--surface-card));
    min-width: 0;
}

/* Inline document preview viewport — the fixed-height box a generated PDF
   is streamed into inside a document panel. 78vh is the app's one preview
   height (lot, booking, shipping order): tall enough to read a full page
   without the panel shoving the record's own fields off screen. The
   positioning is what the absolutely-placed iframe and its loading overlay
   anchor to.

   This exists so a panel doesn't need `style="height: 78vh"` — the
   inline-style budget in TestTemplatesStyleGuard is tracked debt, not a
   pattern to copy. booking_detail.html still spends one of its three there
   and can adopt this class when that debt is paid down. */
.doc-preview-frame {
    position: relative;
    height: 78vh;
}

/* Preview pane. The iframe streams the real generated PDF; the browser's
   PDF viewer lays the pages out and scrolls inside the fixed-height frame
   (no scale-to-fit JS — that was for the old HTML preview, which had no
   pagination of its own). 78vh matches the booking page's preview. */
.lot-preview-pane {
    position: relative;
    background: rgb(var(--surface-subtle));
    padding: 12px;
}
.lot-preview-frame {
    position: relative;
    width: 100%;
    height: 78vh;
    margin: 0 auto;
    overflow: hidden;
    background: var(--paper-white);
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-md);
    box-shadow: 0 1px 2px oklch(0 0 0 / 0.04);
}

/* Preview action chips — anchored to the top-right of the preview pane.
   "Full size" opens the same preview PDF in a browser tab (the operator's
   zoom lever for the 8pt-dense HBL on small laptops); "Edit" jumps to the
   rail below — most sessions are review-only, so neither earns more than
   a chip. */
.lot-preview-actions {
    position: absolute;
    top: 18px;
    right: 18px;
    display: flex;
    gap: 8px;
    z-index: 2;
}
.lot-preview-jump {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: rgb(var(--text-secondary));
    background: rgb(var(--surface-card));
    border: 1px solid rgb(var(--border-input));
    border-radius: var(--radius-md);
    cursor: pointer;
    box-shadow: 0 1px 2px oklch(0 0 0 / 0.04);
    transition: color 0.15s, border-color 0.15s;
}
.lot-preview-jump:hover {
    color: rgb(var(--text-primary));
    border-color: rgb(var(--text-faint));
}
.lot-preview-jump:focus-visible {
    outline: 2px solid rgb(var(--text-link));
    outline-offset: 1px;
}
.lot-preview-jump svg {
    width: 11px;
    height: 11px;
}
.lot-preview-frame iframe#doc-preview-frame {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    background: var(--paper-white);
}
@media (max-width: 1023px) {
    .lot-preview-pane { padding: 0; }
    .lot-preview-frame { border: none; border-radius: 0; border-bottom: 1px solid rgb(var(--border-default)); }
}

/* Locked state when the lot passes its post-departure lock (booking ATD +
   10 days — repository/lot_lock.go). Inputs visually go grey and pointer
   events are disabled; the lock banner above the rail carries the admin
   unlock affordance.
   A locked lot dims + locks the editor rail. We dim the rail's sections
   individually (not the container) so one section can keep its own marker:
   a child cannot beat its parent's opacity, so container-level opacity
   would grey out `.rail-freeze-exempt` (the freight-invoice billing
   section) twice. That section is still frozen by the lot lock (strict FI,
   client decision 2026-07-19) via its own .is-locked marker below — the
   split survives so the FI section can carry its distinct explanatory note
   and its separate FI-approved freeze. */
.lot-editor-rail[data-locked="1"] > *:not(.rail-freeze-exempt) {
    opacity: 0.55;
    pointer-events: none;
    filter: saturate(0.6);
}

/* The freight-invoice charge editor (.rail-freeze-exempt) dims + goes
   non-interactive when the lot is locked, when the freight invoice is itself
   approved, or when the viewer isn't an accountant or admin (the
   freightInvoice gate). Same visual treatment as the locked-rail dim. The
   server is the real guard (lotFieldEditError); this is only the cue. The
   .is-locked marker is set server-side in the template. */
.rail-freeze-exempt.is-locked {
    opacity: 0.55;
    pointer-events: none;
    filter: saturate(0.6);
}

/* Small hint that explains why this one section stays live in a locked rail. */
.rail-freeze-exempt-note {
    margin: 0;
    padding: 8px 24px 0;
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--freeze-ink);
}

/* Freight Invoice Charges is a per-lot charge editor with no bearing on the
   HBL / Shipment Advice / Shipping Order previews, so it only shows when the
   operator is on the Freight Invoice tab. Keyed on the workspace's
   data-active-doc — the same lever the per-doc readiness gates use — so the
   visibility flips with the tab in pure CSS. Default (HBL) load hides it. */
#lot-workspace:not([data-active-doc="freight-invoice"]) [data-fi-only="1"] {
    display: none;
}

/* Shipment Advice header overrides (Mode of Shipment, Terms of Shipment) print
   on no other output doc, so the section only shows on the Shipment Advice tab.
   Same data-active-doc lever as the FI-charges block above. The wrapper is not a
   direct-child section.rail-section, so the "hide all sections" rule below
   (which also covers the SA tab) leaves it alone; this rule hides it everywhere
   but the SA tab. */
#lot-workspace:not([data-active-doc="shipment-advice"]) [data-sa-only="1"] {
    display: none;
}

/* The flip side: on the Freight Invoice AND Shipment Advice tabs the rail shows
   ONLY that doc's own block (the data-fi-only / data-sa-only wrapper above) —
   the HBL field sections (Identifiers, Cargo, Commercial Invoice, Export &
   Customs, Shipper, Consignee & Notify, Containers) are hidden so each doc's
   edit surface isn't tangled with HBL edits. Those sections are direct-child
   <section.rail-section> elements; the readiness gates (.rail-doc-gate) and the
   nested doc-specific wrapper are untouched. Their data still feeds the doc
   (it's all lot/HBL-derived) and is edited on the HBL tab; a readiness-gate jump
   from these tabs switches back to HBL first (scrollRailToField). */
#lot-workspace[data-active-doc="freight-invoice"] .lot-editor-rail > section.rail-section,
#lot-workspace[data-active-doc="shipment-advice"] .lot-editor-rail > section.rail-section {
    display: none;
}

/* Sealed-state line. The lot lock and HBL confirmation render it in the lot
   title row (see .lot-title-row above); the FI approval renders it between the
   tab strip and the workspace body. One compact full-width line: the pill +
   tint still announce the state, but the steady-state row does not compete
   with the controls around it (the lock is read far more often than it is
   acted on — the unlock control hides behind a disclosure). The class also
   keys the JS re-derivation — id-scoped
   (#lot-lock-banner / #fi-approval-banner), because .freeze-banner itself
   renders in three "sealed" branches: lock locked, FI approved, and HBL
   confirmed. */
.freeze-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 18px;
    background: var(--freeze-bg);
    border-bottom: 1px solid var(--freeze-border);
    font-size: var(--fs-xs);
    color: var(--freeze-ink);
}
.freeze-banner .fb-pill {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-weight: 800;
    font-size: var(--fs-2xs);
    letter-spacing: 0.10em;
    padding: 3px 8px;
    background: var(--freeze-accent-deep);
    color: var(--freeze-on-accent);
    border-radius: var(--radius-sm);
}
.freeze-banner .fb-text { flex: 1; }
.freeze-banner .fb-by { font-weight: 600; }
.freeze-banner .fb-btn,
.state-prompt .fb-btn {
    background: var(--paper-white);
    border: 1px solid var(--freeze-accent);
    color: var(--freeze-ink);
    font-size: var(--fs-xs);
    font-weight: 600;
    padding: 5px 12px;
    border-radius: var(--radius-md);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s;
}
.freeze-banner .fb-btn:hover,
.state-prompt .fb-btn:hover:not(.is-armed) {
    background: var(--freeze-accent);
    color: var(--freeze-on-accent);
}
.fb-btn.is-armed {
    background: var(--armed-bg);
    color: var(--armed-ink);
    border-color: var(--armed-border);
    animation: lot-arm-pulse 1s ease-in-out infinite;
}

/* The admin unlock control inside the locked line: a disclosure whose summary
   is the only thing rendered at rest — the reason input + submit appear on
   demand (the unlock is a rare exception flow, not a standing form). Typing
   the mandatory reason IS the deliberate two-step, so no armed confirm here. */
.fb-disclosure {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}
.fb-disclosure > summary {
    list-style: none;
    display: inline-flex;
}
.fb-disclosure > summary::-webkit-details-marker { display: none; }
.fb-unlock-form {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}
.fb-reason-input {
    font-size: var(--fs-xs);
    padding: 5px 10px;
    width: 15rem;
    border: 1px solid var(--freeze-accent);
    border-radius: var(--radius-md);
    background: var(--paper-white);
    color: rgb(var(--text-primary));
}
.fb-reason-input::placeholder { color: rgb(var(--text-muted)); }
.fb-reason-input:focus {
    outline: 2px solid var(--freeze-accent);
    outline-offset: 1px;
}
/* Inline rejection under the unlock form (blank reason). */
.fb-error {
    flex-basis: 100%;
    margin: 0;
    font-size: var(--fs-2xs);
    font-weight: 600;
    color: var(--armed-ink);
}
.freeze-banner:has(.fb-error) { flex-wrap: wrap; }
@keyframes lot-arm-pulse {
    /* --armed-bg at alpha; literal because var() OKLCH can't take an
       alpha channel without relative color syntax. */
    0%, 100% { box-shadow: 0 0 0 0 oklch(0.55 0.18 30 / 0.5); }
    50%      { box-shadow: 0 0 0 4px oklch(0.55 0.18 30 / 0); }
}

/* ── lot_detail title row ──
   One header band: the lot number, its lifecycle badge, the shipment-state
   pills. The two state slots (#lot-lock-banner, #hbl-confirm-banner) are
   `display: contents` wrappers so whichever branch they render becomes a
   direct flex item here, a pill beside the badge in every state but one (the
   confirm refused on a moved bill, which claims a full line). The wrappers
   keep their ids because the HTMX unlock / confirm swaps target them. The row
   itself is the shared .page-title-row; .lot-title-row only scopes the rules
   below to it. */
.lot-state-slot { display: contents; }
/* Every state: a pill and, at most, one small button. No fill, no rule: it is a
   label in a header, not a banner. A div, not a span: the locked branch holds a
   disclosure, a form and a refusal line. Wraps so those can drop under the pill
   on a narrow viewport. */
.lot-state-chip {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.lot-state-by {
    font-size: var(--fs-xs);
    color: rgb(var(--text-muted));
}
/* The one band left in the title row: the refusal of a confirm on a moved bill,
   which arrives in answer to a click and lists what changed. The base
   .state-prompt has only a bottom rule (it was welded to the workspace panel);
   here it stands alone and needs a full edge. */
.lot-title-row .state-prompt {
    flex-basis: 100%;
    border-radius: var(--radius-md);
    padding: 8px 12px;
    border: 1px solid rgb(var(--border-subtle));
}
.lot-title-row .state-prompt--override {
    border-color: rgb(var(--accent-amber-soft-border));
}

/* Informational lot-state line — the mirror of .freeze-banner for the
   pending / grace / overridden states. Same compact one-line shape, but an
   even calmer surface (no fill at all): a status, not a lock. Only one
   banner branch ever renders (they share the slot). */
.state-prompt {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 18px;
    background: transparent;
    border-bottom: 1px solid rgb(var(--border-subtle));
    font-size: var(--fs-xs);
    color: rgb(var(--text-secondary));
}
.state-prompt .sp-pill,
.lot-state-chip .sp-pill,
.email-stage .sp-pill {
    font-weight: 700;
    font-size: var(--fs-2xs);
    letter-spacing: 0.05em;
    padding: 3px 8px;
    background: rgb(var(--surface-card));
    color: rgb(var(--text-muted));
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-sm);
    white-space: nowrap;
}
/* The pill's hue is the state: green for a confirmed bill, bronze for the
   departure lock (same fill the FI approval banner uses), amber for an admin
   override. The grey base pill stays for the waiting states. Declared after the
   base rule above, which it shares specificity with. */
.state-prompt .sp-pill--ok,
.lot-state-chip .sp-pill--ok,
.email-stage .sp-pill--ok {
    background: rgb(var(--accent-green-soft-bg));
    color: rgb(var(--accent-green-soft-text));
    border-color: rgb(var(--accent-green-soft-border));
}
.state-prompt .sp-pill--warn,
.lot-state-chip .sp-pill--warn {
    background: rgb(var(--accent-amber-soft-bg));
    color: rgb(var(--accent-amber-soft-text));
    border-color: rgb(var(--accent-amber-soft-border));
}
.lot-state-chip .sp-pill--sealed {
    background: var(--freeze-accent-deep);
    color: var(--freeze-on-accent);
    border-color: var(--freeze-accent-deep);
}
.state-prompt .sp-text { flex: 1; }
/* "What was sent": the subject and the full address lists of an emailed
   document, folded away. The line itself names one address and counts the
   rest — a bill copied to the office was twenty addresses of wall. */
.state-prompt .sp-more { display: inline; margin-left: 6px; }
/* Opened, the block is tall: the pill belongs beside the first line. */
.state-prompt:has(.sp-more[open]) { align-items: flex-start; }
.state-prompt .sp-more > summary {
    display: inline;
    cursor: pointer;
    color: rgb(var(--text-link));
}
.state-prompt .sp-more > summary:hover { text-decoration: underline; }
.state-prompt .sp-more-body {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: 2px 10px;
    margin: 6px 0 4px;
}
.state-prompt .sp-more-body dt {
    font-size: var(--fs-2xs);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: rgb(var(--text-muted));
}
.state-prompt .sp-more-body dd {
    margin: 0;
    color: rgb(var(--text-primary));
    overflow-wrap: anywhere;
}
.state-prompt .sp-text .mono { color: rgb(var(--text-primary)); }
.state-prompt .fb-by { font-weight: 600; }
/* A prompt whose body wraps to several lines must not centre its pill against
   the middle of the block — the label belongs beside the first line. */
.state-prompt--stacked { align-items: flex-start; }
.state-prompt--stacked .sp-pill { margin-top: 1px; }
/* The one old/new change table (issues #1412, #1211, #1778): the shared
   shape for "this field went from X to Y", used by the bill-changed banner
   and the confirm checkpoint. Same visual family as the inbox conflict table
   (mono smallcaps headers, muted old, solid new), on the global tokens so it
   sits on any surface. Rows run oldest first with a time each. */
.diff-table {
    width: 100%;
    margin: 6px 0;
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-lg);
    border-collapse: separate;
    border-spacing: 0;
    overflow: hidden;
    background: rgb(var(--surface-card));
    color: rgb(var(--text-secondary));
}
.diff-table thead th {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgb(var(--text-muted));
    text-align: left;
    padding: 5px 10px;
    background: rgb(var(--surface-subtle));
    border-bottom: 1px solid rgb(var(--border-default));
}
.diff-table tbody td {
    padding: 6px 10px;
    font-size: var(--fs-xs);
    border-bottom: 1px solid rgb(var(--border-subtle));
    vertical-align: top;
}
.diff-table tbody tr:last-child td { border-bottom: none; }
.diff-table .dt-when {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: rgb(var(--text-muted));
}
.diff-table .dt-field { font-weight: 600; color: rgb(var(--text-primary)); }
.diff-table .dt-old,
.diff-table .dt-new { font-family: 'Geist Mono', ui-monospace, monospace; }
.diff-table .dt-old { color: rgb(var(--text-muted)); }
.diff-table .dt-new { color: rgb(var(--text-primary)); font-weight: 600; }
.diff-table .dt-src { color: rgb(var(--text-muted)); }
/* Standalone variant: the base rule is a strip welded to the top of a panel
   (the lot page's issuance banner), so it carries only a bottom rule. On the
   booking page the same statement stands on its own between the header and
   the details card and needs a full edge. Its own modifier rather than
   Tailwind border utilities, which would collide with the base
   border-bottom at equal specificity and leave the winner up to file order. */
.state-prompt--standalone {
    border: 1px solid rgb(var(--border-subtle));
    border-radius: var(--radius-md);
    padding: 10px 14px;
}
/* Admin-override state: the lock is deliberately open, which deserves a warm
   tint so it reads as an exception, not the default. Amber soft family
   (tokens.css), the same vocabulary as .alert-warning. */
.state-prompt--override {
    background: rgb(var(--accent-amber-soft-bg));
    border-bottom-color: rgb(var(--accent-amber-soft-border));
    color: rgb(var(--accent-amber-soft-text));
}
.state-prompt--override .sp-pill {
    background: var(--paper-white);
    color: rgb(var(--accent-amber-soft-text));
    border-color: rgb(var(--accent-amber-soft-border));
}
/* The CTA: still the loudest control on its (now compact) row, painted in
   the bronze freeze family so it previews the state the click creates.
   .btn-sm carries the size; the id-scoped armed rule (#fi-approved-badge
   .is-armed, above) repaints it warm-red on the first confirm click. */
.state-prompt .sp-btn {
    background: var(--freeze-accent-deep);
    color: var(--freeze-on-accent);
    border-color: var(--freeze-accent-deep);
    font-weight: 600;
    white-space: nowrap;
}
.state-prompt .sp-btn:hover:not(.is-armed) {
    background: var(--freeze-ink);
    border-color: var(--freeze-ink);
}

/* Generic armed state for the inline arm-confirm helper (data-arm /
   data-arm-submit, defined in layout.html). On the first click the helper
   adds .is-armed and flips the label to "Click again to …"; the label
   carries the word, the warm-red fill + pulse carry the alarm. Filled .btn
   variants get the red fill; text-style buttons just pulse. The
   component-specific arm rules above (approve badge, container trash) are more
   specific and still win for those elements. */
.is-armed { animation: lot-arm-pulse 1s ease-in-out infinite; }
.btn.is-armed {
    background: var(--armed-bg);
    color: var(--armed-ink);
    border-color: var(--armed-border);
}
.btn.is-armed:hover {
    background: var(--armed-bg-hover);
    color: var(--armed-ink);
}

/* Watermark segmented control in tab-tools. Two buttons, one active.
   Active COPY = neutral; active ORIGINAL = amber tint so the operator
   knows at a glance the next Generate will produce a stamped PDF. */
.seg-control {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-md);
    overflow: hidden;
    background: rgb(var(--surface-card));
}
.seg-control .seg-btn {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    padding: 4px 10px;
    background: transparent;
    color: rgb(var(--text-muted));
    cursor: pointer;
    border: none;
    border-right: 1px solid rgb(var(--border-default));
    transition: background 0.12s, color 0.12s;
}
.seg-control .seg-btn:last-child { border-right: none; }
.seg-control .seg-btn:hover { color: rgb(var(--text-primary)); }
.seg-control .seg-btn.is-active {
    /* Inverted pill: ink fill, paper text — both flip with the theme. */
    background: rgb(var(--text-primary));
    color: rgb(var(--surface-card));
}
.seg-control[data-mode="original"] .seg-btn.is-active {
    background: var(--freeze-accent);
    color: var(--freeze-on-accent);
}

/* Confirmed by: the confirmation as a segment per party (2026-09-18). The
   label is a segment that does nothing; a filled segment is a side that has
   confirmed; an empty one is the click that records it. */
.seg-control .seg-label {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 4px 8px;
    color: rgb(var(--text-faint));
    border-right: 1px solid rgb(var(--border-default));
    background: rgb(var(--surface-subtle));
}
.confirm-by .seg-btn {
    text-transform: uppercase;
}
.confirm-by .seg-btn.is-on {
    background: rgb(var(--accent-green-soft-bg));
    color: rgb(var(--accent-green-soft-text));
    cursor: default;
}
.confirm-by .seg-btn.is-armed {
    background: var(--armed-bg);
    color: var(--armed-ink);
    animation: lot-arm-pulse 1s ease-in-out infinite;
}
.confirm-by .seg-btn:disabled {
    color: rgb(var(--text-faint));
    cursor: not-allowed;
}
.confirm-by-unmark {
    font-size: var(--fs-2xs);
    color: rgb(var(--text-muted));
    background: none;
    border: none;
    padding: 2px 4px;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.confirm-by-unmark:hover { color: rgb(var(--text-primary)); }
.confirm-by-unmark.is-armed {
    color: var(--armed-ink);
    background: var(--armed-bg);
    border-radius: var(--radius-xs);
    text-decoration: none;
    animation: lot-arm-pulse 1s ease-in-out infinite;
}

/* Armed state for the container row trash button — same pulse as the
   freeze-banner unmark, scoped to the inline icon so the row clearly
   reads "click again to remove" without needing a text label flip. */
.lc-remove-btn.is-armed {
    color: var(--armed-bg);
    animation: lot-arm-pulse 1s ease-in-out infinite;
}

/* Container table cells follow the same lock-by-default formula as the
   rail: filled cells render as plain text (no border, no input chrome);
   click to unlock and edit. Save re-locks; blur with no change re-locks.
   data-ct-locked attribute is set by initContainerLocks JS on load and
   maintained on every interaction. */
.container-table input[data-ct-locked="1"],
.container-table textarea[data-ct-locked="1"],
.container-table select[data-ct-locked="1"] {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    padding-left: 2px;
    padding-right: 2px;
    cursor: pointer;
    color: rgb(var(--text-primary));
}
.container-table input[data-ct-locked="1"]:hover,
.container-table textarea[data-ct-locked="1"]:hover,
.container-table select[data-ct-locked="1"]:hover {
    background: rgb(var(--surface-subtle));
    border-radius: var(--radius-sm);
}
.container-table select[data-ct-locked="1"] {
    appearance: none;
    -webkit-appearance: none;
    background-image: none;
}
/* Hide the native number-spinner arrows on locked number inputs.
   Without this, CARTONS / WEIGHT / CBM render as a borderless value
   plus floating up/down chevrons — a half-stripped input that reads
   visually inconsistent with the bordered-input columns next to it.
   Mirrors the select chevron suppression above. Unlocked cells keep
   the spinner so the editable affordance stays obvious. */
.container-table input[type="number"][data-ct-locked="1"] {
    appearance: textfield;
    -moz-appearance: textfield;
}
.container-table input[type="number"][data-ct-locked="1"]::-webkit-inner-spin-button,
.container-table input[type="number"][data-ct-locked="1"]::-webkit-outer-spin-button {
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
}

/* Per-tab column visibility on the Containers table. Cells carry a
   `data-cols` attribute listing the doc-tab names where they apply
   (or "all" to always show). The table's `data-active-tab` attribute,
   set by showLotTab() JS, controls which cells display. Hidden cells
   collapse so the table only shows columns relevant to the active doc. */
.container-table[data-active-tab="hbl"] [data-cols]:not([data-cols~="all"]):not([data-cols~="hbl"]),
.container-table[data-active-tab="shipment-advice"] [data-cols]:not([data-cols~="all"]):not([data-cols~="shipment-advice"]),
.container-table[data-active-tab="shipping-order"] [data-cols]:not([data-cols~="all"]):not([data-cols~="shipping-order"]),
.container-table[data-active-tab="freight-invoice"] [data-cols]:not([data-cols~="all"]):not([data-cols~="freight-invoice"]) {
    display: none;
}

/* ── Per-doc readiness (lot_detail) ──────────────────────────────────
   Each output doc (HBL, Shipment Advice, Shipping Order, Freight
   Invoice) has its own required fields, so "what's still needed" differs
   per tab. Three surfaces show it, all driven by the workspace's active
   doc with no JS branching:

     1. .doc-tab-badge  — chip on each tab: "Ready" (quiet green), an
                          amber count (fillable now), or a red count
                          (blocking — a field's upstream doc is late).
     2. .rail-doc-gate  — one gate per doc in the rail; only the one whose
                          data-doc matches data-active-doc is shown.
     3. .rf-note / .rf-* — inline per-field markers in the rail, revealed
                          for the active doc only via data-st-<doc>.

   Red is reserved for blocking (an overdue upstream doc). An amber count
   is "your turn", not an error. A ready doc stays calm so the rail reads
   "good to generate" at a glance rather than as a wall of red. */

/* 1. Tab badge. "Ready" is the common, good case — a calm confirmation,
   so it is the quietest of the three; the counts carry the urgency. */
.doc-tab-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 6px;
    min-width: 16px;
    height: 16px;
    padding: 0 5px;
    border-radius: 9999px;
    font-size: var(--fs-3xs);
    font-weight: 600;
    line-height: 1;
    vertical-align: middle;
}
.doc-tab-badge.is-ready {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: rgb(var(--accent-green-soft-text));
    background: rgb(var(--accent-green-soft-bg));
}
.doc-tab-badge.is-fillable {
    color: rgb(var(--accent-amber-soft-text));
    background: rgb(var(--accent-amber-soft-bg));
}
.doc-tab-badge.is-blocking {
    color: rgb(var(--accent-red-soft-text));
    background: rgb(var(--accent-red-soft-bg));
}
/* Waiting: nothing actionable, but required fields are still expected from
   upcoming documents (they print as MISSING on the paper). Muted, never
   green — "Ready" here would contradict the preview. */
.doc-tab-badge.is-waiting {
    color: rgb(var(--text-muted));
    background: rgb(var(--surface-subtle));
}

/* 2. Rail gate. All four are rendered into the rail; reveal only the one
   matching the workspace's active doc. Mirrors the container-table's
   per-tab visibility pattern above. */
.rail-doc-gate { display: none; }
#lot-workspace[data-active-doc="hbl"]             .rail-doc-gate[data-doc="hbl"],
#lot-workspace[data-active-doc="shipment-advice"] .rail-doc-gate[data-doc="shipment-advice"],
#lot-workspace[data-active-doc="shipping-order"]  .rail-doc-gate[data-doc="shipping-order"],
#lot-workspace[data-active-doc="freight-invoice"] .rail-doc-gate[data-doc="freight-invoice"] {
    display: block;
}
.rail-doc-gate {
    padding: 12px 24px;
    border-bottom: 1px solid rgb(var(--border-default));
}
/* Faint state wash — a tint, not a fill. The green case is the lightest
   so a ready doc never draws the eye away from the work. is-waiting is the
   nothing-actionable-but-fields-still-expected state: neutral on purpose,
   neither the green all-clear nor the amber your-turn. */
.rail-doc-gate.is-ready    { background: rgb(var(--accent-green-soft-bg) / 0.35); }
.rail-doc-gate.is-needs    { background: rgb(var(--accent-amber-soft-bg) / 0.40); }
.rail-doc-gate.is-blocking { background: rgb(var(--accent-red-soft-bg) / 0.45); }
.rail-doc-gate.is-waiting  { background: rgb(var(--surface-subtle) / 0.5); }

.rail-gate-ready {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-2xs);
    font-weight: 600;
    color: rgb(var(--accent-green-soft-text));
}
.rail-gate-ready svg { width: 14px; height: 14px; flex-shrink: 0; }
.rail-gate-head {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgb(var(--text-secondary));
}
.rail-doc-gate.is-needs    .rail-gate-head { color: rgb(var(--accent-amber-soft-text)); }
.rail-doc-gate.is-blocking .rail-gate-head { color: rgb(var(--accent-red-soft-text)); }
/* "Nothing needed from you yet" — same shape as the green ready line,
   neutral voice (this is patience, not celebration). */
.rail-gate-waiting {
    font-size: var(--fs-2xs);
    font-weight: 600;
    color: rgb(var(--text-muted));
}
/* "Arrives with documents" sub-head + rows: provenance, not work. Faint in
   every gate state, so it must come after the is-needs/is-blocking head
   colours above to win the cascade. */
.rail-doc-gate .rail-gate-head--expected {
    color: rgb(var(--text-faint));
    margin-top: 8px;
}
.rail-gate-item.is-expected { color: rgb(var(--text-muted)); }
.rail-gate-list {
    margin-top: 6px;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
.rail-gate-item {
    display: block;
    width: 100%;
    text-align: left;
    margin-left: -6px;
    padding: 3px 6px;
    border-radius: var(--radius-md);
    font-size: var(--fs-2xs);
    background: transparent;
    transition: background 0.12s ease;
}
.rail-gate-item:hover { background: rgb(var(--surface-card)); }
.rail-gate-item.is-overdue { color: rgb(var(--accent-red-soft-text)); }
.rail-gate-item.is-manual  { color: rgb(var(--accent-amber-soft-text)); }
/* Informational gate row — a required gap with no rail input (booking /
   company-settings sources, the FI/SA buyer_name consignee). Surfaced so the
   operator sees it, but it is not a jump target: drop the hover lift and the
   pointer cursor so it never looks clickable. The is-overdue / is-manual
   colour still applies. */
.rail-gate-item--static { cursor: default; }
.rail-gate-item--static:hover { background: transparent; }

/* 3. Inline per-field markers. Each rail field carries one data-st-<doc>
   token per output doc ("overdue" / "manual" / ""); the active doc's
   token reveals that field's suffix tag and tints its label + input.
   Both suffix tags are hidden by default and shown only for the matching
   state, so the same field reads "(required)" on one tab and stays quiet
   on another with no per-doc logic in the template. */
.rf-note { display: none; }
/* Overdue (red) → the field's upstream doc is late; show "(required)". */
#lot-workspace[data-active-doc="hbl"]             .rail-field[data-st-hbl="overdue"] .rf-note--req,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="overdue"] .rf-note--req,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="overdue"] .rf-note--req,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="overdue"] .rf-note--req {
    display: inline;
    color: rgb(var(--accent-red-soft-text));
}
/* Manual (amber) → operator must type it; show "(fill in)". */
#lot-workspace[data-active-doc="hbl"]             .rail-field[data-st-hbl="manual"] .rf-note--fill,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="manual"] .rf-note--fill,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="manual"] .rf-note--fill,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="manual"] .rf-note--fill {
    display: inline;
    color: rgb(var(--accent-amber-soft-text));
}
/* Label tint follows the active doc's state. */
#lot-workspace[data-active-doc="hbl"]             .rail-field[data-st-hbl="overdue"] .rf-label,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="overdue"] .rf-label,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="overdue"] .rf-label,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="overdue"] .rf-label {
    color: rgb(var(--accent-red-soft-text));
}
#lot-workspace[data-active-doc="hbl"]             .rail-field[data-st-hbl="manual"] .rf-label,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="manual"] .rf-label,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="manual"] .rf-label,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="manual"] .rf-label {
    color: rgb(var(--accent-amber-soft-text));
}
/* Input border tint — soft, so the field reads "needed" without looking
   like a validation error. */
#lot-workspace[data-active-doc="hbl"]             .rail-field[data-st-hbl="overdue"] .rf-input,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="overdue"] .rf-input,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="overdue"] .rf-input,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="overdue"] .rf-input {
    border-color: rgb(var(--accent-red-soft-border));
}
#lot-workspace[data-active-doc="hbl"]             .rail-field[data-st-hbl="manual"] .rf-input,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="manual"] .rf-input,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="manual"] .rf-input,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="manual"] .rf-input {
    border-color: rgb(var(--accent-amber-soft-border));
}

/* Per-field lock on the editor rail. The lock icon = a row in the
   `field_locks` table — meaning the field has been manually edited and
   the parser will not overwrite it on the next upload. The icon only
   renders when locked; parser-set values render as plain editable
   inputs. Clicking the icon submits an HTMX form POST to /unlock-field;
   the server returns HX-Refresh and the page reloads with the icon
   gone. After a manual save (typing into an unlocked field), the server
   inserts a field_locks row; the icon appears on the next render —
   the "Edited by you" provenance line that swaps in is the immediate
   feedback in the meantime.

   Layout: row 1: [label .....................] [🔒-form]
           row 2: value (full width — wraps for long content)
           row 3: provenance line ("Edited by you · 3 min ago" or
                  "from Producer Invoice · 15 min ago")

   Readonly state strips the input chrome so the value reads as plain
   text — same grid cell, same width, no page-reshuffle when toggling. */
/* Universal field rule: every editable rail field is a flex-column block
   delimited from its siblings by a 1px hairline border-top + 10px padding-
   top. Same rhythm as .party-group below — separators, not boxes. The
   border-top creates visible "this is one field" units without the visual
   weight of a card. Inside the field, label + lock pair via .rail-field-
   head, value flows below at full width, provenance line beneath that. */
.rail-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    padding-top: 10px;
    padding-bottom: 4px;
    border-top: 1px solid rgb(var(--border-default));
}
/* The first row of a section sits right under the section header — no
   border there (the section's own border-b above provides the boundary).
   For 2-col grids, both col 1 (nth-child(1)) and col 2 (nth-child(2)) are
   in row 1 if neither has md:col-span-2. If the first child IS col-span-2
   (a textarea), nth-child(2) is in row 2 and SHOULD have a border — but
   the cost of one duplicate hairline edge-case beats the cost of grid-
   inspection JS. Accept the small inconsistency. */
.rail-fields > .rail-field:nth-child(-n+2) {
    border-top: 0;
    padding-top: 4px;
}
.rail-field-head {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    align-self: flex-start; /* lock anchors to the label text, not the cell edge */
}
.rail-field-head > label { margin: 0; }
.rail-field input,
.rail-field textarea,
.rail-field select {
    min-width: 0;
    width: 100%;
}
/* Auto-grow rail textareas to fit their content (height only — width stays
   100% from the rule above, so text still wraps). SHIPPING MARKS and PRODUCT
   DESCRIPTION hold free text that often runs several lines and flows verbatim
   onto the HBL, so the operator must see the whole value. A fixed rows="3"
   box clips the overflow (resize-none blocks dragging it taller), hiding text
   that will print on the bill of lading. min-height keeps an empty field at a
   ~3-row target so it still reads as a multi-line input; the locked variant
   below overrides it to 0 since a locked value is always present. */
.rail-field textarea {
    field-sizing: content;
    min-height: 4.75rem;
}

/* Readonly visual: a locked field keeps its box (border + radius +
   padding) so the rail reads as one consistent set of fields. A faint
   inset fill marks it as locked; the padlock by the label is the release
   affordance, and pointer-events: none blocks editing until it's clicked. */
.rail-field[data-readonly="1"] input,
.rail-field[data-readonly="1"] textarea,
.rail-field[data-readonly="1"] select {
    background: rgb(var(--surface-inset));
    border-color: rgb(var(--border-input));
    box-shadow: none;
    cursor: default;
    pointer-events: none;
    color: rgb(var(--text-primary));
}
.rail-field[data-readonly="1"] textarea {
    /* Auto-grow comes from the .rail-field textarea rule above; a locked
       value is always present, so drop the empty-field min-height and sit
       tight to the content. */
    resize: none;
    min-height: 0;
}
.rail-field[data-readonly="1"] select {
    /* Hide the native dropdown chevron — when locked the value is just
       text, the chevron would imply "click to pick" which misleads. The
       box (border + padding) stays via the shared rule above. */
    appearance: none;
    -webkit-appearance: none;
    background-image: none;
}
/* Party-group: an HBL party block (Consignee / Notify A / Notify B).
   Each group is one semantic unit — name + address paired, one lock
   that governs both fields, one provenance line for the whole party.
   Visual containment uses spacing + a hairline separator BETWEEN parties
   (not a card border around each), per the design principle "cards are
   the lazy answer". */
.party-groups {
    display: flex;
    flex-direction: column;
}
.party-group {
    padding: 14px 0;
    border-top: 1px solid rgb(var(--border-default));
}
.party-group:first-child {
    padding-top: 4px;
    border-top: 0;
}
.party-head {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 10px;
}
.party-label {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: rgb(var(--text-primary));
    margin: 0;
}

/* Party body — 2-col grid (name | address). The address textarea grows
   naturally to fit multi-line content; name stays compact. Uses the full
   row width so we don't waste the right side with empty space. */
.party-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    gap: 16px;
    align-items: start;
}
.party-cell {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.party-cell-label {
    font-size: var(--fs-3xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgb(var(--text-muted));
}
.party-input {
    width: 100%;
    min-width: 0;
}
/* Address is a multi-line block on the HBL, so the editable address input
   grows to show every line instead of clipping at rows="3" — the same
   auto-grow the rail's description / marks textareas already get
   (.rail-field textarea above) and that the locked party state gets below.
   Without this rule the address only grew once LOCKED, so an *editable*
   address read as a cramped 3-line box while the description beside it
   sprawled — the exact inconsistency this matches up. min-height keeps an
   empty field at a ~3-row target so it still reads as a multi-line input. */
textarea.party-input {
    field-sizing: content;
    min-height: 4.75rem;
}

/* Readonly visual for party inputs — same boxed locked treatment as the
   per-field rail state above (border + padding kept, faint inset fill).
   data-readonly on .party-group governs the whole group's read/write state;
   the inline "unlock" on the party's provenance line releases it. */
.party-group[data-readonly="1"] .party-input {
    background: rgb(var(--surface-inset));
    border-color: rgb(var(--border-input));
    box-shadow: none;
    cursor: default;
    pointer-events: none;
    color: rgb(var(--text-primary));
}
.party-group[data-readonly="1"] textarea.party-input {
    resize: none;
    /* Same reason as .rail-field readonly textarea above: locked party
       addresses can't be scrolled (pointer-events: none), so grow them
       to fit content instead of clipping at rows="3". */
    field-sizing: content;
    min-height: 0;
}

@media (max-width: 1199px) {
    /* On narrower rails the name | address split feels cramped; stack. */
    .party-body { grid-template-columns: 1fr; gap: 10px; }
}

/* ─────────────────────────────────────────────────────────────────────
 * Inbox presence cascade — global because these elements live in the
 * layout (sidebar + bottom-right chip), not under /inbox. They use raw
 * OKLCH literals rather than design tokens so they remain readable on
 * any page background, including the legacy Tailwind pages that don't
 * adopt [data-cp-inbox]. The bulk of inbox.css stays loaded only on
 * /inbox; only these two surfaces + the shared cp-pulse keyframe are
 * promoted here.
 * ─────────────────────────────────────────────────────────────────── */

@keyframes cp-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
@media (prefers-reduced-motion: reduce) {
    .cf-presence-chip .dot,
    .status-dot.is-pulsing { animation: none !important; opacity: 0.85; }
}

/* A live dot that keeps beating while work is in flight. The dot is the state
   channel for a .count and for a .tag; the pulse says the number is still
   moving. */
.status-dot.is-pulsing { animation: cp-pulse 1.6s ease-in-out infinite; }

/* The two work counts on the Home nav row (#1322: parsing + reviews) are counts,
   so they wear no chrome — a leading dot carries the state and the numeral is
   just a numeral. Only the first VISIBLE one may absorb the row's free space, or
   flexbox splits it across both auto margins and the first count floats detached
   mid-row. The :not([hidden]) guard matters: a hidden parsing count is still a
   DOM sibling, and without it the lone review count would lose its auto margin
   and hug the Home label instead of right-aligning (Codex P2 on #1330). */
.nav-count {
    margin-left: auto;
    display: inline-flex; align-items: center; gap: 5px;
    font-size: var(--fs-2xs);
}
.nav-count:not([hidden]) + .nav-count { margin-left: 6px; }
.nav-count[hidden] { display: none; }

.cf-presence-chip {
    position: fixed;
    bottom: 20px; right: 20px;
    background: oklch(99.5% 0.002 245);
    border: 1px solid oklch(85% 0.006 245);
    border-radius: var(--radius-lg);
    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08), 0 0 0 1px rgba(15, 23, 42, 0.03);
    padding: 10px 14px 10px 12px;
    display: flex; align-items: center; gap: 10px;
    font-family: 'Geist', -apple-system, system-ui, sans-serif;
    font-size: var(--fs-xs);
    color: oklch(20% 0.014 250);
    z-index: 40;
    min-width: 220px;
    transition: opacity 0.22s cubic-bezier(0.22, 1, 0.36, 1),
                transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
    opacity: 1;
    transform: translateY(0);
}
.cf-presence-chip[hidden] {
    display: flex;
    opacity: 0;
    transform: translateY(8px);
    pointer-events: none;
}
.cf-presence-chip .dot {
    width: 8px; height: 8px; border-radius: 50%;
    background: oklch(54% 0.18 254);
    animation: cp-pulse 1.6s ease-in-out infinite;
}
.cf-presence-chip .label { font-weight: 600; }
.cf-presence-chip .meta {
    margin-left: auto;
    font-size: var(--fs-2xs);
    color: oklch(54% 0.010 250);
}

.dark .cf-presence-chip {
    background: oklch(22% 0.020 250);
    border-color: oklch(38% 0.014 250);
    color: oklch(96% 0.005 245);
}
.dark .cf-presence-chip .meta { color: oklch(62% 0.010 245); }

/* =========================================================================
   PO board (#1151 phase 3c) — /lots' default lane view.
   State pills are semantic component classes over the accent tokens
   (uniformity rule #511): red = a human must act, amber = the next
   document settles it, grey = on track. The spine dots read the four
   document milestones at a glance on a 13" laptop.
   ========================================================================= */
/* A filter chip is a control, not one of the three marker kinds: it is clicked,
   it changes what the list shows, and it carries a selected state. Named for
   what it does so nobody folds it back into the badge family. */
.filter-chip {
    display: inline-flex;
    align-items: center;
    padding: 4px 12px;
    border: 1px solid rgb(var(--border-default));
    border-radius: 999px;
    color: inherit;
    text-decoration: none;
}
.filter-chip.is-active {
    /* Selection is NEUTRAL emphasis — red is reserved for lanes that need a
       human (the board's one-red-thing rule), never for "this filter is on". */
    background: rgb(var(--text-muted) / 0.12);
    border-color: rgb(var(--text-muted) / 0.4);
    font-weight: 600;
}

/* The order row a type-to-filter matched (#1903, /producer-invoices).
   NEUTRAL emphasis, the same call .filter-chip.is-active makes: these rows are
   tick boxes, and a terracotta or status tint on one would read as "this order
   is picked" — which on this page is what mints a link. Found is not chosen.
   A tint rather than a border, because the rows wrap in a flow and a border
   would reflow the very line it is marking; the spread shadow gives the tint
   breathing room without taking layout space either. */
.is-filter-hit {
    background: rgb(var(--text-muted) / 0.14);
    border-radius: var(--radius-sm);
    box-shadow: 0 0 0 3px rgb(var(--text-muted) / 0.14);
}

/* A filter box is type=search so the browser gives us its own clear button and
   its Escape key for free, rather than a control of ours. Chrome and Safari
   then draw that button in THEIR blue, which is the one off-palette pixel on a
   warm-paper page, so the glyph is re-cut as a mask and takes muted ink.
   Opt-in by class rather than on [type=search] at large: the Fleet filter is
   the app's other search box and repainting it belongs to its own change. */
.form-input.filter-search::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    height: 0.875rem;
    width: 0.875rem;
    cursor: pointer;
    background-color: rgb(var(--text-muted));
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 4l8 8M12 4l-8 8' stroke='%23000' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.form-input.filter-search::-webkit-search-cancel-button:hover {
    background-color: rgb(var(--text-primary));
}

/* The producer form's foldable sections (producer_form.html). The header row
   is the summary, so number, title and state chip stay readable when folded;
   the native marker is replaced by the .pf-chev arrow, which points down when
   folded and up when open. */
.pf-section > summary {
    list-style: none;
    cursor: pointer;
}
.pf-section > summary::-webkit-details-marker { display: none; }
.pf-section[open] > summary { margin-bottom: 1rem; }
.pf-chev { transition: transform 140ms ease-out; }
.pf-section[open] .pf-chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
    .pf-chev { transition: none; }
}

/* The guide bar's jump button hides by attribute; .btn sets display, which
   would otherwise win over [hidden]. */
[data-pf-guide] .btn[hidden] { display: none; }

/* --- Address chips (#2137) ---
   A recipient box is a list of addresses, not a paragraph: each is a chip
   with its own remove, and the input for the next one sits at the end.
   chips.js builds it over a textarea[data-chips]; the textarea keeps the
   list for the form. An address a send did not reach is amber (#2090). */
.email-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-height: 38px;
    padding: 5px 8px;
    background: rgb(var(--surface-card));
    border: 1px solid rgb(var(--border-input));
    border-radius: var(--radius-lg);
    cursor: text;
}
.email-chips:focus-within { border-color: rgb(var(--accent-blue-strong)); box-shadow: 0 0 0 3px rgb(var(--accent-blue-soft-border) / 0.5); }
.email-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 4px 2px 9px;
    border-radius: 999px;
    border: 1px solid rgb(var(--border-default));
    background: rgb(var(--surface-subtle));
    font-size: var(--fs-xs);
    line-height: 1.4;
    color: rgb(var(--text-primary));
    white-space: nowrap;
}
.email-chip--warn { background: rgb(var(--accent-amber-soft-bg)); border-color: rgb(var(--accent-amber-soft-border)); color: rgb(var(--accent-amber-soft-text)); }
.email-chip-x {
    display: inline-flex;
    padding: 2px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: rgb(var(--text-muted));
    cursor: pointer;
}
.email-chip-x:hover { background: rgb(var(--surface-active)); color: rgb(var(--text-primary)); }
.email-chips-input {
    flex: 1 1 140px;
    min-width: 140px;
    border: 0;
    outline: 0;
    padding: 3px 2px;
    background: transparent;
    font-size: var(--fs-xs);
    color: rgb(var(--text-primary));
}
.email-chips-input::placeholder { color: rgb(var(--text-muted)); }
