/* --- Page header ---
   Flex wrapper used by index/list pages. Pairs with the `pageHeader`
   partial (templates/_page_header.html). Caller renders the title block
   via the 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;
}

/* --- 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". Pick the element that fits the outline:
   `<h2>` for a card-level section, `<h3>` for a block within one.

   .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 eyebrow inside the lot-detail editor rail — one
                  tier smaller (2xs), bold, secondary ink (defined below).

   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 section inside the lot-detail editor rail.
   One tier smaller and one shade darker than .section-h: the rail packs
   many sections into a narrow column, 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));
}

/* --- 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));
}

/* --- Chat message styles --- */
.chat-turn { padding: 16px 20px; }
.chat-turn + .chat-turn { border-top: 1px solid rgb(var(--border-subtle)); }
.chat-q {
    font-size: var(--fs-sm);
    font-weight: 500;
    color: rgb(var(--text-primary));
    margin-bottom: 8px;
}
.chat-a {
    font-size: var(--fs-sm);
    line-height: 1.6;
    color: rgb(var(--text-secondary));
}
.chat-a p { margin: 4px 0; }
.chat-a strong { color: rgb(var(--text-primary)); font-weight: 600; }
.chat-a ul, .chat-a ol { margin: 4px 0; padding-left: 20px; }
.chat-a li { margin: 2px 0; }
.chat-a code { font-size: var(--fs-xs); padding: 1px 4px; border-radius: var(--radius-sm); background: rgb(var(--surface-inset)); }
.chat-err { color: rgb(239 68 68); }
.dark .chat-err { color: rgb(248 113 113); }
.chat-sql { margin-top: 8px; }
.chat-sql summary { font-size: var(--fs-xs); color: rgb(var(--text-faint)); cursor: pointer; }
.chat-sql summary:hover { color: rgb(var(--text-muted)); }
.chat-sql pre { margin-top: 4px; padding: 8px; border-radius: var(--radius-md); background: rgb(var(--surface-inset)); font-size: var(--fs-xs); color: rgb(var(--text-muted)); overflow-x: auto; }

/* --- 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-arrived    { background: rgb(var(--status-arrived-bg));    color: rgb(var(--status-arrived-text)); }
.badge-delivered  { background: rgb(var(--status-delivered-bg));  color: rgb(var(--status-delivered-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)); }

/* --- Status bar segment classes --- */
.bar-booked     { background: rgb(var(--status-booked-bar)); }
.bar-released   { background: rgb(var(--status-released-bar)); }
.bar-docs_in    { background: rgb(var(--status-docs_in-bar)); }

.bar-stuffed    { background: rgb(var(--status-stuffed-bar)); }
.bar-bl_ready   { background: rgb(var(--status-bl_ready-bar)); }
.bar-shipped    { background: rgb(var(--status-shipped-bar)); }
.bar-arrived    { background: rgb(var(--status-arrived-bar)); }
.bar-delivered  { background: rgb(var(--status-delivered-bar)); }
.bar-on_hold    { background: rgb(var(--status-on_hold-bar)); }
.bar-cancelled  { background: rgb(var(--status-cancelled-bar)); }

/* --- JS-generated format badge --- */
.hint-tag {
    background-color: rgb(var(--accent-blue-soft-bg));
    color: rgb(var(--accent-blue-soft-text));
}

/* --- Count pill ---
   Round 28x28 numeric badge for cardinality (lot count, container count).
   Layout/typography lives in CSS so templates carry just .pill-count
   instead of repeating utility classes. */
.pill-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 999px;
    font-size: var(--fs-xs);
    font-weight: 700;
    background-color: rgba(0 0 0 / 0.06);
    color: rgb(var(--text-primary));
}
.dark .pill-count {
    background-color: rgba(255 255 255 / 0.1);
    color: rgb(var(--text-primary));
}

/* --- 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));
}
.alert-success .alert-btn {
    background-color: rgb(var(--accent-green-strong));
    color: #fff;
}
.alert-success .alert-btn:hover {
    background-color: rgb(var(--accent-green-strong-hover));
    color: #fff;
}

/* 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)); }

/* Document-type pill — globally available companion to the inbox-scoped
   `.tag.t-*` system. Semantic mapping matches inbox (VESSEL_PLAN→blue,
   etc.). Pair with sizing/typography utilities on the wrapper.
   Returned by helpers.docPillClass.

   Base fallback (unknown DocumentType) uses --text-secondary, not
   --accent-gray-soft-text, so 10px pill text on the soft-gray bg clears
   WCAG AA (>4.5:1) in both light and dark. The accent-gray pair is
   reserved for visually-muted past/inactive variants like .vc-badge.past
   where the low-contrast intent is deliberate. */
.doc-pill                  { background-color: rgb(var(--accent-gray-soft-bg));   color: rgb(var(--text-secondary)); }
.doc-pill--vessel          { background-color: rgb(var(--accent-blue-soft-bg));   color: rgb(var(--accent-blue-soft-text)); }
.doc-pill--release         { background-color: rgb(var(--accent-amber-soft-bg));  color: rgb(var(--accent-amber-soft-text)); }
.doc-pill--book            { background-color: rgb(var(--accent-green-soft-bg));  color: rgb(var(--accent-green-soft-text)); }
.doc-pill--invoice         { background-color: rgb(var(--accent-violet-soft-bg)); color: rgb(var(--accent-violet-soft-text)); }
.doc-pill--offdock         { background-color: rgb(var(--accent-teal-soft-bg));   color: rgb(var(--accent-teal-soft-text)); }

/* ── 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. */
.pipeline .stage.done    { background: rgb(var(--accent-green-strong)); color: #fff; }
.pipeline .stage.current { background: rgb(var(--accent-blue-strong));  color: #fff; }
.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.approaching::before { background: rgb(var(--accent-amber-strong)); }
.vessel-card.on-track::before    { background: rgb(var(--accent-green-strong)); }
.vessel-card.departed::before    { background: rgb(var(--text-muted)); }

/* ── Urgency badges ── */
.vc-badge {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs); font-weight: 600;
    padding: 2px 8px; border-radius: var(--radius-sm); white-space: nowrap;
}
.vc-badge.critical { background: rgb(var(--accent-red-soft-bg));    color: rgb(var(--accent-red-soft-text));    border: 1px solid rgb(var(--accent-red-soft-border)); }
.vc-badge.warn     { background: rgb(var(--accent-amber-soft-bg));  color: rgb(var(--accent-amber-soft-text));  border: 1px solid rgb(var(--accent-amber-soft-border)); }
.vc-badge.ok       { background: rgb(var(--accent-green-soft-bg));  color: rgb(var(--accent-green-soft-text));  border: 1px solid rgb(var(--accent-green-soft-border)); }
.vc-badge.past     { background: rgb(var(--accent-gray-soft-bg));   color: rgb(var(--accent-gray-soft-text));   border: 1px solid rgb(var(--accent-gray-soft-border)); }
.vc-badge.info     { background: rgb(var(--accent-blue-soft-bg));   color: rgb(var(--accent-blue-soft-text));   border: 1px solid rgb(var(--accent-blue-soft-border)); }

/* ── Action chips ── */
.vc-chip {
    font-size: var(--fs-2xs); font-weight: 500;
    padding: 1px 8px; border-radius: var(--radius-sm);
    display: inline-flex; align-items: center; gap: 4px;
}
.vc-chip.critical { background: rgb(var(--accent-red-soft-bg));    color: rgb(var(--accent-red-soft-text)); }
.vc-chip.warn     { background: rgb(var(--accent-amber-soft-bg));  color: rgb(var(--accent-amber-soft-text)); }
.vc-chip.info     { background: rgb(var(--accent-blue-soft-bg));   color: rgb(var(--accent-blue-soft-text)); }
.vc-chip.ok       { background: rgb(var(--accent-green-soft-bg));  color: rgb(var(--accent-green-soft-text)); }

/* ── Notification cards ── */
.notif-card .notif-desc {
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
    transition: all 0.2s ease;
}
.notif-card.expanded .notif-desc {
    display: block; -webkit-line-clamp: unset;
    overflow: visible;
}
.notif-card { transition: box-shadow 0.15s ease; }
.notif-card.expanded { box-shadow: var(--shadow-md); }

/* ── 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)); }

/* ── Batch action bars ── */
.batch-bar { display: flex; align-items: center; gap: 12px; padding: 12px 20px; border-radius: var(--radius-lg); border: 1px solid; }
.batch-bar .bb-btn {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-2xs); font-weight: 600;
    padding: 6px 14px; border-radius: var(--radius-md); border: none; cursor: pointer; white-space: nowrap;
}
.batch-bar.blue   { background: rgb(var(--accent-blue-soft-bg));   border-color: rgb(var(--accent-blue-soft-border)); }
.batch-bar.blue .bb-btn         { background: rgb(var(--accent-blue-strong));       color: #fff; }
.batch-bar.blue .bb-btn:hover   { background: rgb(var(--accent-blue-strong-hover)); }
.batch-bar.green  { background: rgb(var(--accent-green-soft-bg));  border-color: rgb(var(--accent-green-soft-border)); }
.batch-bar.green .bb-btn        { background: rgb(var(--accent-green-strong));       color: #fff; }
.batch-bar.green .bb-btn:hover  { background: rgb(var(--accent-green-strong-hover)); }
.batch-bar.violet { background: rgb(var(--accent-violet-soft-bg)); border-color: rgb(var(--accent-violet-soft-border)); }
.batch-bar.violet .bb-btn       { background: rgb(var(--accent-violet-strong));       color: #fff; }
.batch-bar.violet .bb-btn:hover { background: rgb(var(--accent-violet-strong-hover)); }

/* ── 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)); }

/* ── MISSING marker pills (dark-mode safe) ── */
.pill-expected {
    background: rgb(var(--surface-subtle));
    color: rgb(var(--text-faint));
    border: 1px solid rgb(var(--border-default));
}
.pill-manual {
    background: #fffbeb; color: #92400e;
    border: 1px solid #fde68a;
}
.dark .pill-manual {
    background: rgba(146 64 14 / 0.15); color: #fcd34d;
    border-color: rgba(146 64 14 / 0.3);
}
.pill-overdue {
    background: #fef2f2; color: #991b1b;
    border: 1px solid #fecaca;
}
.dark .pill-overdue {
    background: rgba(153 27 27 / 0.15); color: #fca5a5;
    border-color: rgba(153 27 27 / 0.3);
}

/* ── Source provenance chip ── */
.source-chip {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs); font-weight: 500;
    color: rgb(var(--text-faint));
    display: inline-flex; align-items: center; gap: 3px;
}
.source-chip svg { opacity: 0.6; }

/* ── 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(37 99 235 / 0.04);
    color: rgb(var(--text-primary));
}
.dark .phase-step.current {
    background: rgba(37 99 235 / 0.12);
}
.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; }

/* ── HBL field grid ── */
.field-row {
    display: flex; align-items: baseline; gap: 12px;
    padding: 5px 0; font-size: var(--fs-sm);
    border-bottom: 1px solid rgb(var(--border-subtle));
}
.field-row:last-child { border-bottom: none; }
.field-label {
    width: 180px; flex-shrink: 0;
    font-size: var(--fs-xs); color: rgb(var(--text-muted));
    font-weight: 500;
}
.field-value {
    flex: 1; min-width: 0;
    color: rgb(var(--text-primary));
}
.field-value.empty { color: rgb(var(--text-faint)); }
/* Dark mode: command palette */
.dark #cmd-palette { background: rgb(31 41 55); border-color: rgb(55 65 81); }
.dark #cmd-palette input { color: rgb(209 213 219); }
.dark #cmd-palette kbd { background: rgb(55 65 81); border-color: rgb(75 85 99); color: rgb(156 163 175); }

.field-source {
    width: 120px; flex-shrink: 0; text-align: right;
}

/* ── 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. */
.form-input[readonly]:focus,
.form-select[readonly]:focus,
.form-textarea[readonly]:focus {
    border-color: rgb(var(--border-input));
    box-shadow: none;
}
/* Missing-state border (e.g. lot detail buyer picker when no buyer
   matches). Two-class specificity beats Tailwind CDN utilities
   regardless of stylesheet injection order. */
.form-input.is-missing,
.form-select.is-missing,
.form-textarea.is-missing {
    border-color: rgb(var(--accent-amber-strong));
}
/* 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 the `.is-missing` rule 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);
}

/* ── Skeleton loaders ──
   Neutral-palette shimmer for async loading states. Pair with the shape
   of the content being fetched — a row-skeleton replaces a table row, a
   text-skeleton replaces a line of copy. Respects reduced-motion via
   the global @media rule in base.css. */
@keyframes skeleton-shimmer {
    0%   { background-position: -200% 0; }
    100% { background-position: 200% 0; }
}
.skeleton {
    display: inline-block;
    background: linear-gradient(
        90deg,
        rgb(var(--surface-inset)) 0%,
        rgb(var(--surface-hover)) 50%,
        rgb(var(--surface-inset)) 100%
    );
    background-size: 200% 100%;
    animation: skeleton-shimmer 1.4s linear infinite;
    border-radius: var(--radius-sm);
}
/* Inline text run — width/height set at call site or via .skeleton-w-* */
.skeleton-text { height: 0.875em; width: 100%; }
/* Block surface — for card bodies, images, iframes */
.skeleton-block { display: block; width: 100%; height: 100%; border-radius: var(--radius-lg); }
/* Full table row — 12px tall, takes full column width */
.skeleton-row { display: block; width: 100%; height: 12px; margin: 10px 0; }
/* Coarse width modifiers so skeletons don't all look the same width */
.skeleton-w-xs { width: 24px; }
.skeleton-w-sm { width: 64px; }
.skeleton-w-md { width: 120px; }
.skeleton-w-lg { width: 200px; }
.skeleton-w-xl { width: 320px; }

/* ── 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;
}

/* ── Side panel slide animation ── */
#side-panel {
    transform: translateX(100%);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.2s ease-out, opacity 0.15s ease-out;
}
#side-panel.open {
    transform: translateX(0);
    opacity: 1;
    pointer-events: auto;
}
#side-panel-backdrop {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease-out;
}
#side-panel-backdrop.open {
    opacity: 1;
    pointer-events: auto;
}

/* ── 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). Mirrors the #side-panel pattern above; 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;
}
.field-src-link {
    color: rgb(var(--text-link));
    text-decoration: none;
}
.field-src-link:hover {
    color: rgb(var(--text-link-hover));
    text-decoration: underline;
}
.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; }

/* ── Sources panel ──
   Compact list above the HBL editor: every document this lot has been
   built from, with the count of fields it wrote. Designed to feel like
   a footnote, not a hero — small type, dense rows, hairline rule between
   them. The whole row is a click target into the source document. */
.sources-panel {
    background: rgb(var(--surface-card));
    border: 1px solid rgb(var(--border-default));
    border-radius: var(--radius-xl);
    padding: 14px 16px 8px;
}
.sources-panel-head { margin-bottom: 8px; }
.sources-panel-title {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: rgb(var(--text-primary));
    margin: 0;
}
.sources-panel-sub {
    font-size: var(--fs-2xs);
    color: rgb(var(--text-muted));
    margin: 2px 0 0;
}
.sources-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.sources-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 6px 0;
    border-top: 1px solid rgb(var(--border-subtle));
    font-size: var(--fs-xs);
    line-height: 1.4;
}
.sources-row:first-child { border-top: 0; }
.sources-row-main {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
    flex: 1;
    text-decoration: none;
    color: inherit;
}
.sources-row-main:hover .sources-row-name { color: rgb(var(--text-link)); }
.sources-row-type {
    color: rgb(var(--text-primary));
    font-weight: 600;
    white-space: nowrap;
}
.sources-row-name {
    color: rgb(var(--text-muted));
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sources-row-meta {
    display: inline-flex;
    align-items: baseline;
    gap: 10px;
    color: rgb(var(--text-faint));
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.sources-row-fields {
    color: rgb(var(--text-secondary));
    background: rgb(var(--surface-inset));
    border-radius: 999px;
    padding: 1px 8px;
    font-weight: 500;
}

/* ────────────────────────────────────────────────────────── */
/* 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-family: 'Geist Mono', ui-monospace, monospace;
    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-family: 'Geist Mono', ui-monospace, monospace;
    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 + signatory + Mark as Approved + Sources/Activity
   + Generate/XLS/Originals. It must never share a row with the tabs —
   its ~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;
}
.tab-tools .field-summary {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-xs);
    color: rgb(var(--text-muted));
    letter-spacing: 0.02em;
    margin-right: 2px;
    white-space: nowrap;
}
.tab-tools .field-summary .miss {
    color: rgb(var(--accent-red-strong));
    font-weight: 700;
}
.tab-tools .field-summary .inf {
    color: rgb(var(--accent-amber-strong));
    font-weight: 700;
}

/* Sources toggle button — same visual family as other small
   bordered chips in the tab-tools row. The "on" state is set
   by JS while the drawer is open. */
.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));
}
.src-btn.is-open {
    color: rgb(var(--text-primary));
    background: rgb(var(--surface-inset));
    border-color: rgb(var(--text-faint));
}

/* Missing-fields strip: red-toned banner that appears between
   the tab strip and the preview when the active doc has missing
   required fields. Self-removes via JS when zero remain. */
.missing-fields-strip {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 9px 18px;
    background: rgb(var(--accent-red-soft-bg));
    border-bottom: 1px solid rgb(var(--accent-red-soft-text) / 0.25);
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-xs);
    color: rgb(var(--accent-red-soft-text));
}
.missing-fields-strip .mfs-label {
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.missing-fields-strip .mfs-jump {
    background: transparent;
    border: 0;
    color: rgb(var(--accent-red-soft-text));
    text-decoration: none;
    border-bottom: 1px dotted rgb(var(--accent-red-soft-text) / 0.5);
    padding: 1px 0;
    cursor: pointer;
    font-family: inherit;
    font-size: inherit;
}
.missing-fields-strip .mfs-jump:hover {
    border-bottom-style: solid;
    color: rgb(var(--accent-red-strong));
}
.missing-fields-strip .mfs-help {
    margin-left: auto;
    font-size: var(--fs-2xs);
    color: rgb(var(--text-muted));
}
/* 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.
   Replaces the old standalone .sources-panel block that lived
   below the preview. Toggled by .src-btn. 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-list {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    flex: 1;
}
.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;
}
.sources-drawer .sd-row:hover .sd-name {
    color: rgb(var(--text-link));
}
.sources-drawer .sd-type {
    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));
}
.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-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-2xs);
    color: rgb(var(--text-muted));
}
.sources-drawer .sd-meta b {
    color: rgb(var(--text-secondary));
    font-weight: 500;
}

/* "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;
    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-sm);
    padding: 5px 11px;
    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);
}
.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-family: 'Geist Mono', ui-monospace, monospace;
    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));
}

/* Scope badges for cell labels (Pass 2 use; defined now so the
   markup added in Pass 1 looks finished). BKG = booking-scoped,
   BYR = buyer-scoped, LOCKED = post-issue. */
.scope-badge {
    display: inline-block;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    padding: 1px 5px;
    border-radius: var(--radius-sm);
    color: rgb(255 255 255);
    vertical-align: 1px;
    margin-left: 4px;
}
.scope-badge.scope-booking { background: rgb(var(--accent-blue-strong)); }
.scope-badge.scope-buyer   { background: rgb(var(--accent-violet-strong)); }
.scope-badge.scope-locked  { background: 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;
}

/* 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, Invoice & Export, 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;
}

/* Locked-lot banner. Sits between the tab strip and the workspace body when
   the lot passes its post-departure lock (repository/lot_lock.go). The lock
   is a high-stakes state and deserves a full-width announcement, not a
   corner badge. The class also keys the JS re-derivation: it appears ONLY
   in the lock banner's locked branch (and the FI banner's approved branch,
   which is data-fi-only scoped). */
.freeze-banner {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 18px;
    background: var(--freeze-bg);
    border-bottom: 1px solid var(--freeze-border);
    font-size: var(--fs-sm);
    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 banner: an inline reason input
   + button. Typing the mandatory reason IS the deliberate two-step, so no
   armed confirm here. */
.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); }
}

/* Informational lot-state banner — the mirror of .freeze-banner for the
   pending / grace / overridden states. Same full-width announcement so the
   lot's lifecycle state can't be missed, but a calmer surface: a status,
   not a lock. Only one banner branch ever renders (they share the slot). */
.state-prompt {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 18px;
    background: rgb(var(--surface-inset));
    border-bottom: 1px solid rgb(var(--border-default));
    font-size: var(--fs-sm);
    color: rgb(var(--text-secondary));
}
.state-prompt .sp-pill {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-weight: 800;
    font-size: var(--fs-2xs);
    letter-spacing: 0.10em;
    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;
}
.state-prompt .sp-text { flex: 1; }
.state-prompt .sp-text .mono { color: rgb(var(--text-primary)); }
.state-prompt .fb-by { font-weight: 600; }
/* 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: deliberately the loudest control on the row, painted in the
   bronze freeze family so it previews the state the click creates.
   .btn-lg 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);
}

/* 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-tag / .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-tag { 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-tag--req,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="overdue"] .rf-tag--req,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="overdue"] .rf-tag--req,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="overdue"] .rf-tag--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-tag--fill,
#lot-workspace[data-active-doc="shipment-advice"] .rail-field[data-st-shipment-advice="manual"] .rf-tag--fill,
#lot-workspace[data-active-doc="shipping-order"]  .rail-field[data-st-shipping-order="manual"] .rf-tag--fill,
#lot-workspace[data-active-doc="freight-invoice"] .rail-field[data-st-freight-invoice="manual"] .rf-tag--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,
    .cf-inbox-pill .pulse { animation: none !important; opacity: 0.85; }
}

.cf-inbox-pill {
    margin-left: auto;
    display: inline-flex; align-items: center; gap: 5px;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-3xs); font-weight: 600;
    padding: 2px 8px; border-radius: 999px;
    letter-spacing: 0.02em;
    color: white;
}
.cf-inbox-pill .pulse {
    width: 5px; height: 5px; border-radius: 50%;
    background: white; opacity: 0.95;
    animation: cp-pulse 1.6s ease-in-out infinite;
    flex-shrink: 0;
}
.cf-inbox-pill.review  { background: oklch(72% 0.16 80); }
.cf-inbox-pill.parsing { background: oklch(54% 0.18 254); }
.cf-inbox-pill[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-family: 'Geist Mono', ui-monospace, monospace;
    font-size: var(--fs-2xs);
    color: oklch(54% 0.010 250);
}
.cf-presence-chip.flash-done {
    background: oklch(94% 0.05 152);
    border-color: oklch(56% 0.16 152);
}
.cf-presence-chip.flash-done .dot { background: oklch(56% 0.16 152); animation: none; }

.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.
   ========================================================================= */
.board-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;
}
.board-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;
}
.board-spine { display: inline-flex; align-items: center; gap: 5px; }
.board-dot {
    width: 9px;
    height: 9px;
    border-radius: 999px;
    border: 1.5px solid rgb(var(--accent-amber-soft-border));
    background: transparent;
}
.board-dot.is-on {
    background: rgb(var(--accent-amber-strong));
    border-color: rgb(var(--accent-amber-strong));
}
.board-state {
    display: inline-block;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: var(--fs-xs);
    text-decoration: none;
}
a.board-state:hover { text-decoration: underline; }
.board-state.attention {
    background: rgb(var(--accent-red-soft-bg));
    color: rgb(var(--accent-red-soft-text));
}
.board-state.waiting {
    background: rgb(var(--accent-amber-soft-bg));
    color: rgb(var(--accent-amber-soft-text));
}
.board-state.ok { color: rgb(var(--text-muted)); }
