/* SailPing -- the page's one stylesheet (static/app.css), loaded by static/index.html
   after Leaflet's. It was the page's style block until 2026-10-18 (RULES.md 5.19 item 4,
   the split, S20d step 2); the tests read it joined back into the page: testkit.page_html(). */
/* ---- The brand's type (RULES.md 4.48): Geist for the interface, Sora for
   headlines and the wordmark, Geist Mono for container numbers and data.
   SELF-HOSTED, all SIL OFL (static/vendor/fonts/README.md): the page used to
   fetch Inter from Google on every load, which told Google about every
   visitor. Latin only for now; Arabic is a later phase. ---- */
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/static/vendor/fonts/geist-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/static/vendor/fonts/geist-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/static/vendor/fonts/geist-latin-600-normal.woff2') format('woff2'); }
@font-face { font-family: 'Geist Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/static/vendor/fonts/geist-mono-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'Geist Mono'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/static/vendor/fonts/geist-mono-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'Sora'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/static/vendor/fonts/sora-latin-600-normal.woff2') format('woff2'); }

/* ---- The brand's colours (RULES.md 4.48) ----
   Deep Sea #0B1E3A · Lens Blue #1668E3 · Signal Blue #4FB8FF (on dark)
   Steel #5B6B82 (secondary text) · Mist #E8F0FC (tint) · Fog #F3F6FA (surface)
   ⚠ A branded forwarder's portal overrides --accent and --accent-text with ITS
   colour (applyBranding); everything here is ours, the default. */
:root {
  --font-ui: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-display: 'Sora', 'Geist', -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'Geist Mono', 'SF Mono', Consolas, monospace;
  /* Look A v2, "Deep Sea" (RULES.md 5.20, his confirm 2026-10-19): light is
     shallow water -- a pale-blue band, white cards on a cool ground. */
  --bg: #eef2f7; --surface: #ffffff; --surface-2: #f6f8fb;
  --border: #e1e7ef; --border-light: #edf1f6;
  --text: #0b1e3a; --text2: #44587a; --text3: #5c6d86;
  --accent: #1668e3; --accent-strong: #1257c2; --accent-bg: #e8f0fd; --accent-ring: rgba(22,104,227,0.16);
  /* The soft blue EDGE of the open page's pill and of today in the week (look
     A v2) -- from --accent, so a forwarder's portal draws it in his colour. */
  --accent-edge: color-mix(in srgb, var(--accent) 38%, transparent);
  /* Accent used as TEXT. Lens Blue measures ~5.1:1 on white, so it clears
     WCAG AA as it is. A forwarder's own colour may not: the server sends a
     darkened variant for them (app._brand_from_company). */
  --accent-text: #1668e3;
  --on-accent: #ffffff;        /* text on an --accent surface */
  /* A FILLED control -- the main button, the chosen chip, the active page's
     count -- with white words on it. Ours is Lens Blue; a forwarder's is his
     colour darkened until white reads on it (applyBranding: accent_text). */
  --accent-fill: #1668e3;
  /* MOVING, as a status: in transit, at the port, a progress bar, a box on
     the journey line. Its own colour, never --accent: a forwarder's colour is
     his menu, buttons and links, and his In transit stays blue (RULES.md 5.20,
     "status colours fixed"; test_colour_meaning.py). */
  --move: #1668e3; --move-text: #1459c7; --move-bg: #e7effc;
  /* The band across the top: the bar on every page, and on the home the
     sentence and the four numbers (fitHomeBand). Always three layers. */
  --band-paint: radial-gradient(900px 300px at 86% -40%, rgba(22,104,227,0.22), transparent 70%),
                radial-gradient(560px 240px at 2% -20%, rgba(79,184,255,0.16), transparent 70%),
                linear-gradient(180deg, #dfe9f9 0%, #e9f0fa 100%);
  --band-base: #e4edf9; --band-line: rgba(22,104,227,0.10); --band-h: 64px;
  /* The side menu's own tint, in the band's family (his "a different color
     that goes with the same vibe, but still light"). */
  --sidebar-tint: #f3f7fd;
  --ease-out: cubic-bezier(.23, 1, .32, 1); --ease-in-out: cubic-bezier(.77, 0, .175, 1);
  --navy: #0b1e3a;
  /* OUR logo's two colours, ink and the blue of our box (RULES.md 4.69) --
     the logo's own, never --accent, which a forwarder's portal makes his. */
  --logo-ink: #0b1e3a; --logo-blue: #1668e3;
  /* Text and fills that sit ON a --navy surface. Needed because --navy
     INVERTS in dark mode -- it becomes near-white -- so anything that
     hardcoded #fff on top of it rendered white on white. Every primary
     button in the app was affected. */
  --on-navy: #ffffff;
  /* What each colour MEANS, everywhere (Phase 4, test_colour_meaning.py):
     accent = moving normally · green = done · amber = look soon ·
     red = costing money, or destructive · neutral = not started / not active. */
  --green: #15803d; --red: #b91c1c; --amber: #b45309; --purple: #7c3aed;
  --neutral: #5b6b82; --neutral-bg: #eef2f7;
  /* A status's DOT (Needs attention, the week ahead, the four numbers):
     brighter than its words, which must read as text. */
  --red-dot: #dc2626; --amber-dot: #e08a0b; --green-dot: #16a34a; --neutral-dot: #a9b5c6;
  /* The sidebar, and the margin the main column leaves for it. 232, not A
     v2's 240: those 8px were the operator's Users table's last room at a
     1280px laptop (a table that scrolls a pixel sideways closes its row
     menus -- S25d's journey run found it). */
  --sidebar-w: 232px;
  --green-bg: #e9f7ec; --red-bg: #fdeded; --amber-bg: #fdf2df; --purple-bg: #f3e8ff; --teal-bg: #e8f0fc;
  --card-shadow: 0 1px 1px rgba(11,30,58,0.04), 0 2px 6px -2px rgba(11,30,58,0.07), 0 16px 34px -22px rgba(11,30,58,0.28);
  --card-shadow-lg: 0 1px 2px rgba(11,30,58,0.05), 0 18px 36px -18px rgba(11,30,58,0.38);
  --pop-shadow: 0 12px 40px rgba(11,30,58,0.18);
  --ctl-ring: 0 0 0 1px rgba(11,30,58,0.09), 0 1px 2px rgba(11,30,58,0.05);
  --sidebar-bg: var(--sidebar-tint); --header-bg: var(--band-base); --table-stripe: #f6f8fb;
  --skeleton: #eceff1;
  --radius: 16px; --radius-sm: 10px; --radius-xs: 6px;
}

/* ---- Provider banner: the app's one honest answer to "is tracking working?" ---- */
.provider-banner { display: none; align-items: flex-start; gap: 14px; padding: 12px 16px;
  border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }
.provider-banner.show { display: flex; }
.provider-banner .pb-body { flex-grow: 1; min-width: 0; }
.provider-banner > .ic { width: 18px; height: 18px; margin-top: 1px; }
.provider-banner .pb-title { font-size: 13px; font-weight: 600; color: var(--text); }
.provider-banner .pb-detail { font-size: 13px; color: var(--text2); margin-top: 2px; }
/* From the status tokens, so dark mode has its own: the fixed pinks were
   light boxes with light words on the operator's dark dashboard. */
.pb-bad { border-color: color-mix(in srgb, var(--red) 28%, transparent); background: var(--red-bg); }
.pb-warn { border-color: color-mix(in srgb, var(--amber) 28%, transparent); background: var(--amber-bg); }
.pb-info { border-color: var(--border); background: var(--surface-2); }
/* The operator's System status as ONE line, on every page but his dashboard
   (RULES.md 4.60, D8). The dashboard has the card; this hides itself there. */
.system-strip { display: none; align-items: center; gap: 10px; padding: 6px 12px;
  margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 13px; color: var(--text2); }
.system-strip.show { display: flex; }
.system-strip > .ic { width: 16px; height: 16px; flex-shrink: 0; }
.system-strip > span { flex-grow: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.system-strip b { color: var(--text); }
body:not(.detail-mode) .content:has(> #view-dashboard:not(.hidden)) > .system-strip { display: none; }

/* Two widgets side by side -- the dashboard's routes and map, Performance's
   lanes and chart. A class, not an inline style: an inline style beat the
   phone rule and squeezed them side by side on a phone (RULES.md 4.60, D9). */
.widget-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.routes-table td:last-child, .routes-table th:last-child { text-align: right; white-space: nowrap; }

/* ---- Impersonation: deliberately loud, and dark so it cannot be mistaken
   for ordinary product chrome. ---- */
.impersonation-bar { display: none; align-items: center; gap: 12px; padding: 10px 16px;
  border-radius: var(--radius); margin-bottom: 16px;
  background: var(--navy); color: var(--on-navy); font-size: 13px; }
.impersonation-bar.show { display: flex; }
.impersonation-bar .ic { color: var(--on-navy); flex-shrink: 0; }
.impersonation-bar .btn { background: var(--on-navy); border-color: var(--on-navy); color: var(--navy); font-weight: 600; }
/* Same trap as .bulk-bar: the rule above is more specific than .btn-danger and
   would win the background while .btn-danger kept the colour, leaving a red
   button unreadable. Nothing destructive sits here today -- the rule is here so
   that the first one to arrive is visible on the day it is added, rather than
   after somebody screenshots it. */
.impersonation-bar .btn-danger { background: var(--red); border-color: var(--red); color: #fff; }

/* What this page holds, in one line, under the header (RULES.md 4.47, #3). */
.page-lede { font-size: 13px; color: var(--text2); margin: -4px 0 14px; }
.page-lede.hidden { display: none; }

/* ---- Compact dashboard stats ---- */
.stat-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
/* A row of 3 or 4 tiles says so with a class, never an inline style: an
   inline style beats every phone rule, which is how five tiles were forced
   into one 390px line (DISCOVERY D2, test_phone.py). */
.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Look A v2 everywhere (part 2): a card has no border, 20px corners and the
   soft shadow; its label is written as it reads; its number is Sora. One
   you can open lifts 2px under a mouse, as the home's numbers do. */
.stat-card { background: var(--surface); border: none; border-radius: 20px;
  padding: 18px 20px; box-shadow: var(--card-shadow); cursor: pointer;
  transition: transform .12s var(--ease-out), box-shadow .12s var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .stat-card[onclick]:hover { transform: translateY(-2px); box-shadow: var(--card-shadow-lg); transition-duration: .16s; }
}
.stat-label { font-size: 13px; font-weight: 500; color: var(--text2); }
.stat-value { font-family: var(--font-display); font-size: 28px; font-weight: 600; letter-spacing: -.02em; line-height: 1.1; margin-top: 8px;
  font-variant-numeric: tabular-nums; }
.stat-note { font-size: 12px; color: var(--text2); margin-top: 4px; }

/* ---- The home screen (RULES.md 5.20: item 24 part 1, then look A v2) ----
   In this order: the band's sentence and the four numbers on its edge; the
   week ahead; Needs attention beside Top routes and the map; the journey
   line ("his correction: the journey line stays in the bottom"). Every card
   20px round; a number, a day or a stop you can open lifts 2px on hover
   (motion 2: 160 ms in, 120 ms out -- a mouse only). */
.home-card { background: var(--surface); border: none; border-radius: 20px; box-shadow: var(--card-shadow); overflow: hidden; margin-bottom: 20px; }
.card-head { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 12px 20px; border-bottom: 1px solid var(--border-light); }
.card-head h2 { font-size: 15px; font-weight: 600; letter-spacing: 0; }
.card-head > .ic { width: 16px; height: 16px; color: var(--text3); }
.card-meta { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end;
  font-size: 13px; color: var(--text3); }
/* The band's sentence: what is costing money now, and the boxes, each a
   round chip that opens it (RULES.md 4.47 stage 3, "in a good way so they
   can notice it"). Gone when nothing is past its last free day. */
.home-headline { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; padding: 4px 0 24px; }
.home-headline.hidden { display: none; }
.home-headline p { font-family: var(--font-display); font-size: 24px; font-weight: 600; line-height: 1.25; letter-spacing: -.015em; margin-right: 8px; }
.home-headline .hl-red { color: var(--red); }
.box-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 2px 10px 2px 12px; border: none; border-radius: 999px;
  background: var(--red-bg); color: var(--red); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--red) 25%, transparent);
  font-family: var(--font-mono); font-size: 13px; font-weight: 500; cursor: pointer; }
.box-chip .ic { width: 13px; height: 13px; }
.box-more { font-size: 13px; color: var(--text2); }
/* The four numbers: a dot and its words, the number, what it means. Red
   money, amber look soon, blue moving, grey nothing to do -- the number
   keeps its card's colour. Each is a button that opens the report it
   counted. */
.today-row { gap: 16px; margin-bottom: 20px; }
/* Two by two once a quarter of the content is too narrow for a label on one
   line (a tablet beside the sidebar). */
@media (max-width: 1100px) { .today-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
button.stat-card { display: block; width: 100%; text-align: left; font: inherit; color: inherit;
  transition: border-color .15s, transform .16s var(--ease-out); }
button.stat-card:active { transform: scale(0.97); transition-duration: .12s; }
.today-card { position: relative; padding: 18px 20px; border: none; border-radius: 20px; box-shadow: var(--card-shadow);
  transition: transform .12s var(--ease-out), box-shadow .12s var(--ease-out); }
[data-theme="dark"] .today-card { background: linear-gradient(180deg, #15263f, #101e33); }
.today-card .stat-label { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 500; color: var(--text2);
  text-transform: none; letter-spacing: 0; padding-right: 32px; }
.today-card .stat-label::before { content: ''; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  background: var(--move); box-shadow: 0 0 0 4px var(--move-bg); }
.today-card .stat-value { display: block; font-family: var(--font-display); font-size: 40px; font-weight: 600; letter-spacing: -.02em; line-height: 1; margin: 16px 0 8px; }
.today-card .stat-note { display: block; font-size: 13px; color: var(--text3); min-height: 20px; }
.today-card .stat-go { position: absolute; top: 14px; right: 16px; width: 28px; height: 28px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; background: var(--surface-2); color: var(--text3);
  box-shadow: inset 0 0 0 1px var(--border-light); }
.today-card .stat-go .ic { width: 14px; height: 14px; }
.today-card.bad .stat-value { color: var(--red); }
.today-card.bad .stat-label::before { background: var(--red-dot); box-shadow: 0 0 0 4px var(--red-bg); }
.today-card.warn .stat-value { color: var(--amber); }
.today-card.warn .stat-label::before { background: var(--amber-dot); box-shadow: 0 0 0 4px var(--amber-bg); }
.today-card.calm .stat-value { color: var(--text3); }
.today-card.calm .stat-label::before { background: var(--neutral-dot); box-shadow: 0 0 0 4px var(--neutral-bg); }
@media (hover: hover) and (pointer: fine) {
  .today-card:hover, .week-day.can-open:hover {
    transform: translateY(-2px); box-shadow: var(--card-shadow-lg); transition-duration: .16s; }
}
/* The week ahead (his point 2): seven days in words -- today marked, what
   costs money now in red, what arrives in blue, free time ending amber when
   within 3 days (the tile's window) and grey after. A day opens its boxes. */
.week-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px; padding: 14px; }
.week-day { position: relative; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
  min-height: 104px; padding: 12px 14px 14px; border: none; border-radius: 16px; background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border-light); font: inherit; color: var(--text); text-align: left;
  transition: transform .12s var(--ease-out), box-shadow .12s var(--ease-out); }
.week-day.can-open { cursor: pointer; }
.week-day.today { background: var(--accent-bg); box-shadow: inset 0 0 0 1px var(--accent-edge); }
.week-day .wd-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 10px; padding-right: 16px; }
.week-day .wd-num { font-family: var(--font-display); font-size: 20px; font-weight: 600; line-height: 1; }
.week-day .wd-name { font-size: 13px; font-weight: 500; color: var(--text3); }
.week-day.today .wd-name { color: var(--accent-text); font-weight: 600; }
.week-day .wd-open { position: absolute; top: 14px; right: 12px; width: 14px; height: 14px; color: var(--text3); }
.wd-ev { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 13px; font-weight: 500; line-height: 1.25; }
.wd-ev::before { content: ''; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--neutral-dot); }
.wd-ev b { font-weight: 600; }
.wd-ev.bad { color: var(--red); }
.wd-ev.bad::before { background: var(--red-dot); box-shadow: 0 0 0 3px var(--red-bg); }
.wd-ev.move { color: var(--move-text); }
.wd-ev.move::before { background: var(--move); }
.wd-ev.warn { color: var(--amber); }
.wd-ev.warn::before { background: var(--amber-dot); }
.wd-ev.calm { color: var(--text2); }
.wd-none { font-size: 13px; color: var(--text3); }
/* A day's boxes (motion 7): a small list that grows out of the day --
   180 ms open from 96%, 120 ms closed. */
.week-card { position: relative; overflow: visible; }
.week-pop { position: absolute; z-index: 30; width: 272px; max-height: 320px; overflow-y: auto; padding: 8px 0; border-radius: 16px;
  background: var(--surface); box-shadow: 0 0 0 1px var(--border), var(--pop-shadow);
  transform-origin: top left; animation: pop-in .18s var(--ease-out); }
.week-pop.closing { animation: pop-out .12s var(--ease-out) forwards; }
.week-pop-head { padding: 4px 14px 8px; font-size: 12px; font-weight: 600; color: var(--text3); }
.week-pop-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 14px; font-size: 13px; color: var(--text2); }
.week-pop-row .cell-link { font-size: 13px; }
@keyframes pop-in { from { opacity: 0; transform: scale(.96); } }
@keyframes pop-out { to { opacity: 0; transform: scale(.96); } }
/* Needs attention beside Top routes and the map; one column on a narrow
   screen. */
.dash-grid { display: grid; grid-template-columns: minmax(0, 2.3fr) minmax(0, 1fr); gap: 20px;
  align-items: start; margin-bottom: 20px; }
.dash-grid > .home-card, .dash-side > .home-card { margin-bottom: 0; }
.dash-side { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
@media (max-width: 1100px) { .dash-grid { grid-template-columns: minmax(0, 1fr); } }
/* A card's count beside its title: red when money is at stake, amber when
   something should be looked at soon. */
.card-count { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 22px; padding: 0 8px; border-radius: 999px;
  background: var(--neutral-bg); color: var(--text2); font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  font-variant-numeric: tabular-nums; }
.card-count.bad { background: var(--red-bg); color: var(--red); }
.card-count.warn { background: var(--amber-bg); color: var(--amber); }
/* Top routes: the route, a bar for its share, how many. */
/* The header row is kept for a screen reader and sets the columns (a fixed
   table takes its widths from its first row), drawn at no height. */
.routes-table { table-layout: fixed; }
.routes-table thead th { height: 0; padding: 0; border: 0; font-size: 0; line-height: 0; background: none; }
.routes-table thead th:nth-child(2) { width: 84px; }
.routes-table thead th:last-child { width: 52px; }
.routes-table td { padding: 8px 12px 8px 20px; border-bottom: 1px solid var(--border-light); font-size: 14px; }
.routes-table td:first-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.routes-table td.route-bar { padding-left: 0; padding-right: 0; }
.routes-table td:last-child { padding-left: 12px; padding-right: 20px; text-align: right; font-family: var(--font-mono); font-size: 13px; font-weight: 500; }
.routes-table tr:last-child td { border-bottom: none; }
.routes-table tbody tr:hover td { background: none; }
.share-bar { height: 6px; border-radius: 999px; background: var(--border-light); overflow: hidden; }
.share-bar i { display: block; height: 100%; border-radius: 999px; background: var(--accent); }
#shipmentMap.map-box { height: 200px; }
/* The journey line ("Where everything is", B's): the four stops on one
   line, the sea given most of the width; every box at sea a dot at its own
   place on the voyage (getProgress); more than 3 at one place (a tenth of
   the voyage) one count. A stop opens its chip on the list. */
.journey { position: relative; height: 248px; margin: 0 20px; }
.journey-line { position: absolute; top: 78px; left: 10%; right: 10%; height: 4px; border-radius: 999px;
  background: linear-gradient(90deg, var(--neutral-dot) 0%, var(--move) 27.5%, var(--move) 75%, var(--green-dot) 100%); }
.stage-row { position: absolute; top: 46px; transform: translateX(-50%); width: 260px; display: flex; flex-direction: column; align-items: center;
  padding: 2px 8px 8px; border: none; border-radius: 20px; background: none; font: inherit; color: var(--text); text-align: center; cursor: pointer;
  transition: transform .12s var(--ease-out), box-shadow .12s var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .stage-row:hover { transform: translateX(-50%) translateY(-2px); box-shadow: none; }
  .stage-row:hover .stage-ring { box-shadow: 0 0 0 1px var(--border), var(--card-shadow-lg); }
}
.stage-ring { width: 64px; height: 64px; border-radius: 50%; background: var(--surface); display: flex; align-items: center; justify-content: center;
  box-shadow: 0 0 0 1px var(--border), 0 6px 14px -8px rgba(11,30,58,0.3); transition: box-shadow .16s var(--ease-out); }
.stage-core { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--neutral-bg); color: var(--text3); }
.stage-core .ic { width: 20px; height: 20px; }
.stage-row.moving .stage-core { background: var(--move-bg); color: var(--move); }
.stage-row.done .stage-core { background: var(--green-bg); color: var(--green); }
.stage-count { display: block; margin-top: 10px; font-family: var(--font-display); font-size: 28px; font-weight: 600; line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.stage-name { display: block; margin-top: 6px; font-size: 14px; font-weight: 600; }
.stage-note { display: block; margin-top: 2px; font-size: 12px; color: var(--text3); }
.stage-mini { display: flex; flex-wrap: nowrap; justify-content: center; gap: 6px; margin-top: 8px; }
.stage-mini span { display: inline-flex; align-items: center; min-height: 22px; padding: 0 10px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.stage-mini .bad { background: var(--red-bg); color: var(--red); }
.stage-mini .warn { background: var(--amber-bg); color: var(--amber); }
.box-dot { position: absolute; width: 10px; height: 10px; border-radius: 50%; background: var(--move); box-shadow: 0 0 0 2px var(--surface); }
.box-pile { position: absolute; top: 44px; transform: translateX(-50%); min-width: 24px; height: 24px; padding: 0 8px; border-radius: 999px;
  background: var(--move); color: #ffffff; font-family: var(--font-mono); font-size: 12px; font-weight: 600; line-height: 24px; text-align: center;
  box-shadow: 0 0 0 2px var(--surface), 0 6px 12px -6px var(--move); }
[data-theme="dark"] .box-pile { color: #0b1e3a; }
.box-pile::after { content: ""; position: absolute; left: 50%; bottom: -5px; margin-left: -4px; border: 4px solid transparent; border-top-color: var(--move); border-bottom: 0; }
/* Motion 3: the boxes at sea slide in from the In transit stop, once, when
   the home opens (240 ms each, all done by 300); the count bubbles fade in
   last. --dx is how far each one travels, set as it is drawn. */
.journey.arriving .box-dot { animation: dot-in .24s var(--ease-out) both; }
.journey.arriving .box-pile { animation: pile-in .12s ease .18s both; }
@keyframes dot-in { from { opacity: 0; transform: translateX(calc(var(--dx, 0px) * -1)); } }
@keyframes pile-in { from { opacity: 0; } }
/* Narrower than a laptop (a tablet, or a small laptop beside the menu): the
   week is a row of days that scrolls sideways, the journey line its four
   stops without the dots, and the bar keeps its room for the search. The
   phone block below tightens the same three further. */
@media (max-width: 1100px) {
  .week-days { display: flex; overflow-x: auto; scrollbar-width: none; }
  .week-day { flex: none; width: 132px; }
  .journey { height: auto; margin: 0; padding: 16px 8px 20px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
  .journey-line { top: 48px; left: 12.5%; right: 12.5%; }
  .stage-row, .stage-row:hover { position: relative; top: auto; left: auto !important; transform: none; width: auto; padding: 0; }
  .stage-mini { flex-wrap: wrap; }
  .box-dot, .box-pile, .journey-legend, .page-when { display: none; }
}
.legend-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--move); }
.legend-pile { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
  background: var(--move); color: #ffffff; font-family: var(--font-mono); font-size: 11px; font-weight: 600; }
[data-theme="dark"] .legend-pile { color: #0b1e3a; }

/* ---- Needs attention ----
   One line a box on a laptop: the number, why, the route, when -- fixed
   columns, so the reasons line up down the card. Two lines when narrow. */
.att-row { display: grid; grid-template-columns: 8px 104px minmax(0, 1fr) 132px 80px; align-items: center;
  column-gap: 12px; min-height: 46px; margin: 0 20px; padding: 8px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; }
.att-row:last-child { border-bottom: none; }
.att-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--neutral-dot); box-shadow: 0 0 0 4px var(--neutral-bg); }
.att-dot.bad { background: var(--red-dot); box-shadow: 0 0 0 4px var(--red-bg); }
.att-dot.warn { background: var(--amber-dot); box-shadow: 0 0 0 4px var(--amber-bg); }
.att-reason, .att-route { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.att-reason { font-size: 14px; font-weight: 500; color: var(--text2); }
.att-reason.bad { color: var(--red); }
.att-reason.warn { color: var(--amber); }
.att-route { color: var(--text3); }
.att-when { color: var(--text3); text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.att-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 8px 20px;
  border-top: 1px solid var(--border-light); font-size: 13px; color: var(--text3); }
@media (max-width: 1100px) {
  .att-row { grid-template-columns: 8px minmax(0, 1fr) auto; row-gap: 2px;
    grid-template-areas: "dot num when" ". reason reason" ". route route"; }
  .att-dot { grid-area: dot; }
  .att-num { grid-area: num; }
  .att-when { grid-area: when; }
  .att-reason { grid-area: reason; white-space: normal; }
  .att-route { grid-area: route; font-size: 12px; }
}

/* ---- Detail fact cards ----
   Four across, not three: the vessel was buried in an eleven-field grid below
   the fold, and "which ship is it on" is a question people ask out loud. Wraps
   to two, then one, so it never squeezes to unreadable. */
.fact-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 16px; }
@media (max-width: 1100px) { .fact-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .fact-row { grid-template-columns: 1fr; } }
.fact-card { background: var(--surface); border: none; border-radius: 20px;
  padding: 18px 20px; box-shadow: var(--card-shadow); }
.fact-label { font-size: 13px; font-weight: 500; color: var(--text2); }
.fact-value { font-family: var(--font-display); font-size: 20px; font-weight: 600; letter-spacing: -.015em; margin-top: 8px; }
.fact-note { font-size: 13px; color: var(--text2); margin-top: 2px; }
/* Look soon / money: the status's own wash and edge -- from the tokens, so
   dark mode has its own (the fixed creams were light cards on a dark page). */
.fact-card.warn { background: var(--amber-bg); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--amber) 28%, transparent), var(--card-shadow); }
.fact-card.warn .fact-label, .fact-card.warn .fact-value, .fact-card.warn .fact-note { color: var(--amber); }
.fact-card.bad { background: var(--red-bg); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--red) 28%, transparent), var(--card-shadow); }
.fact-card.bad .fact-label, .fact-card.bad .fact-value, .fact-card.bad .fact-note { color: var(--red); }

/* ---- The route strip ----
   Load port on the left, discharge port on the right, the box somewhere
   between them. It is the first thing on the detail page because it is the
   thing people open the page to find out, and reading it needs no numbers:
   how far along the bar the ship sits IS the answer.

   Mohammad, 2026-09-15, with SafeCube's screens attached: "for any shippment
   or container when you click on it they have a more relerfing UI". */
.route-strip { background: var(--surface); border: none;
  border-radius: 20px; padding: 20px 24px; margin-bottom: 16px;
  box-shadow: var(--card-shadow); }
.rs-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
.rs-port { min-width: 0; flex: 1 1 0; }
.rs-port.to { text-align: right; }
.rs-label { font-size: 13px; font-weight: 500; color: var(--text3); }
.rs-name { font-family: var(--font-display); font-size: 20px; font-weight: 600; letter-spacing: -.015em; margin-top: 4px; word-break: break-word; }
.rs-date { font-size: 13px; color: var(--text2); margin-top: 2px; }
.rs-rail { position: relative; height: 28px; margin: 18px 2px 0; }
.rs-track { position: absolute; left: 0; right: 0; top: 11px; height: 6px;
  background: var(--border-light); border-radius: 999px; }
.rs-fill { position: absolute; left: 0; top: 11px; height: 6px;
  background: var(--green); border-radius: 999px; transition: width .24s var(--ease-out); }
.rs-end { position: absolute; top: 7px; width: 14px; height: 14px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--border); box-sizing: border-box; }
.rs-end.start { left: 0; background: var(--green); border-color: var(--green); }
.rs-end.finish { right: 0; }
.rs-end.finish.done { background: var(--green); border-color: var(--green); }
/* The marker is clamped to 2%..98% in JS so it never hangs off either end. */
.rs-boat { position: absolute; top: 0; transform: translateX(-50%); width: 28px; height: 28px;
  border-radius: 50%; background: var(--surface); border: 2px solid var(--move);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 0 0 4px var(--move-bg); box-sizing: border-box; }
.rs-boat .ic { width: 14px; height: 14px; color: var(--move); }
.rs-boat.done { border-color: var(--green); box-shadow: none; }
.rs-boat.done .ic { color: var(--green); }
.rs-foot { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 14px; font-size: 12px; color: var(--text2); flex-wrap: wrap; }
.rs-foot b { color: var(--text); font-weight: 600; }

/* ---- The event timeline, as a table ----
   It was a stacked list where every row repeated the same three shapes at
   three different sizes. A table lets the eye run down one column -- which is
   how somebody actually reads a timeline: dates first, then across. */
.ev-table { width: 100%; border-collapse: collapse; }
.ev-table th { text-align: left; font-size: 11px; text-transform: uppercase;
  letter-spacing: .5px; color: var(--text2); font-weight: 600;
  padding: 0 10px 8px; border-bottom: 1px solid var(--border-light); white-space: nowrap; }
.ev-table td { padding: 10px; border-bottom: 1px solid var(--border-light);
  font-size: 13px; vertical-align: top; }
.ev-table tbody tr:last-child td { border-bottom: 0; }
.ev-rail { width: 24px; padding: 10px 0 10px 2px !important; position: relative; }
/* One continuous line down the column, trimmed at the first and last dot so it
   does not float above the top row or below the bottom one. */
.ev-rail::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0;
  width: 2px; background: var(--border-light); transform: translateX(-50%); }
.ev-table tbody tr:first-child .ev-rail::before { top: 14px; }
.ev-table tbody tr:last-child .ev-rail::before { bottom: calc(100% - 16px); }
.ev-dot { position: relative; z-index: 1; width: 12px; height: 12px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--border); margin: 4px auto 0;
  box-sizing: border-box; }
.ev-dot.done { background: var(--green); border-color: var(--green); }
.ev-dot.current { background: var(--move); border-color: var(--move);
  box-shadow: 0 0 0 3px var(--move-bg); }
.ev-dot.est { border-style: dashed; border-color: var(--text3); background: var(--surface); }
.ev-when { white-space: nowrap; font-weight: 600; }
.ev-sub { font-size: 11px; color: var(--text3); font-weight: 400; margin-top: 1px; }
/* "Journey so far" and "Expected next" (RULES.md 4.60, D2). */
.ev-table tr.ev-group td { font-size: 12px; font-weight: 600; color: var(--text2);
  padding: 14px 10px 6px; border-bottom: 1px solid var(--border-light); }
.ev-table tr.ev-group:first-child td { padding-top: 8px; }
.ev-table tr.ev-none td { font-weight: 400; color: var(--text3); border-bottom: 0; }
.ev-loc { color: var(--text2); }
/* Where an event came from (RULES.md 4.65 item 18): a quiet outlined tag, so
   it reads as a label and not as a status. */
.ev-source { display: inline-block; margin-left: 6px; padding: 0 8px; border: 1px solid var(--border);
  border-radius: 999px; font-size: 11px; font-weight: 400; color: var(--text2); white-space: nowrap;
  vertical-align: middle; }
.ev-scroll { max-height: 420px; overflow-y: auto; }

/* ---- The paste grid on the import screen ----
   A spreadsheet is the shape this data already lives in, so the manual entry
   is a spreadsheet too: paste a block out of Excel and it lands in the right
   columns. Column A is the identifier; the rest are the customer's OWN
   fields, by name (RULES.md 4.6a). */
.pg-or { display: flex; align-items: center; gap: 12px; margin: 18px 0 12px;
  color: var(--text3); font-size: 12px; }
.pg-or::before, .pg-or::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* A real spreadsheet, not one enormous column. `width: 100%` used to stretch
   whatever columns existed across the whole dialogue, so an account with no
   custom fields got ONE column a thousand pixels wide — which is what he
   reported. Columns are a fixed width now and a spacer cell absorbs the
   leftover, exactly as a sheet looks. */
.pg-wrap { border: 1px solid var(--border); border-radius: var(--radius);
  overflow: auto; max-height: 40vh; background: var(--surface); }
.pg { border-collapse: collapse; width: 100%; font-size: 12px; }
.pg th { position: sticky; top: 0; z-index: 1; background: var(--surface-2);
  border-bottom: 1px solid var(--border); border-right: 1px solid var(--border-light);
  padding: 6px 8px; text-align: left; font-size: 11px; font-weight: 600;
  color: var(--text2); white-space: nowrap; letter-spacing: .02em; }
/* A width AND a floor. `table-layout: auto` treats width as a suggestion and
   sizes to the heading, so "PICKUP#" came out 67px while "DEPOSCO DELIVERY
   LOCATION" took 192 — the floor evens that out without clipping a field name
   the way `table-layout: fixed` would. */
.pg th, .pg td { width: 165px; min-width: 125px; }
/* The slack. Not a data column: it has no input and is never read, it just
   stops the real columns being stretched to fill the dialogue. */
.pg th.pg-sp, .pg td.pg-sp { width: auto; min-width: 0; border-right: 0;
  background: var(--surface); }
.pg th.pg-n, .pg td.pg-n { width: 34px; min-width: 34px; text-align: right;
  color: var(--text3); background: var(--surface-2); font-variant-numeric: tabular-nums;
  padding-right: 8px; }
.pg td { border-bottom: 1px solid var(--border-light);
  border-right: 1px solid var(--border-light); padding: 0; }
.pg input { width: 100%; border: 0; background: none; padding: 4px 8px;
  font: inherit; color: var(--text); outline: none; }
.pg input:focus { background: var(--accent-bg); box-shadow: inset 0 0 0 2px var(--accent); }
.pg tr:last-child td { border-bottom: 0; }
.pg-foot { display: flex; align-items: center; gap: 10px; margin-top: 10px;
  font-size: 12px; color: var(--text2); flex-wrap: wrap; }

/* ---- Brand preview: what their customer actually meets ----
   A colour picker tells you nothing about what a stranger will see, so the
   screen shows the sign-in page itself, twice: as it is now and as it would
   be. Preview-before-publish is the standard pattern for white-label settings
   and this is why (RULES.md 4.32). */
.bp-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 860px) { .bp-pair { grid-template-columns: 1fr; } }
.bp-label { font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text3); margin-bottom: 6px;
  display: flex; align-items: center; gap: 6px; }
.bp { border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; background: #fff; }
.bp-chrome { display: flex; align-items: center; gap: 6px; padding: 8px 10px;
  background: #eceff3; border-bottom: 1px solid #dfe3e9; }
.bp-dot { width: 8px; height: 8px; border-radius: 50%; background: #c8cdd6; }
.bp-url { margin-left: 6px; font-size: 11px; color: #5b6472; background: #fff;
  border-radius: 20px; padding: 4px 10px; flex: 1; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; font-family: var(--mono, monospace); }
/* The screen inside the frame IS the sign-in page: drawn at 1280x760 from
   the page's own classes and scaled to fit (fitBrandPreviews) -- a true small
   copy, not a drawing of one, so it cannot fall out of step with the real
   page (RULES.md 4.56). Nothing in it can be clicked. */
.bp-screen { position: relative; overflow: hidden; background: var(--surface);
  height: calc(760px * var(--bp-scale, .4)); }
.bp-scale { position: absolute; top: 0; left: 0; width: 1280px; height: 760px;
  transform: scale(var(--bp-scale, .4)); transform-origin: top left;
  pointer-events: none; user-select: none;
  /* The frame the copy is sized by -- see the .sp rules beside .lh-title. */
  container-type: size; }
.sp { display: grid; grid-template-columns: minmax(0, 45fr) minmax(0, 55fr);
  width: 100%; height: 100%; background: var(--bg); text-align: left; }
/* 🔴 Our card ("Without your branding") wears OUR colours whoever looks at
   it. Not inherited: on a forwarder's Settings the page around it wears HIS
   (applyBranding sets them on the root), and our card showed his button.
   Not written onto the card either: that pinned the light blue in dark mode.
   These are the stylesheet's own values for our page, light and dark. */
.sp.sp-ours { --accent: #1668e3; --accent-text: #1668e3; --on-accent: #ffffff; --accent-fill: #1668e3; }
[data-theme="dark"] .sp.sp-ours { --accent: #4fb8ff; --accent-text: #4fb8ff; --on-accent: #0b1e3a; --accent-fill: #2a72e5; }
.sp .bp-flabel { font-size: 14px; font-weight: 500; color: var(--text); margin-bottom: 6px; }
.sp .bp-input { height: 48px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); }

/* ---- Table cell sub-labels ---- */
.cell-sub { font-size: 12px; color: var(--text2); margin-top: 1px; }
.cell-main { font-weight: 500; }
.route-cell { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.route-cell .ic { color: var(--text3); width: 13px; height: 13px; flex-shrink: 0; }
/* Dark: Deep Sea surfaces, Signal Blue for actions (RULES.md 4.48 — "on dark"). */
/* Look A v2's dark: the deep sea -- a darker ground, cards a step up from it,
   the band a blue glow, the menu deep water. Signal Blue stays the words'
   blue (RULES.md 4.48); a filled button is a deeper blue with white on it. */
[data-theme="dark"] {
  /* The browser's own controls (tick-boxes, selects' lists, scrollbars) in
     their dark form too: a white tick-box on every row of a dark list. */
  color-scheme: dark;
  --bg: #070f1b; --surface: #0f1c2f; --surface-2: #132239;
  --border: #233149; --border-light: #1a283e;
  --text: #e7eef8; --text2: #b3c1d4; --text3: #94a4bb;
  --accent: #4fb8ff; --accent-strong: #2fa4f5; --accent-bg: rgba(79,163,255,0.13); --accent-ring: rgba(79,184,255,0.24);
  --accent-edge: color-mix(in srgb, var(--accent) 55%, transparent);
  --accent-text: #4fb8ff;
  --on-accent: #0b1e3a;        /* Signal Blue is light: Deep Sea text on it */
  --accent-fill: #2a72e5;      /* A v2 drew #2f7ff0; this is the nearest that carries white at 4.5:1 */
  --move: #4fa3ff; --move-text: #8cc4ff; --move-bg: rgba(79,163,255,0.15);
  --band-paint: radial-gradient(1000px 380px at 84% -36%, rgba(47,127,240,0.58), transparent 70%),
                radial-gradient(640px 300px at 8% -46%, rgba(79,184,255,0.22), transparent 70%),
                linear-gradient(180deg, #0d1d35 0%, #0a1626 100%);
  --band-base: #0c1a2f; --band-line: rgba(255,255,255,0.06);
  --sidebar-tint: #0a1626;
  --navy: #e8f0fc;
  --on-navy: #0b1e3a;
  --logo-ink: #e7eef8; --logo-blue: #4fb8ff;
  --green: #4ade80; --red: #f87171; --amber: #fbbf24; --purple: #c4b5fd;
  --neutral: #a3b3c9; --neutral-bg: rgba(255,255,255,0.07);
  --red-dot: #f26464; --amber-dot: #f2b33d; --green-dot: #34c46e; --neutral-dot: #5d6e88;
  --green-bg: rgba(74,222,128,0.13); --red-bg: rgba(255,110,110,0.15); --amber-bg: rgba(251,194,60,0.14); --purple-bg: rgba(196,181,253,0.14); --teal-bg: rgba(79,184,255,0.14);
  --card-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 0 0 1px rgba(255,255,255,0.06), 0 20px 40px -26px rgba(0,0,0,0.9);
  --card-shadow-lg: inset 0 1px 0 rgba(255,255,255,0.06), 0 22px 40px -20px rgba(0,0,0,0.95);
  --pop-shadow: 0 12px 40px rgba(0,0,0,0.55);
  --ctl-ring: 0 0 0 1px rgba(255,255,255,0.10);
  --table-stripe: #122137;
  --skeleton: #1a283e;
}
/* A forwarder's portal (RULES.md 5.20, his "A", 2026-10-19): the band and the
   side menu take HIS colour -- a light tint of it in light mode (mixed into
   white), a deep one in dark (into a neutral near-black, so his hue shows;
   into our navy it stayed navy) -- by a recipe that gives back ours, within
   a shade, from Lens Blue; ours stays exactly as above wherever no brand is
   set (test_brand_tint.py). applyBranding marks the root data-branded
   and writes his colour (--accent) and his two text colours; a value written
   on the root would beat the dark theme, so the choice is made here. His
   preview card (.sp, Settings → Branding) wears data-branded too. */
[data-branded] { --accent-text: var(--accent-on-light); --accent-strong: var(--accent-on-light); }
[data-theme="dark"][data-branded], [data-theme="dark"] [data-branded] {
  --accent-text: var(--accent-on-dark); --accent-strong: var(--accent-on-dark); }
:root[data-branded] {
  --band-paint: radial-gradient(900px 300px at 86% -40%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
                radial-gradient(560px 240px at 2% -20%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
                linear-gradient(180deg, color-mix(in srgb, var(--accent) 13%, #ffffff) 0%, color-mix(in srgb, var(--accent) 8%, #ffffff) 100%);
  --band-base: color-mix(in srgb, var(--accent) 11%, #ffffff);
  --band-line: color-mix(in srgb, var(--accent) 12%, transparent);
  --sidebar-tint: color-mix(in srgb, var(--accent) 4%, #ffffff);
}
:root[data-branded][data-theme="dark"] {
  --band-paint: radial-gradient(1000px 380px at 84% -36%, color-mix(in srgb, var(--accent) 42%, transparent), transparent 70%),
                radial-gradient(640px 300px at 8% -46%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%),
                linear-gradient(180deg, color-mix(in srgb, var(--accent) 16%, #0a0e14) 0%, color-mix(in srgb, var(--accent) 9%, #0a0e14) 100%);
  --band-base: color-mix(in srgb, var(--accent) 13%, #0a0e14);
  --band-line: rgba(255,255,255,0.06);
  --sidebar-tint: color-mix(in srgb, var(--accent) 9%, #0a0e14);
}
* { margin:0; padding:0; box-sizing:border-box; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body { font-family: var(--font-ui); background: var(--bg); color: var(--text); display: flex; min-height: 100vh; font-size: 16px; line-height: 1.5; letter-spacing: -0.006em; }
/* Read by assistive technology, not drawn. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* ---- The SailPing logo (RULES.md 4.69, his pick 2026-10-20): the Stack
   "Ribs" MARK -- three containers, ours (the middle one, pulled out) in
   blue -- beside the WORD SailPing in Sora SemiBold, as one lockup. Drawn
   here, not an image, so it takes the theme: --logo-ink and --logo-blue,
   Deep Sea and Lens Blue in light, #e7eef8 and Signal Blue in dark and on
   the dark sign-in panel (.login-hero). Light and dark only, no one-colour
   set (his words: "no need for others colords, like the full black or full
   white"). Never --accent: that is a forwarder's colour on his portal. The
   word is sized to the mark as his lockup draws it: its capitals 0.46 of
   the mark's height (font-size ~0.63 of it), -0.015em the lockup's width.
   The mark (clMark) fills its slot; each slot gives it its size. ---- */
.cl-word { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.015em;
  color: var(--logo-ink); white-space: nowrap; }
.cl-mark { display: block; width: 100%; height: 100%; }
.cl-ink { fill: var(--logo-ink); }
.cl-blue { fill: var(--logo-blue); }
/* "Powered by SailPing" under a forwarder's form (brandFootHTML): our small
   mark -- the 16 px drawing, at 16 px -- and our name in our ink, as his
   file powered-by-sailping draws it; "Powered by" stays the line's grey. */
.cl-by { display: inline-flex; align-items: center; gap: 4px; margin-left: 2px; vertical-align: middle; }
.cl-by .cl-mark { width: 16px; height: 16px; flex: none; }
.cl-by .cl-word { font-size: 12px; }
b, strong { font-weight: 600; }
::selection { background: var(--accent); color: var(--on-accent); }

/* Icon system */
.ic { width: 1em; height: 1em; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; vertical-align: -0.15em; flex-shrink: 0; }

/* ---------- Sidebar ----------
   Look A v2 (RULES.md 5.20): its own light tint in the band's family (deep
   water in dark), the open page a soft round pill with a blue edge, live
   counts, the four groups kept (4.22), the account card pinned to its foot,
   and it folds to icons (toggleNavFold, remembered on this browser). */
.sidebar { width: var(--sidebar-w); background: var(--sidebar-bg); border-right: 1px solid var(--border); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 50; }
/* Motion 6, the fold (220 ms): ONLY while the person folds it
   (body.nav-folding, set by toggleNavFold) -- a width transition left on
   for good also ran whenever the window's height was re-measured, and a
   full-page picture drew the menu 30px too wide over the page. */
body.nav-folding .sidebar { transition: width .22s var(--ease-in-out); }
body.nav-folding .main { transition: margin-left .22s var(--ease-in-out); }
.sidebar-logo { padding: 20px 24px 8px; min-height: 64px; display: flex; align-items: center; gap: 12px; }
.logo-mark { width: 30px; height: 30px; border-radius: 6px; background: linear-gradient(135deg, var(--accent), #0b1e3a); display: flex; align-items: center; justify-content: center; color: #fff; flex-shrink: 0; }
.logo-mark .ic { width: 16px; height: 16px; }
/* OUR mark SHOWS again, beside the word (RULES.md 4.69, his new logo, the
   lockup). Until then it was hidden (4.49, 4.50: "remove the eye from the
   beginning of the brand name") -- that was about the old eye. `.is-ours`
   (set by applyBranding) now only sizes ours: no box, no gradient, the
   mark's own two colours; a forwarder's mark -- his logo, or the ship --
   keeps its box. 32px, not the 28 his preview drew: the 32-grid mark is
   sharp only at whole multiples of 32 -- at 28 its ribs smeared to grey on
   a 1x screen -- and the word at 20 is the lockup's size for it. */
.sidebar-logo .logo-mark.is-ours { width: 32px; height: 32px; border-radius: 0; }
.sidebar-logo .logo-text { font-size: 16px; font-weight: 700; color: var(--text); letter-spacing: -0.01em; }
.sidebar-logo .logo-text .cl-word { font-size: 20px; }
.sidebar-nav { flex: 1; padding: 4px 12px 16px; display: flex; flex-direction: column;
  gap: 12px; overflow-y: auto; overflow-x: hidden; }
/* One section: a quiet heading, then its screens. */
.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-head { font-size: 11px; font-weight: 600; color: var(--text3); white-space: nowrap;
  text-transform: uppercase; letter-spacing: .12em; padding: 8px 12px 6px; }
.nav-item { display: flex; align-items: center; gap: 12px; min-height: 40px; padding: 8px 8px 8px 12px;
  border-radius: 999px; color: var(--text2); cursor: pointer;
  font-size: 14px; font-weight: 500; transition: background-color .12s, color .12s, box-shadow .12s;
  border: none; background: none; width: 100%; text-align: left;
  font-family: inherit; position: relative; }
.nav-item:hover { background: var(--surface-2); color: var(--text); }
.nav-item.active { background: var(--surface); color: var(--text); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--accent-edge), 0 8px 18px -12px var(--accent); }
[data-theme="dark"] .nav-item.active { background: var(--accent-bg); }
.nav-item .nav-icon { width: 18px; height: 18px; flex: none; display: flex; align-items: center; justify-content: center; color: var(--text3); }
.nav-item.active .nav-icon { color: var(--accent-text); }
.nav-item .nav-icon .ic { width: 18px; height: 18px; }
.nav-item .nav-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .1s ease; }
/* How many are on the screen that button opens. Empty when there is nothing to
   say, and `:empty` keeps the padding from drawing an empty pill. Red when
   money is at stake (Dashboard: Needs attention holds a red item). */
.nav-count { min-width: 24px; height: 22px; padding: 0 8px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px; background: var(--neutral-bg); font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  color: var(--text2); font-variant-numeric: tabular-nums; }
.nav-count:empty { display: none; }
.nav-count.is-bad { background: var(--red-dot); color: #ffffff; box-shadow: 0 4px 10px -4px var(--red-dot); }
/* The fold, at the menu's edge. Only where the menu stands beside the page. */
.nav-fold { position: absolute; top: 24px; right: -14px; z-index: 2; width: 28px; height: 28px; padding: 0;
  display: flex; align-items: center; justify-content: center; border: none; border-radius: 50%;
  background: var(--surface); color: var(--text3); cursor: pointer; box-shadow: var(--ctl-ring), 0 6px 14px -8px rgba(11,30,58,0.3); }
.nav-fold:hover { color: var(--text); }
.nav-fold .ic { width: 14px; height: 14px; transition: transform .22s var(--ease-in-out); }
body.nav-folded .nav-fold .ic { transform: rotate(180deg); }
/* The account card, pinned to the menu's foot (his note on A v2: "bottom
   left"). The ONE account button: on a phone placeAccount() moves it into
   the header, where the drawer cannot hide it. */
.sidebar-foot { padding: 8px 8px 12px; }
.sidebar-foot .account-wrap { width: 100%; }
.sidebar-foot .account-btn { width: 100%; max-width: none; min-height: 56px; padding: 8px; gap: 8px; border: none; border-radius: 20px;
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--border-light); text-align: left; }
[data-theme="dark"] .sidebar-foot .account-btn { background: rgba(255,255,255,0.04); }
.sidebar-foot .account-btn:hover, .sidebar-foot .account-btn[aria-expanded="true"] { box-shadow: inset 0 0 0 1px var(--border); }
.sidebar-foot .account-menu { top: auto; bottom: calc(100% + 8px); left: 0; right: auto; }
.account-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.account-sub { font-size: 12px; font-weight: 400; color: var(--text3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Folded: icons only (72px); a name shows beside its icon on hover, in ONE
   tip drawn over the page (#navTip, showNavTip -- the menu scrolls, and a
   name inside it would be cut at its edge): the first after 300 ms, the
   next ones at once while the pointer stays in the menu. The name stays in
   the button for a screen reader. The foot is the round initials alone. */
@media (min-width: 769px) {
body.nav-folded { --sidebar-w: 72px; }
body.nav-folded .sidebar-logo { padding: 20px 0 8px; justify-content: center; }
/* Folded, the mark alone -- ours or his -- and the name is put away. */
body.nav-folded .sidebar-logo .logo-text { display: none; }
body.nav-folded .sidebar-nav { padding: 4px 12px 16px; align-items: center; }
body.nav-folded .nav-group { align-items: center; }
body.nav-folded .nav-head { font-size: 0; padding: 0; width: 32px; height: 1px; margin: 6px 0; background: var(--border); }
body.nav-folded .nav-item { width: 48px; padding: 8px; justify-content: center; }
body.nav-folded .nav-item .nav-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
.nav-tip { position: fixed; z-index: 130; padding: 6px 12px; border-radius: 999px; background: var(--text); color: var(--bg);
  font-size: 13px; font-weight: 500; white-space: nowrap; box-shadow: var(--pop-shadow); pointer-events: none;
  opacity: 0; transition: opacity .12s ease; }
.nav-tip.show { opacity: 1; }
.nav-tip.instant { transition: none; }
body.nav-folded .nav-count { position: absolute; top: 0; right: -2px; min-width: 18px; height: 18px; padding: 0 4px; font-size: 11px;
  box-shadow: 0 0 0 2px var(--sidebar-bg); }
body.nav-folded .sidebar-foot { display: flex; justify-content: center; }
body.nav-folded .sidebar-foot .account-btn { width: auto; min-height: 0; padding: 6px; border-radius: 50%; background: none; box-shadow: none; }
body.nav-folded .sidebar-foot .account-text, body.nav-folded .sidebar-foot .account-caret { display: none; }
body.nav-folded .sidebar-foot .account-menu { left: 0; }
}

/* ---------- Header ---------- */
/* appShell is a flex item of <body>, and a flex item defaults to min-width:auto
   -- it refuses to shrink below its own content. So the widest table on a page
   (Users, at ~1063px) dragged the entire shell past the viewport and the whole
   window scrolled sideways: sidebar and header included, which looks broken
   rather than merely tight. min-width:0 lets the shell match the window, and
   the wide table then scrolls inside its own .table-scroll container, which is
   what every other table already did. */
#appShell { flex: 1 1 auto; min-width: 0; width: 100%; }
/* The band (look A v2): one paint, --band-paint, drawn on the main column
   from its top to --band-h -- the bar's height on every page, and on the home
   down to the middle of the four numbers (fitHomeBand), so they sit on its
   edge. The bar carries the same paint at the same size, so where the two
   overlap they are the same pixels, and a scrolled bar still wears the
   band's top. The line under the band is the first layer. */
.main { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; min-width: 0;
  background-image: linear-gradient(var(--band-line), var(--band-line)), var(--band-paint);
  background-size: 100% 1px, 100% var(--band-h), 100% var(--band-h), 100% var(--band-h);
  background-position: 0 var(--band-h), 0 0, 0 0, 0 0;
  background-repeat: no-repeat; }
.header { min-height: 64px; padding: 12px 28px; display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background-color: var(--header-bg); background-image: var(--band-paint); background-size: 100% var(--band-h);
  background-position: 0 0; background-repeat: no-repeat;
  box-shadow: 0 1px 0 var(--band-line); position: sticky; top: 0; z-index: 40; }
body.home-band .header { box-shadow: none; }
.header-left { font-family: var(--font-display); font-weight: 600; font-size: 20px; display: flex; align-items: center; gap: 12px; letter-spacing: -0.01em; min-width: 0; }
/* The day, beside Dashboard (the reader's zone). */
.page-when { font-family: var(--font-ui); font-size: 14px; font-weight: 400; color: var(--text2); white-space: nowrap; letter-spacing: 0; }
.page-when::before { content: "\00b7"; margin: 0 8px 0 2px; }
.header-center { flex: 1; max-width: 400px; margin-left: auto; position: relative; }
.header-right { display: flex; gap: 8px; align-items: center; }
/* Never wraps or shrinks: on a phone, a 2px step in the spacing scale was
   enough to fold "← Back" onto two lines. There the arrow stands alone. */
.back-btn { background: var(--surface); border: none; box-shadow: var(--ctl-ring); border-radius: 999px; padding: 6px 14px; cursor: pointer; color: var(--text2); font-size: 14px; font-family: inherit; display: none; align-items: center; gap: 6px; font-weight: 500; white-space: nowrap; flex-shrink: 0; }
@media (max-width: 560px) { .back-btn .back-word { display: none; } }
.back-btn:hover { color: var(--text); background: var(--surface-2); }

/* Global search */
.global-search { width: 100%; height: 40px; padding: 8px 16px 8px 40px; border: 1px solid transparent; border-radius: 999px; font-size: 14px; outline: none; background: var(--surface); box-shadow: var(--ctl-ring); color: var(--text); font-family: var(--font-ui); transition: border-color .15s, box-shadow .15s; }
[data-theme="dark"] .global-search { background: rgba(255,255,255,0.06); }
.global-search:hover { border-color: var(--border); }
.global-search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.global-search::placeholder { color: var(--text3); }
.search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text3); display: flex; pointer-events: none; }
.search-icon .ic { width: 15px; height: 15px; }

/* Notifications bell */
.bell-wrap { position: relative; }
/* Phone only: the search button and the bottom bar (see the phone block). */
.search-toggle, .tabbar { display: none; }
/* Round, on the band (look A v2): the bell, the phone's search. */
.search-toggle { background: var(--surface); border: none; box-shadow: var(--ctl-ring); border-radius: 50%; width: 40px; height: 40px; padding: 0; justify-content: center; cursor: pointer; color: var(--text); align-items: center; }
.search-toggle .ic { width: 18px; height: 18px; }
.bell-btn { background: var(--surface); border: none; box-shadow: var(--ctl-ring); border-radius: 50%; width: 40px; height: 40px; padding: 0; justify-content: center; cursor: pointer; color: var(--text); position: relative; transition: box-shadow .12s; display: flex; align-items: center; }
[data-theme="dark"] .bell-btn, [data-theme="dark"] .search-toggle { background: rgba(255,255,255,0.06); }
.bell-btn:hover { box-shadow: var(--ctl-ring), 0 6px 14px -8px rgba(11,30,58,0.35); }
.bell-btn .ic { width: 18px; height: 18px; }
.bell-badge { position: absolute; top: -4px; right: -4px; background: var(--red-dot); color: #fff; font-size: 11px; font-weight: 600; min-width: 18px; height: 18px; border-radius: 999px; display: flex; align-items: center; justify-content: center; padding: 0 4px; box-shadow: 0 0 0 2px var(--band-base); }
.bell-dropdown { position: absolute; top: calc(100% + 8px); right: 0; width: 384px; max-width: 90vw; max-height: 480px; overflow-y: auto; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--pop-shadow); z-index: 120; display: none; }
.bell-dropdown.show { display: block; }
.bell-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 1px solid var(--border-light); position: sticky; top: 0; background: var(--surface); z-index: 2; }
.bell-head b { font-size: 15px; font-weight: 650; }
.bell-empty { padding: 32px 16px; text-align: center; color: var(--text3); font-size: 14px; }
.bell-empty .ic { width: 22px; height: 22px; margin-bottom: 8px; display: block; margin-left: auto; margin-right: auto; }
.notif-item { padding: 12px 16px; border-bottom: 1px solid var(--border-light); display: flex; gap: 12px; align-items: flex-start; cursor: pointer; transition: background 0.1s; }
.notif-item:hover { background: var(--surface-2); }
.notif-item.unread { background: var(--accent-bg); }
.notif-icon { flex-shrink: 0; margin-top: 1px; color: var(--text2); width: 18px; height: 18px; }
.notif-icon .ic { width: 18px; height: 18px; }
.notif-item.unread .notif-icon { color: var(--accent-text); }
.notif-body { flex: 1; min-width: 0; }
.notif-title { font-size: 14px; font-weight: 600; }
.notif-detail { font-size: 13px; color: var(--text2); margin-top: 1px; }
.notif-time { font-size: 12px; color: var(--text3); margin-top: 4px; }

/* The account button and its menu (Phase 4, the app frame): who you are,
   Settings, dark mode, Sign out. Sized like the bell beside it. */
.account-wrap { position: relative; }
.account-btn { display: flex; align-items: center; gap: 8px; max-width: 260px; padding: 4px; background: none; border: none; border-radius: 999px; cursor: pointer; color: var(--text); font-family: inherit; font-size: 14px; font-weight: 600; transition: box-shadow .12s, background-color .12s; }
.account-initials { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent-fill), color-mix(in oklab, var(--accent-fill) 55%, #0b1e3a)); color: #ffffff; font-size: 13px; font-weight: 600; }
.account-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-caret { width: 16px; height: 16px; flex-shrink: 0; color: var(--text3); }
.account-menu { position: absolute; top: calc(100% + 8px); right: 0; width: 264px; max-width: 90vw; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--pop-shadow); z-index: 120; display: none; font-family: var(--font-ui); }
.account-menu.show { display: block; }
.account-head { padding: 10px 10px 12px; margin-bottom: 6px; border-bottom: 1px solid var(--border-light); }
.account-head-name { font-size: 14px; font-weight: 650; overflow-wrap: anywhere; }
.account-head-sub { font-size: 12px; color: var(--text3); margin: 2px 0 8px; overflow-wrap: anywhere; }
.account-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: none; border-radius: 10px; background: none; color: var(--text); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer; }
.account-item:hover, .account-item:focus-visible { background: var(--surface-2); }
.account-item .ic { width: 16px; height: 16px; color: var(--text2); }
/* A row's secondary actions behind "…" (RULES.md 4.60, D11): rowMenu(). */
details.row-menu { display: inline-block; }
/* Framed wherever it sits -- on Team it was a bare glyph, framed only on
   Users by .user-actions (checker, 2026-10-12). */
details.row-menu > summary { list-style: none; border: 1px solid var(--border); border-radius: 999px; }
details.row-menu > summary::-webkit-details-marker { display: none; }
/* Closed is gone, not merely unseen: a fixed list left in the layout read as
   running off a phone's edge. */
details.row-menu:not([open]) > .row-menu-list { display: none; }
.row-menu-list { position: fixed; z-index: 120; min-width: 200px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--pop-shadow); }
.row-menu-list hr { border: 0; border-top: 1px solid var(--border-light); margin: 4px 0; }
.row-menu-list .rm-danger, .row-menu-list .rm-danger .ic { color: var(--red); }

/* ---------- Buttons ---------- */
/* Round (look A v2: B's round buttons and chips, one shape everywhere). A
   press shrinks it to 97% and lets it spring back (motion 4: 120 ms down,
   160 ms back) -- never on a phone's long-press, never for reduced motion. */
.btn { padding: 8px 16px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--text); cursor: pointer; font-size: 14px; font-weight: 500; transition: background-color .12s, border-color .12s, color .12s, transform .16s var(--ease-out); display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; font-family: inherit; box-shadow: 0 1px 2px rgba(11,30,58,0.05); }
.btn:hover { background: var(--surface-2); border-color: var(--text3); }
.btn:active { transform: scale(0.97); transition-duration: .12s; }
.btn:disabled { opacity: 0.5; cursor: default; box-shadow: none; }
.btn .ic { width: 15px; height: 15px; }
/* ONE button set (Phase 4, test_buttons.py): .btn, .btn-sm, .btn-lg, and
   .btn-block for full width; .btn-icon for an icon alone (.btn-tiny inside a
   tree). No button sizes itself by hand. */
.btn-lg { padding: 12px 20px; font-size: 15px; }
.btn-block { width: 100%; justify-content: center; }
.btn-icon.btn-tiny { padding: 2px 4px; margin-top: 1px; min-width: 0; }
/* The brand's action colour (RULES.md 4.48: Lens Blue — "actions"). Through
   --accent-text, never --accent: on a forwarder's portal that is THEIR colour,
   darkened by the server until white text on it reads (app._brand_from_company).
   It was --navy, so every primary button ignored the brand entirely (open item
   17 since 2026-09-12). */
/* --accent-fill since look A v2: ours Lens Blue in light and a deeper blue
   in dark, both carrying white words; a forwarder's is still his darkened
   --accent-text (applyBranding sets both). */
.btn-primary { background: var(--accent-fill); border-color: var(--accent-fill); color: #ffffff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), 0 10px 22px -12px var(--accent-fill); }
.btn-primary:hover { opacity: 0.92; background: var(--accent-fill); border-color: var(--accent-fill); }
.btn-danger { background: var(--red); border-color: var(--red); color: #fff; }
.btn-danger:hover { opacity: 0.92; background: var(--red); border-color: var(--red); }
/* Destructive, but one step quieter than Stop: Archive (his "as you said",
   RULES.md 4.65 item 14, audit CL-29). Stop stays filled red (4.1). The
   text is red on the card's own surface, so it never goes white-on-white
   the way the bulk bar once made it (test_danger_buttons_visible.py). */
.btn-danger-outline { background: var(--surface); border-color: var(--red); color: var(--red); }
.btn-danger-outline:hover { background: var(--red-bg); border-color: var(--red); color: var(--red); }
.btn-ghost { background: none; border-color: transparent; box-shadow: none; }
.btn-sm { padding: 6px 10px; font-size: 13px; }
.btn-sm .ic { width: 14px; height: 14px; }
.btn-icon { padding: 8px; min-width: 32px; justify-content: center; background: none; border: none; box-shadow: none; }
.btn-icon:hover { background: var(--surface-2); border-color: transparent; }

.content { padding: 20px 28px 32px; overflow-y: auto; flex: 1; }

/* ---------- KPI Cards ---------- */
.kpi-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
/* Four across where the markup says so: .cols-4 stands earlier in this sheet
   and lost to the rule above, so the operator's Billing and a customer's
   page put their fourth card on a line of its own. */
.kpi-row.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.kpi-card { background: var(--surface); border: none; border-radius: 20px; padding: 20px 24px; box-shadow: var(--card-shadow); }
.kpi-header { font-size: 14px; font-weight: 500; color: var(--text2); margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
.kpi-header .ic { width: 16px; height: 16px; color: var(--text3); }
.kpi-value { font-family: var(--font-display); font-size: 34px; font-weight: 600; text-align: center; margin-bottom: 8px; line-height: 1; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.kpi-divider { height: 1px; background: var(--border-light); margin: 14px 0; }
.kpi-modes { display: flex; justify-content: center; gap: 16px; font-size: 13px; color: var(--text2); }
.kpi-modes span { display: flex; align-items: center; gap: 4px; }
.kpi-card.in-transit .kpi-value { color: var(--move-text); }
.kpi-card.delivered-card .kpi-value { color: var(--green); }
.kpi-card.pending .kpi-value { color: var(--neutral); }
.kpi-hint { text-align: center; font-size: 12px; color: var(--text3); margin-top: 4px; }
.kpi-mini { margin-top: 10px; font-size: 13px; color: var(--text2); }
.kpi-mini-item { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed var(--border-light); }
.kpi-mini-item:last-child { border-bottom: none; }
.kpi-mini-item span:first-child { font-family: var(--font-mono); font-size: 12px; color: var(--text2); }

/* ---------- Tabs ----------
   ONE tab style (Phase 4, test_tabs.py): <div class="tab-nav" role="tablist">
   of <button class="tab" role="tab">. Look A v2 (part 2): a round track of
   round tabs, the open one the side menu's soft pill with its edge -- one
   shape everywhere. Its corners are half its one-line height (22px): a pill on
   one line, a soft box -- not a big oval -- where it wraps (a phone, Reports
   on a tablet; task-096). .tab-count is a waiting number. */
.tab-nav { display: flex; gap: 4px; width: fit-content; max-width: 100%; padding: 4px; margin-bottom: 16px;
  border-radius: 22px; background: var(--surface); box-shadow: var(--ctl-ring); overflow-x: auto; scrollbar-width: none; }
.tab-nav .tab { display: inline-flex; align-items: center; min-height: 36px; padding: 6px 16px; margin: 0; border: none; border-radius: 999px;
  background: none; color: var(--text2); font-family: inherit; font-size: 14px; font-weight: 500; white-space: nowrap; flex-shrink: 0;
  cursor: pointer; transition: background-color .12s, color .12s, box-shadow .12s; }
.tab-nav .tab:hover { color: var(--text); background: var(--surface-2); }
.tab-nav .tab.active { color: var(--text); font-weight: 600; background: var(--accent-bg); box-shadow: inset 0 0 0 1px var(--accent-edge); }
/* A quiet neutral pill, as the menu's and the cards' counts: Lens Blue on its
   own tint read 4.44:1 (task-096). */
.tab-count { display: inline-block; margin-left: 6px; padding: 1px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; background: var(--neutral-bg); color: var(--text2); }
.tab.active .tab-count { background: var(--surface); }
/* Reports (RULES.md 4.65 item 23): what each report holds, a word coloured
   by what it means (red money, amber look soon), and the heading only the
   printed page shows. */
.report-lede { font-size: 13px; color: var(--text2); margin: 0 0 14px; max-width: 760px; line-height: 1.5; }
.report-tone.bad { color: var(--red); font-weight: 600; }
.report-tone.warn { color: var(--amber); font-weight: 600; }
.report-table .is-muted { color: var(--text3); }
.report-print-head { display: none; }
/* Four tabs with their counts are wider than the content beside the sidebar
   at a tablet's 800px; they wrap rather than hide the last (task-072). */
#reportTabs { flex-wrap: wrap; overflow-x: visible; }

/* ---------- Filter Bar ---------- */
.filter-left { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
/* ONE list toolbar (Phase 4, test_toolbar.py): .filter-left is what is shown
   -- the chips, then that list's own filter -- and .toolbar-actions is what
   you can do, in one order on every list, the main action last. When the
   row wraps, the actions keep to the right. */
.toolbar-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-left: auto; }
.toolbar-search { min-width: 160px; cursor: text; }
/* Two groups that only exist for the phone (test_phone.py): on a laptop they
   are invisible to the layout and the toolbar is exactly as before. */
.chip-row, .toolbar-secondary { display: contents; }
.toolbar-more { display: none; }
/* A chip: round, its count in a pill of its own at its end (look A v2). The
   chosen one is filled with the accent and white words -- a forwarder's own
   colour on his portal (--accent-fill), as his buttons. */
.chip { display: inline-flex; align-items: center; gap: 8px; min-height: 36px; padding: 4px 14px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--text2); cursor: pointer; font-size: 14px; font-weight: 500; font-family: inherit; white-space: nowrap;
  transition: background-color .12s, border-color .12s, color .12s, transform .16s var(--ease-out); }
.chip:hover { border-color: var(--text3); color: var(--text); }
.chip:active { transform: scale(0.97); transition-duration: .12s; }
.chip.active { background: var(--accent-fill); color: #ffffff; border-color: var(--accent-fill);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.2), 0 10px 20px -12px var(--accent-fill); }
.chip.delayed-chip.active { background: var(--red); border-color: var(--red); color: #fff; }
[data-theme="dark"] .chip.delayed-chip.active { color: #0b1e3a; }
.chip .ic { width: 15px; height: 15px; color: var(--text3); }
.chip.active .ic { color: inherit; }
/* How many boxes the chip holds (renderChipCounts): a quiet pill. */
.chip-count { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; margin-right: -8px; padding: 0 8px;
  border-radius: 999px; background: var(--neutral-bg); font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--text2); font-variant-numeric: tabular-nums; }
.chip-count:empty { display: none; }
.chip.active .chip-count { background: rgba(255,255,255,0.22); color: inherit; }
.chip.delayed-chip:not(.active) .chip-count { background: var(--red-bg); color: var(--red); }
/* A list's own select and search: round, ringed rather than bordered, as
   the bar's search and the Containers list's (look A v2). */
.date-select, .preset-select { min-height: 36px; padding: 6px 14px; border: 1px solid transparent; border-radius: 999px; box-shadow: var(--ctl-ring);
  font-size: 14px; background: var(--surface); color: var(--text); font-family: inherit; cursor: pointer; }
.date-select:focus, .preset-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
/* A search box in a toolbar, styled like the selects beside it. The ones on
   Custom Fields and Users were browser defaults — thin black border, tiny
   text — and read as unfinished (discovery A13). */
.search-field { min-height: 36px; padding: 6px 14px; border: 1px solid transparent; border-radius: 999px; box-shadow: var(--ctl-ring);
  font-size: 14px; background: var(--surface); color: var(--text); font-family: inherit; }
.search-field:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
input.preset-select, input.date-select { cursor: text; }

/* ---------- Table ---------- */
/* A table's card, as the home's: no border, 20px corners; its title 15px
   beside a quiet icon, as a card head. */
.table-wrap { background: var(--surface); border: none; border-radius: 20px; overflow: hidden; box-shadow: var(--card-shadow); }
.table-toolbar { display: flex; align-items: center; justify-content: space-between; min-height: 56px; padding: 10px 20px; border-bottom: 1px solid var(--border-light); gap: 10px; flex-wrap: wrap; }
.table-toolbar b { display: inline-flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; }
.table-toolbar b > .ic { width: 16px; height: 16px; color: var(--text3); }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { text-align: left; padding: 12px 16px; font-size: 12px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; background: var(--table-stripe); border-bottom: 1px solid var(--border); white-space: nowrap; cursor: pointer; user-select: none; }
th:hover { color: var(--text2); }
th .sort-ind { margin-left: 4px; font-size: 11px; }
th .sort-ind.active { color: var(--accent-text); }
/* The filter row (RULES.md 4.42): one pill per filter, then + Add filter.
   ⚠ NOT `.filter-bar` — that name belonged to the Dashboard's date bar (gone
   since 2026-10-18: nothing read it), and reusing it restyled a screen this
   feature never touched. Grep a class name before inventing one. */
.col-filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 10px 16px; border-bottom: 1px solid var(--border); }
.filter-pill { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-2); padding: 2px 2px 2px 12px; font-size: 13px; }
.filter-pill .fp-label { font-weight: 600; color: var(--text2); white-space: nowrap; margin-right: 2px; }
.filter-pill select, .filter-pill input { border: none; background: transparent; font: inherit; color: var(--text); padding: 4px 6px; min-width: 0; }
.filter-pill input[type="text"], .filter-pill input[type="number"] { width: 130px; }
.filter-pill select:focus, .filter-pill input:focus { outline: 2px solid var(--accent-ring); border-radius: 4px; }
.filter-pill-stale { opacity: 0.7; }
.fp-x { border: none; background: none; cursor: pointer; color: var(--text3); padding: 2px 8px; font-size: 16px; line-height: 1; border-radius: 999px; }
.fp-x:hover { color: var(--red); background: var(--surface); }
.filter-add { max-width: 170px; }
/* Saved views (RULES.md 4.43): name, count, ×. */
.views-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border); }
/* The filters and the saved views share one row (test_toolbar.py). */
.list-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
.list-filters > .col-filters, .list-filters > .views-row { padding: 0; border-bottom: 0; }
.list-filters > .views-row { margin-left: auto; }
.views-label { font-size: 12px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; margin-right: 2px; }
.view-chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 4px 4px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); font-size: 13px; color: var(--text); cursor: pointer; user-select: none; }
.view-chip:hover { border-color: var(--accent); }
.view-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.view-chip.active { background: var(--accent-bg); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
.view-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
.view-count { font-size: 12px; color: var(--text3); font-variant-numeric: tabular-nums; }
.view-chip.active .view-count { color: inherit; }
.view-dirty { color: var(--amber); font-size: 18px; line-height: 1; }
.view-x { border: none; background: none; cursor: pointer; color: var(--text3); font-size: 15px; line-height: 1; padding: 2px 8px; border-radius: 999px; }
.view-x:hover { color: var(--red); background: var(--surface-2); }
/* Import: values that are not one of a dropdown's options (RULES.md 4.40). */
.import-values { grid-column: 1 / -1; margin-top: 2px; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--amber) 28%, transparent); border-radius: var(--radius-sm); background: var(--amber-bg); }
.iv-head { font-size: 12px; color: var(--text2); margin-bottom: 6px; }
.iv-row { display: grid; grid-template-columns: 1fr 250px; gap: 10px; align-items: start; padding: 6px 0; border-top: 1px solid var(--border-light); }
.iv-row:first-of-type { border-top: none; }
.iv-value { display: flex; gap: 8px; align-items: baseline; min-width: 0; font-size: 13px; }
.iv-value span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.iv-rows { font-size: 11px; color: var(--text3); white-space: nowrap; }
.iv-note { font-size: 11px; margin-top: 4px; }
.iv-ok { color: var(--green); }
.iv-warn { color: var(--amber); }
@media (max-width: 640px) { .iv-row { grid-template-columns: 1fr; } }
td { padding: 12px 16px; border-bottom: 1px solid var(--border-light); vertical-align: middle; }
tbody tr { transition: background 0.08s; }
tbody tr:hover td { background: var(--surface-2); }
tr:last-child td { border-bottom: none; }
/* RULES.md 4.47: amber = the ship is late (stage 1); red = still at the port
   past its last free day, money running (stage 3). Red used to mean "past
   ETA", which painted every on-time box waiting at Aqaba as a problem. */
tr.late td, tr.late:hover td { background: var(--amber-bg); }
tr.overdue td, tr.overdue:hover td { background: var(--red-bg); }
tr.overdue td:first-child { box-shadow: inset 3px 0 0 var(--red); }
tr.selected td { background: var(--accent-bg); }
.checkbox { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }
/* The Containers list (and the Archive, the same table) stays dense
   (RULES.md 5.20, item 24 part 1): a row is its two lines -- the value and
   the note under it -- never three or four because a column was squeezed.
   Tighter cells; the box's carrier and kind, its last free day and when it
   was updated each kept on their line. Laptop only: a phone draws each row
   as a card (the phone block). */
@media (min-width: 769px) {
  table.container-cards th, table.container-cards td { padding-left: 8px; padding-right: 8px; }
  table.container-cards td { padding-top: 8px; padding-bottom: 8px; font-size: 13px; }
  table.container-cards th { font-size: 11px; letter-spacing: .08em; padding-top: 12px; padding-bottom: 12px; }
  table.container-cards th:first-child, table.container-cards td:first-child { padding-left: 16px; }
  table.container-cards td[data-col="container_number"] .cell-sub,
  table.container-cards td[data-col="status"] .cell-sub,
  table.container-cards td[data-col="last_free_day"] > div,
  table.container-cards td[data-col="updated"] > div,
  table.container-cards .owner-cell { white-space: nowrap; }
  table.container-cards td[data-col="vessel_name"] { font-size: 12px; font-weight: 500; letter-spacing: .02em; min-width: 120px; }
  table.container-cards .progress-cell { min-width: 96px; }
  /* A row past its free time: a lighter wash than a badge's red, so the
     row's own words stay calm (look A v2). */
  table.container-cards tr.overdue td, table.container-cards tr.overdue:hover td { background: color-mix(in srgb, var(--red-bg) 55%, var(--surface)); }
  table.container-cards tr.overdue td:first-child { box-shadow: none; }
}
/* The Containers list (and the Archive, the same table) in look A v2: the
   chips and the actions stand on the ground above the table -- the chips a
   row of their own, then the filters and saved views, with the actions at
   the right -- and the table alone in its card. Two wrappers, nothing moved:
   .list-head (the toolbar and the filters) and .list-card (the bulk bar, the
   table, the pages). A phone keeps its two short rows (DISCOVERY D3). */
#view-shipments > .table-wrap, #view-shipgroup > .table-wrap { background: none; border: none; box-shadow: none; border-radius: 0; overflow: visible; }
.list-card { background: var(--surface); border-radius: 20px; box-shadow: var(--card-shadow); overflow: hidden; }
@media (min-width: 769px) {
  .list-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 12px; margin-bottom: 14px; }
  .list-head > .table-toolbar { display: contents; }
  .list-head .filter-left { grid-column: 1 / -1; grid-row: 1; gap: 6px; }
  .list-head .list-filters { grid-column: 1; grid-row: 2; padding: 0; border-bottom: none; }
  .list-head .list-filters > .views-row { margin-left: 0; }
  .list-head .toolbar-actions { grid-column: 2; grid-row: 2; gap: 8px; }
  .list-head .toolbar-actions .btn, .list-head .preset-select { min-height: 36px; font-size: 14px; }
}
/* A tablet beside the menu has no room for the filters and five actions on
   one row: the filters take theirs, the actions the next. */
@media (min-width: 769px) and (max-width: 1100px) {
  .list-head .list-filters { grid-column: 1 / -1; }
  .list-head .toolbar-actions { grid-column: 1 / -1; grid-row: 3; }
}
.list-head .preset-select { border-radius: 999px; padding: 6px 14px; border: none; box-shadow: var(--ctl-ring); }
/* "Views" is a word beside its chips here, not an eyebrow (look A v2). */
.list-head .views-label { text-transform: none; letter-spacing: 0; font-size: 13px; font-weight: 500; margin: 0 2px 0 8px; }
.progress-pair { display: flex; align-items: center; gap: 8px; }
.progress-pair .progress-track { width: 48px; height: 6px; margin: 0; border-radius: 999px; }
.progress-pair .progress-fill { border-radius: 999px; }
.progress-pair .progress-label { font-family: var(--font-mono); }

/* Progress bar */
.progress-cell { min-width: 130px; }
.progress-track { height: 5px; background: var(--border-light); border-radius: 3px; overflow: hidden; margin-bottom: 4px; }
.progress-fill { height: 100%; border-radius: 3px; background: var(--move); }
.progress-fill.done { background: var(--green); }
/* Amber, like the late row it sits in (RULES.md 4.47): late is a warning, red
   is kept for money being lost. */
.progress-fill.late { background: var(--amber); }
.progress-label { font-size: 12px; color: var(--text2); font-variant-numeric: tabular-nums; }

/* Badges: a round pill with a dot of its own colour (look A v2, B's round
   shape -- one shape everywhere). Moving is --move, never --accent: a
   forwarder's colour never paints a status (RULES.md 5.20). */
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; min-height: 24px; border-radius: 999px; font-size: 12px; font-weight: 600; letter-spacing: 0.01em; white-space: nowrap; }
.badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.badge-delivered { background: var(--green-bg); color: var(--green); }
.badge-in_transit, .badge-arrived_port, .badge-discharged, .badge-inland_transit { background: var(--move-bg); color: var(--move-text); }
.badge-at_risk { background: var(--red-bg); color: var(--red); }
/* Waiting to sail, or not yet reported, is not a warning: grey, not amber
   (amber means "look soon" -- DISCOVERY E). */
.badge-unknown, .badge-booked { background: var(--neutral-bg); color: var(--neutral); }
/* How far the ETA moved from the first one we saw — "+13d" (RULES.md 4.65
   item 22). Later is amber (look soon); earlier is grey — news, not trouble. */
.eta-slip { display: inline-block; margin-left: 6px; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; vertical-align: middle; }
.eta-slip.later { background: var(--amber-bg); color: var(--amber); }
.eta-slip.earlier { background: var(--neutral-bg); color: var(--neutral); }
.badge .ic { width: 12px; height: 12px; }
.cell-link { color: var(--accent-text); cursor: pointer; font-weight: 550; font-family: var(--font-mono); font-size: 13px; }
.cell-link:hover { text-decoration: underline; }
/* Monospace is for container numbers and bills. A link made of WORDS
   ("Forgotten your password?", "Retrack", "Select all") in that face read as
   a broken stylesheet to every first-time reviewer (discovery A14). */
.cell-link.text-link { font-family: inherit; font-size: inherit; }

/* The "who does this belong to" cell. It used to be plain coloured text, so
   nothing said it could be changed — the cue is the point (RULES.md 4.39). */
.owner-cell { display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  color: var(--accent-text); border-radius: var(--radius-sm); padding: 2px 4px;
  margin: -2px -4px; }
.owner-cell:hover { background: var(--accent-bg); }
.owner-caret { width: 13px; height: 13px; opacity: .55; }
/* The customer picker (RULES.md 4.45): a small panel under the cell. */
.owner-pop { position: fixed; z-index: 200; width: 290px; max-width: calc(100vw - 16px);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--pop-shadow); padding: 8px; }
.op-head { font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text3); padding: 4px 6px 6px; }
.op-search { width: 100%; box-sizing: border-box; margin-bottom: 6px; padding: 8px 14px;
  border: 1px solid var(--border); border-radius: 999px; font: inherit; font-size: 13px;
  background: var(--surface); color: var(--text); }
.op-search:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.op-list { max-height: 300px; overflow-y: auto; }
.op-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 8px; border: none; background: none; border-radius: var(--radius-sm);
  cursor: pointer; font: inherit; color: var(--text); }
.op-item:hover, .op-item:focus-visible { background: var(--surface-2); outline: none; }
.op-item.current { background: var(--accent-bg); }
.op-name { display: block; font-size: 13px; font-weight: 600; }
.op-sub { display: block; font-size: 12px; color: var(--text3); margin-top: 1px; }
.op-check { margin-left: auto; color: var(--accent-text); font-weight: 700; }
.op-empty { font-size: 12px; color: var(--text2); padding: 8px 6px 4px; line-height: 1.5; }

/* Bulk bar */
.bulk-bar { display: none; align-items: center; gap: 10px; padding: 10px 16px; background: var(--accent-bg); border-bottom: 1px solid var(--border-light); font-size: 14px; }
.bulk-bar.show { display: flex; }
.bulk-bar .btn { background: var(--surface); }
/* The bar sits on a tinted background, so its buttons are lifted onto white.
   That rule is more specific than .btn-danger and was winning the BACKGROUND
   while .btn-danger kept winning the COLOUR -- white text on white, leaving
   Stop tracking and Archive as two empty red outlines. Destructive buttons
   keep all three properties here. */
.bulk-bar .btn-danger { background: var(--red); border-color: var(--red); color: #fff; }
.bulk-bar .btn-danger:hover { background: var(--red); border-color: var(--red); }

/* Pagination */
.pagination { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-top: 1px solid var(--border-light); font-size: 13px; color: var(--text2); flex-wrap: wrap; gap: 8px; }
.page-btns { display: flex; gap: 4px; }
.page-btns button { min-width: 32px; padding: 6px 12px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); cursor: pointer; font-size: 13px; color: var(--text); font-family: inherit; }
.page-btns button:disabled { opacity: 0.3; cursor: default; }
.page-btns button:hover:not(:disabled) { background: var(--surface-2); border-color: var(--text3); }
.per-page { display: flex; align-items: center; gap: 6px; }
.per-page select { padding: 4px 12px; border: 1px solid var(--border); border-radius: 999px; font-size: 13px; font-family: inherit; background: var(--surface); color: var(--text); }

/* ---------- Empty states ----------
   ONE way to say "nothing here" (Phase 4, test_empty_states.py):
   .empty-state  a screen or card with nothing in it -- always an icon and a
                 line, a title and the button that fixes it when there is one.
                 .compact inside a card.
   .empty-line   one quiet, centred line: an empty table, list or panel, and
                 "Loading…". .is-error when it reports a failure -- a screen
                 that could not load must not look like an empty one. */
.empty-state { text-align: center; padding: 56px 20px; color: var(--text3); }
.empty-state.compact { padding: 24px 20px; }
.empty-line { text-align: center; padding: 24px 16px; color: var(--text3); font-size: 14px; }
.empty-line.is-error { color: var(--red); }
.empty-icon { width: 42px; height: 42px; margin: 0 auto 14px; color: var(--text3); }
.empty-icon .ic { width: 42px; height: 42px; stroke-width: 1.4; }
.empty-title { font-size: 16px; font-weight: 650; color: var(--text2); margin-bottom: 6px; }
.empty-sub { font-size: 14px; margin-bottom: 18px; }
.empty-sub:last-child { margin-bottom: 0; }

/* ---------- Skeletons ---------- */
.skeleton { position: relative; overflow: hidden; background: var(--skeleton); border-radius: 4px; }
.skeleton::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); animation: shimmer 1.4s infinite; }
@keyframes shimmer { 100% { transform: translateX(100%); } }
.skel-row td { padding: 12px 14px; }
.skel-line { height: 12px; margin: 4px 0; }
.skel-kpi { height: 120px; border-radius: var(--radius); }

/* ---------- Detail Page ---------- */
.detail-layout { display: grid; grid-template-columns: 1fr 380px; gap: 20px; align-items: start; }
/* A card and its head as the home's (.home-card, .card-head): no border,
   20px corners, the title 15px beside a quiet icon. */
.detail-card { background: var(--surface); border: none; border-radius: 20px; box-shadow: var(--card-shadow); overflow: hidden; }
.detail-card .dc-head { min-height: 56px; padding: 12px 20px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
.detail-card .dc-head h3 { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
.detail-card .dc-head h3 .ic { width: 16px; height: 16px; color: var(--text3); }
.detail-card .dc-body { padding: 18px 20px; }
.detail-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
.detail-hero .dh-number { font-family: var(--font-mono); font-size: 20px; font-weight: 700; color: var(--text); letter-spacing: -0.01em; }
.detail-hero .dh-badges { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
.detail-hero .dh-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.dp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 16px; }
.dp-fl { font-size: 13px; color: var(--text3); font-weight: 500; }
/* A figure among the fields (what you pay, how many): Sora, as the home's
   numbers. */
.dp-fv.dp-num { font-family: var(--font-display); font-size: 24px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.dp-fv { font-size: 14px; margin-top: 4px; word-break: break-word; }

/* The journey stepper, the .journey-pct bar and the `pulse` keyframe all lived
   here and are GONE (2026-09-15). The stepper became .ev-table, and the
   progress bar moved into .route-strip, which is the only place that draws it
   now. Nothing references any of it -- checked by grep before deleting, not
   assumed -- so this note exists only so the next person does not go looking
   for a rule that was quietly dropped. It was not: it moved. */

/* Map */
.map-box { height: 300px; border-radius: 0; z-index: 0; }
/* A map in light mode shows its tiles at their own strength (his note on A
   v2: the light map not washed out). In dark mode the same tiles are turned
   to night by a filter -- no second set is fetched: every Esri tile is his
   money (CLAUDE.md, the map tiles). Lines and markers are not touched. */
[data-theme="dark"] .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.88) saturate(0.45); }
[data-theme="dark"] .leaflet-container { background: #0c1a2f; }

/* ---------- Modals ---------- */
.modal-overlay { position: fixed; inset: 0; background: rgba(10,13,20,0.5); backdrop-filter: blur(2px); z-index: 110; display: none; align-items: center; justify-content: center; padding: 20px; }
/* Look A v2: 20px corners, ringed, and it opens from 96% in 200 ms -- the
   action's own move, centred (a pop-up is not anchored to its button);
   none under reduced motion (the one block below). */
.modal { background: var(--surface); border-radius: 20px; width: 520px; max-width: 95vw; max-height: 90vh; overflow-y: auto;
  box-shadow: 0 0 0 1px var(--border), var(--pop-shadow); border: none; animation: modal-in .2s var(--ease-out); }
@keyframes modal-in { from { opacity: 0; transform: scale(.96); } }
/* Modals containing a table, not a form. Wide enough that the columns fit
   at a normal window size, so nothing has to be scrolled sideways to be
   read -- a scrollbar inside a dialog is where buttons go to hide. */
.modal.modal-wide { width: 1040px; max-width: 96vw; }
/* The way to a shipment typed in by hand, inside New shipment (RULES.md 4.57). */
.manual-offer { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; margin-top: 16px; padding: 12px 14px; border: 1px dashed var(--border);
  border-radius: var(--radius); font-size: 13px; color: var(--text2); }
/* The other sizes (Phase 4, test_dialogs.py): a pop-up's width is a class,
   never a style. */
.modal.modal-sm { width: 460px; }
.modal.modal-lg { width: 640px; }
/* The operator's Website page (RULES.md 5.20, 4.68): a reading width, not the
   screen's; its cards -- Contact sales, Sign-ups, Blog -- one under another. */
.website-page { max-width: 960px; display: flex; flex-direction: column; gap: 20px; }
.website-lede { font-size: 13px; color: var(--text2); margin: 0 0 14px; }
.contact-row { display: flex; gap: 12px; align-items: flex-start; padding: 14px 0; border-bottom: 1px solid var(--border-light); }
.contact-row:last-child { border-bottom: none; }
.contact-main { flex: 1; min-width: 0; }
.contact-who { font-size: 14px; overflow-wrap: anywhere; }
.contact-text { font-size: 13px; margin-top: 6px; white-space: pre-wrap; overflow-wrap: anywhere; }
.contact-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; max-width: 50%; }
.contact-actions a.btn { text-decoration: none; overflow-wrap: anywhere; }
.signup-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.signup-table th, .signup-table td { padding: 8px 20px; text-align: left; border-bottom: 1px solid var(--border-light); }
.signup-table td + td, .signup-table th + th { text-align: right; font-variant-numeric: tabular-nums; }
.signup-table .signup-total td { font-weight: 600; border-bottom: none; }
/* ---- The operator's Edit user window, grouped (RULES.md 5.20, his words
   "all of things are stuffed"): Who, Role and access, Price, Notes -- each a
   titled group with a hairline between, the person said once at the top. ---- */
.uf-section + .uf-section { border-top: 1px solid var(--border-light); padding-top: 20px; margin-top: 4px; }
.uf-section-title { font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--text);
  margin: 0 0 14px; }
.uf-who { display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-bottom: 18px;
  border-radius: 16px; background: var(--surface-2); }
.uf-avatar { width: 40px; height: 40px; flex-shrink: 0; border-radius: 50%; display: flex; align-items: center;
  justify-content: center; background: var(--accent-bg); color: var(--accent-text);
  font-family: var(--font-display); font-size: 14px; font-weight: 600; }
.uf-who-text { flex: 1; min-width: 0; }
.uf-who-name { font-size: 15px; font-weight: 600; overflow-wrap: anywhere; }
.uf-who-status { flex-shrink: 0; }
.uf-quiet { font-weight: 400; color: var(--text3); }
.uf-hint { font-size: 12px; line-height: 1.5; color: var(--text2); margin-top: 6px; }
/* A field said in words ("Signs in with their email", "Set a new password"),
   standing level with the fields beside it. */
.uf-line-label { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.uf-line-value { min-height: 38px; display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text2); }
.uf-limit { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 16px; align-items: center; }
.uf-limit-use .uf-hint { margin-top: 6px; }
.uf-limit-bar.is-near i { background: var(--amber); }
.uf-limit-bar.is-full i { background: var(--red); }
.uf-note { font-size: 13px; line-height: 1.5; color: var(--text2); background: var(--surface-2);
  border: 1px solid var(--border-light); border-radius: var(--radius); padding: 12px 14px; }
@media (max-width: 640px) { .uf-limit { grid-template-columns: minmax(0, 1fr); gap: 8px; } }
/* askConfirm / askText: the words under the title keep their paragraphs. */
.ask-body { font-size: 14px; color: var(--text2); line-height: 1.55; white-space: pre-line; }
.ask-body + .form-group { margin-top: 16px; }
/* Action buttons wrap onto a second line rather than forcing the whole
   table wider than the dialog. */
.cell-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
/* The operator's Users rows keep their buttons on one line on a laptop: that
   column is 1% wide, so a wrapping cell stacked them one per line on every
   row (reviewer, 2026-10-09). They wrap on a phone only (the 768px block). */
.user-actions { flex-wrap: nowrap; }
.modal-header { padding: 18px 24px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: var(--surface); z-index: 2; }
.modal-header h3 { font-family: var(--font-display); font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
.modal-body { padding: 24px; }
/* Pinned to the bottom of the window: a pop-up taller than the screen scrolls
   inside itself, and its buttons used to scroll away with it (discovery A9 —
   the Import window's Continue sat below a 900px laptop's edge). */
.modal-footer { padding: 16px 24px; border-top: 1px solid var(--border-light); display: flex; justify-content: flex-end; gap: 8px; background: var(--surface-2); border-radius: 0 0 20px 20px;
  position: sticky; bottom: 0; z-index: 2; }
/* The same for the Import window, whose buttons live inside each step. */
/* bottom: -22px rides over the body's own 22px padding; the extra padding
   keeps 12px of air under the buttons instead of pressing them to the edge. */
.import-actions { position: sticky; bottom: -24px; z-index: 2; background: var(--surface);
  padding: 12px 0 32px; margin-bottom: -24px; border-top: 1px solid var(--border-light); }
.form-group { margin-bottom: 16px; }
/* Written as it reads, the way the sign-in card has it (test_one_vocabulary.py). */
.form-group label { display: block; font-size: 13px; color: var(--text); margin-bottom: 6px; font-weight: 600; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border); font-size: 14px; font-family: inherit; outline: none; background: var(--surface); color: var(--text); transition: border-color 0.12s, box-shadow 0.12s; }
.form-group input:hover, .form-group select:hover, .form-group textarea:hover { border-color: var(--text3); }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.form-group textarea { resize: vertical; min-height: 60px; }
/* column-gap, not gap: stacked on a phone, a row gap would add to each
   field's own margin and the pair would sit further apart than the rest. */
.form-row { display: grid; grid-template-columns: 1fr 1fr; column-gap: 12px; }
/* One column on a phone: two fields side by side at 390px left the phone
   number a 12px sliver (discovery A11). */
@media (max-width: 640px) { .form-row { grid-template-columns: 1fr; } }
/* A phone number: a narrow code box (its option starts with the code, so a
   cut-off one still shows "+962") and a number box that takes the rest. */
.phone-pair { display: flex; gap: 8px; }
/* A field and a button beside it (the profile's email and its Change). A
   password's eye is not this: it sits inside its field (.pw-field, below). */
.field-pair { display: flex; gap: 8px; }
.field-pair input { flex: 1; min-width: 0; }
/* Wide enough for "+962 Jordan" whole -- at 7.5em it read "+962 Jorc" on
   sign-up, Settings and the invite (THIRD-EYE C5). A longer name is still
   cut, after its code, which is what matters. */
.phone-pair select { flex: 0 0 9.5em; width: 9.5em; min-width: 0; }
.phone-pair input { flex: 1 1 auto; min-width: 0; }

/* Drop zone */
.drop-zone { border: 1.5px dashed var(--border); border-radius: var(--radius); padding: 48px; text-align: center; cursor: pointer; transition: all 0.12s; background: var(--surface-2); }
.drop-zone:hover, .drop-zone.over { border-color: var(--accent); background: var(--accent-bg); }
.dz-icon { width: 38px; height: 38px; margin: 0 auto 12px; color: var(--text3); }
.dz-icon .ic { width: 38px; height: 38px; stroke-width: 1.4; }
.dz-text { font-size: 15px; color: var(--text2); font-weight: 550; }
.dz-hint { font-size: 13px; color: var(--text3); margin-top: 4px; }

/* Wizard steps */
.wizard-steps { display: flex; gap: 8px; margin-bottom: 24px; }
.wizard-step { flex: 1; text-align: center; font-size: 13px; color: var(--text3); font-weight: 600; position: relative; padding-top: 18px; }
.wizard-step::before { content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 12px; height: 12px; border-radius: 50%; background: var(--border); }
.wizard-step.active { color: var(--accent-text); }
.wizard-step.active::before { background: var(--accent); }
.wizard-step.done { color: var(--green); }
.wizard-step.done::before { background: var(--green); }
.wizard-line { position: absolute; top: 5px; left: calc(-50% + 18px); right: calc(50% + 18px); height: 2px; background: var(--border); }
.wizard-line.done { background: var(--green); }
.preview-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.preview-table th, .preview-table td { padding: 6px 10px; border: 1px solid var(--border-light); text-align: left; }
.preview-table th { background: var(--table-stripe); color: var(--text2); }

/* Toast */
.toast-wrap { position: fixed; bottom: 22px; right: 22px; z-index: 300; display: flex; flex-direction: column; gap: 8px; }
.toast { padding: 12px 18px; border-radius: 16px; font-size: 14px; font-weight: 500; box-shadow: var(--pop-shadow); animation: slideUp 0.25s ease; max-width: 360px; }
.toast-ok { background: var(--text); color: #fff; }
.toast-err { background: var(--red); color: #fff; }
@keyframes slideUp { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
@keyframes spin { to { transform:rotate(360deg); } }
.spinner { display: inline-block; width: 13px; height: 13px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.6s linear infinite; }
.spinner-dark { border-color: var(--border); border-top-color: var(--accent); }

/* ---------- Login ---------- */
.login-wrap { position: fixed; inset: 0; background: radial-gradient(1200px 600px at 50% -10%, rgba(22,104,227,0.08), transparent 60%), var(--bg); display: flex; z-index: 500; overflow-y: auto; }
/* Centred by auto margins, NOT align-items: center -- in a scrolling layer
   that puts the top of a card taller than the screen above it, where no
   scroll reaches: the sign-up's logo was cut off on a phone (THIRD-EYE C5).
   Auto margins centre when there is room and start at the top when not.
   (The sign-in page is a grid of its own: .login-split.) */
.login-wrap:not(.login-split) > * { margin: auto; }
.login-card { background: var(--surface); border: none; border-radius: 20px; padding: 40px; width: 392px; max-width: 92vw; box-shadow: var(--pop-shadow); }
.login-logo { text-align: center; margin-bottom: 32px; }
/* The logo on a signed-out screen: a link home (RULES.md 4.55) -- our
   website for our brand, his portal's sign-in page on a forwarder's. Never
   link-blue or underlined, and only as wide as the logo, not the whole card. */
[data-brand-home] { color: inherit; text-decoration: none; }
.login-logo .ll-home { display: block; width: fit-content; margin: 0 auto; }
.login-logo .ll-icon { width: 46px; height: 46px; margin: 0 auto 14px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), #0b1e3a); display: flex; align-items: center; justify-content: center; color: #fff; }
.login-logo .ll-icon .ic { width: 24px; height: 24px; }
.login-logo .ll-name { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
/* Ours is the lockup on ONE line, the mark beside the word (RULES.md 4.69);
   his logo, or the ship, stays above his name. */
.login-logo .ll-home:has(> .is-ours) { display: flex; align-items: center; gap: 14px; }
.login-logo .ll-icon.is-ours { width: 38px; height: 38px; margin: 0; border-radius: 0; }
.login-logo .ll-name .cl-word { font-size: 24px; }
.login-logo .ll-sub { font-size: 13px; color: var(--text3); margin-top: 4px; }

/* ---- Sign in, as he designed it (RULES.md 4.48): Deep Sea on the left with
   our logo (mark and word, 4.69) and the promise; the form on the right, on
   white. One column on a phone, the brand above the form. (The eye drawn
   large and faint behind went with the eye itself, 4.50.) ---- */
/* Look A v2 (S26b, RULES.md 5.20 "the sign-in page"): the form is a card of
   the app's own -- white (a step up in dark), 20px, the soft shadow -- on the
   app's shallow water, as every card inside is. */
.login-wrap.login-split { display: grid; grid-template-columns: minmax(0, 45fr) minmax(0, 55fr);
  align-items: stretch; justify-content: stretch; background: var(--bg); }
/* Ours is the deep sea of the app's dark band (look A v2): two soft glows on
   Deep Sea. A forwarder's panel is painted over it by applyBranding. */
.login-hero { position: relative; overflow: hidden; color: #ffffff;
  background: radial-gradient(900px 520px at 92% -12%, rgba(47,127,240,0.45), transparent 62%),
              radial-gradient(640px 420px at -10% 108%, rgba(79,184,255,0.16), transparent 70%), #0b1e3a;
  padding: 56px 64px; display: flex; flex-direction: column; justify-content: space-between; gap: 40px;
  /* Our logo in its dark colours, in either theme: the panel is always dark. */
  --logo-ink: #e7eef8; --logo-blue: #4fb8ff; }
.login-hero .lh-brand { display: flex; align-self: flex-start; align-items: center; gap: 12px; position: relative; z-index: 1; }
.login-hero .lh-brand:has(> .is-ours) { gap: 16px; }
.login-hero .lh-brand .ll-icon { width: 56px; height: 34px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 8px; color: #fff; }
.login-hero .lh-brand .ll-icon.is-ours { width: 44px; height: 44px; border-radius: 0; }
.login-hero .lh-brand .ll-name { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; }
.login-hero .lh-brand .ll-name .cl-word { font-size: 28px; }
.lh-copy { position: relative; z-index: 1; margin-bottom: 18vh; }
.lh-title { font-family: var(--font-display); font-weight: 600; font-size: clamp(36px, 4.4vw, 62px);
  line-height: 1.06; letter-spacing: -0.035em; max-width: 11ch; }
.lh-sub { margin-top: 20px; font-size: 18px; line-height: 1.55; color: #b8c6da; max-width: 30em; }
.login-pane { display: flex; align-items: center; justify-content: center; padding: 48px 24px; }
.login-split .login-card { border: none; box-shadow: var(--card-shadow-lg); padding: 40px; width: 440px;
  max-width: 100%; background: var(--surface); border-radius: 20px; }
.lp-title { font-family: var(--font-display); font-weight: 600; font-size: 34px; letter-spacing: -0.025em;
  color: var(--text); margin-bottom: 24px; }
.login-split .form-group label { font-size: 14px; font-weight: 500; }
.login-split .form-group input { padding: 12px 14px; font-size: 15px; border-radius: 12px; }
.lp-label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.lp-label-row label { margin-bottom: 0 !important; }
.lp-link { color: var(--accent-text); font-size: 14px; font-weight: 500; cursor: pointer; text-decoration: none; }
.lp-link:hover { text-decoration: underline; }
.lp-foot { text-align: center; margin-top: 24px; font-size: 14px; color: var(--text2); }
/* A white-label forwarder's website under his form (RULES.md 4.59); gone
   entirely when he has none -- `hidden` loses to a display rule otherwise. */
.lp-site { display: block; width: fit-content; margin: 12px auto 0; font-size: 13px; }
.lp-site[hidden] { display: none; }
/* The show-password eye, on every password field (RULES.md 4.60 D12; HISTORY
   Part 13 item 48). It sits INSIDE its field at the right edge, so every field
   of a form keeps one width, and the field's words stop short of it. Here,
   after the sign-in's own field padding, which it must beat. Centred by
   `translate`, not `transform`: a press scales a .btn (.btn:active), and a
   transform here would drop the eye out of the field. Its hover only darkens
   it -- a 44px circle on a phone would stand out of a shorter field. */
.pw-field { position: relative; }
.form-group .pw-field input { padding-right: 40px; }
.pw-field .pw-eye { position: absolute; top: 50%; right: 4px; translate: 0 -50%; color: var(--text2); }
.pw-field .pw-eye:hover { background: none; color: var(--text); }
/* Edge draws an eye of its own in a password field: two eyes in one. */
.pw-field input::-ms-reveal { display: none; }
/* The Branding preview's password box, with its eye (RULES.md 4.56). */
.sp .bp-pw { display: flex; align-items: center; justify-content: flex-end; padding-right: 12px; color: var(--text2); }
.sp .bp-pw .ic { width: 15px; height: 15px; }
/* A forwarder's own sign-in (RULES.md 4.56). His panel is painted by
   applyBranding (his photo under a shade, or his colour); "full" puts the
   photo behind the whole page and the form in a white card on it. The
   preview (.sp-full) draws the same thing from the same rules. */
.login-split.layout-full .login-hero, .sp-full .login-hero { background: transparent !important; }
.login-split.layout-full .login-pane, .sp-full .login-pane { background: transparent; }
.login-split.layout-full .login-card, .login-split.sp-full .login-card {
  background: var(--surface); padding: 40px; border-radius: 20px;
  box-shadow: 0 24px 64px rgba(4, 10, 22, .35); }
/* A long headline of his own steps down a size rather than running to four
   lines of the largest type on the page. */
.lh-title.is-long { font-size: clamp(28px, 3vw, 40px); max-width: 18ch; }
/* The miniature (brandPreviewCard) is the page at 1280 by 760, so what the
   page sizes by the WINDOW it sizes by that frame instead: cqw and cqh are a
   share of .bp-scale, which is 1280 by 760 on any screen. Without this a
   wide monitor drew a bigger headline than the page has. */
.sp .lh-copy { margin-bottom: 18cqh; }
.sp .lh-title { font-size: clamp(36px, 4.4cqw, 62px); }
.sp .lh-title.is-long { font-size: clamp(28px, 3cqw, 40px); }
@media (max-width: 860px) {
  /* minmax(0, …), never a bare 1fr: 1fr will not shrink below its widest
     content, and the 420px form made the phone screen slide sideways. */
  /* Only the real page (.login-wrap): the miniature in Settings is the
     page on a laptop, and on a phone it drew a laptop wearing phone type. */
  .login-wrap.login-split { grid-template-columns: minmax(0, 1fr); align-content: start; }
  .login-wrap .login-hero { padding: 24px 24px 28px; gap: 18px; }
  .login-wrap .lh-copy { margin-bottom: 0; }
  .login-wrap .lh-title { font-size: 28px; }
  .login-wrap .lh-sub { font-size: 15px; margin-top: 10px; }
  .login-wrap .login-hero .lh-brand:has(> .is-ours) { gap: 14px; }
  .login-wrap .login-hero .lh-brand .ll-icon.is-ours { width: 38px; height: 38px; }
  .login-wrap .login-hero .lh-brand .ll-name .cl-word { font-size: 24px; }
  .login-wrap .login-pane { padding: 24px 16px 40px; align-items: flex-start; }
  /* The card on a phone: the fields keep their width. */
  .login-wrap.login-split .login-card, .login-wrap.layout-full .login-card { padding: 28px 20px; }
}
.login-error { background: var(--red-bg); color: var(--red); font-size: 13px; padding: 10px 12px; border-radius: var(--radius-sm); margin-bottom: 14px; display: none; }
/* A notice, not an error ("your session has ended"): grey, not red. */
.login-note { background: var(--surface-2); color: var(--text2); font-size: 13px; padding: 10px 12px;
  border-radius: var(--radius-sm); margin-bottom: 14px; display: none; }
.login-note.show { display: block; }
/* ---- Two-step verification, the second step (RULES.md 5.20) ---- */
.lp-badge { width: 48px; height: 48px; border-radius: 16px; display: flex; align-items: center;
  justify-content: center; background: var(--accent-bg); color: var(--accent-text); margin-bottom: 20px; }
.lp-badge .ic { width: 24px; height: 24px; }
.lp-title.lp-title-step { font-size: 28px; margin-bottom: 8px; }
.lp-lede { font-size: 14px; line-height: 1.55; color: var(--text2); margin: 0 0 24px; }
/* One field under six boxes: the field takes every key, paste and autofill;
   the boxes only show what it holds (drawOtpCells). */
.otp { position: relative; }
.otp-cells { display: flex; gap: 8px; }
.otp-cell { flex: 1 1 0; min-width: 0; max-width: 56px; height: 56px; display: flex; align-items: center;
  justify-content: center; border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
  font-family: var(--font-display); font-size: 24px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--text); transition: border-color .12s, box-shadow .12s; }
.otp-gap { flex: 0 0 8px; }
.otp-cell.is-active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
/* Where the next digit goes: a caret in the empty box, as a field has --
   still, not blinking (look A v2: every move under 300 ms, none for ever). */
.otp-cell.is-active:not(.is-filled)::after { content: ''; width: 2px; height: 24px; border-radius: 1px;
  background: var(--accent-text); }
.otp.is-wrong .otp-cell { border-color: var(--red); }
.login-card .form-group .otp-input { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%;
  padding: 0; border: 0; border-radius: 12px; background: transparent; color: transparent;
  caret-color: transparent; font-size: 16px; box-shadow: none; cursor: text; }
.login-card .form-group .otp-input::selection { background: transparent; }
.lp-alt { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; margin-top: 18px; }
.lp-alt .lp-back { display: inline-flex; align-items: center; gap: 6px; color: var(--text2); }
.lp-back .ic { width: 16px; height: 16px; }
/* The change of step: the card's words rise in (180 ms, the page's own
   page-in; stilled under reduced motion by the one rule below). */
.login-card.lp-step-in > * { animation: page-in .18s var(--ease-out); }
/* The whole row is the label, so the text is a tap target too -- a 13px
   checkbox on its own is a poor thing to aim at on a phone. */
.remember-row { display: flex; align-items: center; gap: 10px; margin: 2px 0 16px;
                font-size: 13px; color: var(--text2); cursor: pointer; user-select: none; }
.remember-row input { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; margin: 0; flex-shrink: 0; }
.login-error.show { display: block; }

/* Role badge */
/* Round, written as it reads, as a status badge -- without the dot: a role
   is not a status. */
.role-badge { display: inline-flex; align-items: center; min-height: 24px; font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 999px; background: var(--accent-bg); color: var(--accent-text); white-space: nowrap; }
.role-badge.role-admin { background: var(--red-bg); color: var(--red); }
.role-badge.role-forwarder { background: var(--purple-bg); color: var(--purple); }
.role-badge.role-customer, .role-badge.role-subcustomer { background: var(--green-bg); color: var(--green); }

/* User table */
.user-actions { display: flex; gap: 6px; }
.user-actions .btn-icon { min-width: 30px; padding: 6px; border: 1px solid var(--border); border-radius: 999px; }
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.status-dot.on { background: var(--green); }
.status-dot.off { background: var(--text3); }

/* Invite link box */
.invite-box { display: flex; gap: 8px; align-items: center; }
.invite-box input { flex: 1; font-family: monospace; font-size: 13px; }

/* Customer cards (admin dashboard) */
.customer-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 14px; }
.customer-card { background: var(--surface); border: none; border-radius: 20px; padding: 20px; cursor: pointer;
  transition: transform .12s var(--ease-out), box-shadow .12s var(--ease-out); box-shadow: var(--card-shadow); }
@media (hover: hover) and (pointer: fine) {
  .customer-card:hover { transform: translateY(-2px); box-shadow: var(--card-shadow-lg); transition-duration: .16s; }
}
.customer-card .cc-name { font-size: 15px; font-weight: 650; display: flex; align-items: center; gap: 8px; letter-spacing: -0.01em; }
.customer-card .cc-username { font-size: 12px; color: var(--text3); margin-top: 2px; }
.customer-card .cc-stats { display: flex; gap: 16px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border-light); }
.customer-card .cc-stat { flex: 1; }
.customer-card .cc-stat-value { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.customer-card .cc-stat-label { font-size: 12px; color: var(--text3); }
.cc-revenue { color: var(--green); }
.cc-containers { color: var(--accent-text); }

/* Section heading (admin dashboard etc.) */
.section-head { display: flex; justify-content: space-between; align-items: center; margin: 20px 0 12px; }
.section-head h2 { font-size: 16px; font-weight: 650; letter-spacing: -0.01em; }

/* Settings */
.settings-layout { display: grid; grid-template-columns: 224px 1fr; gap: 24px; align-items: start; }
.settings-nav { display: flex; flex-direction: column; gap: 18px; position: sticky; top: 72px; }
/* One group of related settings: a quiet heading, then its entries. */
.snav-group { display: flex; flex-direction: column; gap: 2px; }
.snav-head { font-size: 11px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text3); padding: 0 12px 6px; }
/* The side menu's own shape (look A v2): round rows, the open one a soft pill
   with his colour's edge -- the edge and the weight say which one is open,
   not a tint alone. */
.snav-item { display: flex; align-items: center; gap: 10px; min-height: 40px; padding: 8px 12px;
  border-radius: 999px; border: none; background: none; color: var(--text2);
  font-size: 14px; font-weight: 500; cursor: pointer; text-align: left;
  font-family: inherit; width: 100%; position: relative;
  transition: background-color .12s, color .12s, box-shadow .12s; }
.snav-item:hover { background: var(--surface); color: var(--text); }
.snav-item.active { background: var(--surface); color: var(--text); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--accent-edge), 0 8px 18px -12px var(--accent); }
[data-theme="dark"] .snav-item.active { background: var(--accent-bg); }
.snav-item.active .ic { color: var(--accent-text); }
.snav-item .ic { width: 16px; height: 16px; flex: none; }
.settings-body { min-width: 0; }

/* Below the two-column breakpoint the nav stops being a sidebar and becomes a
   scrolling strip, because a 224px column and a form do not both fit on a
   phone. The group headings come along so the order still makes sense.
   1024, not 760 (audit CL-33): above 768 the app's own sidebar (232px) is
   still there too, so at 780 the panel was squeezed to ~225px between the
   two. As a strip from 1024 down, the panel keeps ~490px at its narrowest. */
@media (max-width: 1024px) {
  .settings-layout { grid-template-columns: 1fr; gap: 14px; }
  /* WRAPPED, not scrolled sideways: a scrolling strip at 800px showed four
     tabs and hid the rest past the edge with no cue (checker, task-059). */
  .settings-nav { position: static; flex-direction: row; gap: 6px 14px;
    flex-wrap: wrap; padding-bottom: 6px; }
  .snav-group { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 4px; flex: none;
    max-width: 100%; }
  .snav-head { padding: 0 6px 0 0; white-space: nowrap; }
  .snav-item { width: auto; white-space: nowrap; }
}
/* ---- The Blog, on the operator's Website page (RULES.md 4.64 item 12a, 5.20): his posts on
   sailping.com. .blog-preview shows the article the SERVER made
   (site_pages.article_html) in the app's own type; its links do nothing here,
   so a click cannot leave a post unsaved. ---- */
.blog-row { display: flex; gap: 12px; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid var(--border-light); }
.blog-row:last-child { border-bottom: none; }
.blog-row a.btn { text-decoration: none; }
.blog-address { display: flex; align-items: center; gap: 4px; color: var(--text2); }
.blog-address input { flex: 1; min-width: 0; }
.blog-body { font-family: var(--font-mono); line-height: 1.6; }
.blog-help { font-size: 13px; color: var(--text2); margin-top: 8px; }
.blog-help summary { cursor: pointer; font-weight: 600; }
.blog-help dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin-top: 8px; }
.blog-help dt { font-family: var(--font-mono); color: var(--text); }
.blog-warn { padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--amber); border-radius: var(--radius); background: var(--amber-bg); font-size: 13px; color: var(--text); }
.blog-delete { margin-right: auto; }
.blog-preview { padding: 4px 4px 8px; color: var(--text); font-size: 16px; line-height: 1.6; overflow-wrap: anywhere; }
.blog-preview a { pointer-events: none; color: var(--accent-text); }
.blog-preview .back { font-size: 14px; text-decoration: none; }
.blog-preview h1 { font-family: var(--font-display); font-size: 28px; line-height: 1.2; margin: 12px 0 8px; }
.blog-preview h2 { font-family: var(--font-display); font-size: 20px; margin: 28px 0 8px; }
.blog-preview h3 { font-family: var(--font-display); font-size: 16px; margin: 20px 0 6px; }
.blog-preview p { margin-top: 12px; }
.blog-preview .lede { font-size: 18px; color: var(--text2); }
.blog-preview ul, .blog-preview ol { margin: 12px 0 0 24px; display: grid; gap: 6px; }
.blog-preview .mono { font-family: var(--font-mono); font-size: 14px; padding: 1px 4px; background: var(--surface-2); border: 1px solid var(--border-light); border-radius: var(--radius-xs); }
.blog-preview table { border-collapse: collapse; margin-top: 16px; width: 100%; font-size: 14px; }
.blog-preview th, .blog-preview td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.blog-preview th { background: var(--surface-2); font-weight: 600; }
.blog-preview .note { margin-top: 24px; padding: 16px 20px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); }
.blog-preview .note p { margin-top: 0; }
.blog-preview .note p + p { margin-top: 12px; }
.blog-preview .cta-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
.pref-row { display: flex; align-items: center; gap: 20px; padding: 14px 0; border-bottom: 1px solid var(--border-light); }
.pref-row:last-child { border-bottom: none; }
.pref-label { flex: 1; min-width: 0; }
.pref-label b { font-size: 14px; }
.pref-desc { font-size: 12px; color: var(--text3); margin-top: 1px; }
.pref-toggle { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text2); cursor: pointer; white-space: nowrap; }
.pref-toggle input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
.colpick-row { display: flex; align-items: center; gap: 12px; padding: 8px 4px; border-bottom: 1px solid var(--border-light); }
.colpick-row:last-child { border-bottom: none; }
.colpick-label { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; font-size: 14px; cursor: pointer; }
.colpick-label input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
.colpick-arrows { display: flex; gap: 4px; }
.colpick-arrows .btn-icon { min-width: 26px; padding: 4px; border: 1px solid var(--border); }
.colpick-section { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 650; color: var(--text3); margin: 10px 0 4px; }
.colpick-hint { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; border-radius: 50%; border: 1px solid var(--border); color: var(--text3); font-size: 11px; font-weight: 700; cursor: help; flex: none; }

/* Custom fields on the detail page */
.cf-panel { margin-top: 14px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.cf-panel-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--table-stripe); font-size: 13px; font-weight: 600; cursor: pointer; user-select: none; }
.cf-caret { display: inline-block; transition: transform .12s ease; color: var(--text3); font-size: 15px; line-height: 1; }
.cf-caret.open { transform: rotate(90deg); }
.cf-count { margin-left: auto; font-size: 12px; font-weight: 500; color: var(--text3); }
.cf-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 12px; }
.cf-chip { display: inline-flex; align-items: baseline; gap: 6px; max-width: 100%; padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--card); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cf-chip b { font-weight: 600; color: var(--text2); }
.cf-more { font-size: 12px; color: var(--accent-text); cursor: pointer; align-self: center; }
.cf-more:hover { text-decoration: underline; }
.cf-rows { padding: 4px 12px 10px; }
.cf-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; }
.cf-row:last-child { border-bottom: none; }
.cf-row.editing { align-items: flex-start; }
.cf-row-label { flex: 0 0 42%; color: var(--text2); }
.cf-row-value { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.cf-row-value.can-edit { cursor: text; border-radius: 4px; padding: 2px 6px; margin: -2px -6px; }
.cf-row-value.can-edit:hover { background: var(--table-stripe); }
.cf-row-edit { flex: 1; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.cf-row-edit input, .cf-row-edit select, .cf-row-edit textarea { flex: 1; min-width: 140px; }
.cf-add { color: var(--accent-text); font-size: 12px; }
.cf-tag { display: inline-block; padding: 2px 8px; border: 1px solid var(--border); border-radius: 999px; font-size: 11px; color: var(--text2); white-space: nowrap; }
/* Tags (RULES.md 4.63 item 2): a box's own words, one chip each -- on its
   page with a × and a box to type the next one, in the list's Tags column,
   and in Settings -> Tags. Grey: a tag means nothing to the product. */
.tags-line { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 10px; }
.tags-line .tags-label { font-size: 12px; font-weight: 600; color: var(--text3); margin-right: 2px; }
.tag-chip { display: inline-flex; align-items: center; gap: 2px; max-width: 240px; padding: 2px 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-2); font-size: 12px; color: var(--text2); }
.tag-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tag-chip.has-x { padding-right: 2px; }
.tag-x { border: none; background: none; cursor: pointer; color: var(--text3); padding: 0 6px; font-size: 14px; line-height: 1; border-radius: 999px; }
.tag-x:hover { color: var(--red); background: var(--surface); }
.tag-input { width: 140px; padding: 4px 10px; font-size: 12px; border: 1px dashed var(--border); border-radius: 999px; background: transparent; color: var(--text); }
.tag-input:focus { border-style: solid; }
.tag-cell { display: flex; flex-wrap: wrap; gap: 4px; min-width: 160px; max-width: 260px; }
.tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; }
.tag-row:last-child { border-bottom: none; }
.tag-row .tag-count { color: var(--text2); margin-right: auto; }
.cf-opt-del { flex: none; width: 26px; height: 26px; border: none; background: none; color: var(--red); font-size: 24px; line-height: 1; cursor: pointer; border-radius: 5px; }
.cf-opt-del:hover { background: rgba(220,38,38,0.12); }
.cf-checks { display: flex; flex-wrap: wrap; gap: 4px 14px; padding: 6px 0; }
.cf-check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 400; cursor: pointer; }
.cf-check input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
/* Inside a form, ".form-group label" and "input" (a block of bold text, a
   padded box) beat the two rules above -- so a row of choices stacked one
   per line in bold, and a radio drew as a text box (RULES.md 4.56). */
.form-group .cf-check { display: inline-flex; margin: 0; font-weight: 400; }
.form-group .cf-check input { padding: 0; }

/* A custom column keeps a readable width and the TABLE scrolls sideways,
   rather than every column squeezing narrower each time one is added. Past
   about a dozen columns the squeeze makes the whole table unreadable at once;
   scrolling only makes you move. */
td.cf-cell, th.cf-head { min-width: 130px; max-width: 260px; }
td.cf-cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Storage meter on the Data tab */
.shelf-quota { margin-bottom: 14px; }
.shelf-bar { height: 6px; background: var(--table-stripe); border-radius: 999px; overflow: hidden; }
.shelf-fill { height: 100%; background: var(--accent); border-radius: 999px; transition: width .2s ease; }
.shelf-fill.full { background: var(--red); }
.shelf-quota-text { font-size: 12px; color: var(--text3); margin-top: 6px; }

/* Shipments: a bill opens to show what is travelling on it */
.ship-row { cursor: pointer; }
.ship-row:hover { background: var(--table-stripe); }
.ship-kids > td { background: var(--table-stripe); padding: 0 12px 10px 48px; }
.subtable { width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; }
.subtable th { background: transparent; font-size: 11px; padding: 8px 12px; cursor: default; }
.subtable td { padding: 8px 12px; font-size: 13px; }
.subtable tr:last-child td { border-bottom: none; }
.cf-row-label.wide { flex: 1 1 100%; margin-bottom: 6px; font-weight: 600; color: var(--text); }
.cf-row.editing { flex-wrap: wrap; }
.cf-opt-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.cf-opt-row input { flex: 1; min-width: 0; }
.cf-opt-row input[readonly] { background: var(--table-stripe); color: var(--text3); font-family: monospace; font-size: 12px; }
.cf-locked-note { font-size: 11px; color: var(--text3); margin-top: 4px; }
@media (max-width: 768px) { .settings-layout { grid-template-columns: 1fr; } .settings-nav { position: static; flex-direction: row; flex-wrap: wrap; } }

.hidden { display: none !important; }
/* Where keyboard focus is (Phase 7, WCAG 2.4.7; test_accessibility.py). One
   ring for everything that can be tabbed to and has no focus style of its
   own; :focus-visible, so a mouse click shows nothing. Fields keep their
   own ring (a border and a glow), set where they are styled. */
:where(button, a, [role="button"], [tabindex], summary, input[type="checkbox"], input[type="radio"]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; }
/* Motion 5 (look A v2): arriving on a page, its work area fades in and rises
   4px (180 ms); the menu and the top bar never move. Only on arriving: a
   live update redraws inside the page and never replays it. */
.content > [id^="view-"]:not(.hidden) { animation: page-in .18s var(--ease-out); }
@keyframes page-in { from { opacity: 0; transform: translateY(4px); } }
/* 🔴 Reduced motion (his "none for reduced motion"): when the computer asks,
   nothing moves -- the numbers, the dots, the menu, the pages and the lists
   simply appear where they end; no lift, no press. A spinner still turns
   once, so "loading" still reads. (countUp asks the same question.) */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    animation-delay: 0s !important; transition-duration: 0.01ms !important; transition-delay: 0s !important;
    scroll-behavior: auto !important; }
  .btn:active, .chip:active, button.stat-card:active, .today-card:hover, .week-day.can-open:hover,
  .stat-card[onclick]:hover, .customer-card:hover { transform: none; }
  .stage-row:hover { transform: translateX(-50%); }
}
/* Detail mode — full width, no sidebar */
body.detail-mode .sidebar { display: none; }
body.detail-mode .main { margin-left: 0; }
body.detail-mode .content { max-width: 1400px; margin: 0 auto; width: 100%; }
@media (max-width: 900px) {
  .detail-layout { grid-template-columns: 1fr; }
  .kpi-row, .kpi-row.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* The menu button and the backdrop exist only on small screens. */
.nav-toggle { display: none; background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
              padding: 6px 8px; cursor: pointer; color: var(--text2); align-items: center; }
.nav-toggle .ic { width: 19px; height: 19px; }
.nav-backdrop { display: none; }

/* ---------- Phones ----------
   This used to squeeze the sidebar to a 58px icon rail and hide the labels with
   `.nav-item span:not(.nav-icon)`. That selector never matched anything --
   renderNav writes the label as a bare text node, not a span -- so every label
   stayed and was clipped to its first letter, which is how the nav ended up
   reading "D S C N D U S".

   A rail is the wrong answer anyway: a third of a 375px screen for navigation
   nobody is looking at. The sidebar becomes a drawer instead: off screen until
   the menu button is pressed, and dismissed by the backdrop or by choosing
   anything inside it. */
@media (max-width: 768px) {
  /* No menu button: More in the bottom bar opens the same drawer. */

  .sidebar { width: 264px; transform: translateX(-100%); transition: transform 0.22s ease; }
  .sidebar.open { transform: translateX(0); box-shadow: 0 0 40px rgba(0,0,0,0.28); }
  /* A detail page hides the sidebar (it is full width on a laptop) -- which
     on a phone hid the DRAWER, so the menu on a container page opened
     nothing. The drawer is off screen until opened anyway. */
  body.detail-mode .sidebar { display: flex; }

  /* His Website page's messages: the words first, the two buttons under. */
  .contact-row { flex-direction: column; }
  .contact-actions { max-width: none; justify-content: flex-start; }

  /* The bottom bar. Above the page, under the drawer and every pop-up. A
     floating round bar in look A v2, clear of the phone's edges. */
  .tabbar { display: flex; position: fixed; left: 12px; right: 12px; bottom: calc(8px + env(safe-area-inset-bottom)); z-index: 40;
            background: var(--surface); border-radius: 999px; padding: 4px 8px;
            box-shadow: 0 0 0 1px var(--border), 0 18px 40px -14px rgba(11,30,58,0.4); }
  .tab-item { position: relative; flex: 1; min-height: 52px; display: flex; flex-direction: column; align-items: center; justify-content: center;
              gap: 2px; border: none; background: none; color: var(--text3); font-family: inherit; font-size: 11px;
              font-weight: 600; cursor: pointer; border-radius: 999px; }
  .tab-item .ic { width: 20px; height: 20px; }
  .tab-item.active { color: var(--accent-text); background: var(--accent-bg); }
  .tab-item .nav-count { position: absolute; top: 2px; left: calc(50% + 6px); min-width: 16px; height: 16px; padding: 0 4px; font-size: 11px; }
  .tab-item .nav-count:not(.is-bad) { display: none; }
  /* The menu is a drawer here: no fold; the account button sits in the
     header (placeAccount), its menu opening downwards. */
  .nav-fold { display: none; }
  .page-when { display: none; }
  .header .account-text { display: none; }
  .header .account-btn { padding: 2px; }
  /* The home on a phone: the numbers two by two, the week a row that
     scrolls sideways, the journey line its four stops without the dots. */
  .home-headline { padding-bottom: 16px; }
  .home-headline p { font-size: 20px; line-height: 1.3; }
  .today-row { gap: 10px; }
  .today-card { padding: 14px; border-radius: 18px; }
  .today-card .stat-label { font-size: 13px; gap: 8px; padding-right: 0; }
  .today-card .stat-value { font-size: 28px; margin: 12px 0 6px; }
  .today-card .stat-note { font-size: 12px; }
  .today-card .stat-go { display: none; }
  .week-days { padding: 12px; }
  .week-day { width: 120px; min-height: 96px; padding: 10px 12px 12px; }
  .card-head { padding: 12px 16px; }
  .att-row { margin: 0 16px; }
  .journey-line { top: 40px; }
  .stage-ring { width: 48px; height: 48px; }
  .stage-core { width: 36px; height: 36px; }
  .stage-count { font-size: 20px; margin-top: 8px; }
  .stage-name { font-size: 12px; }
  .stage-note, #weekMeta { display: none; }
  /* Toasts above the bar. (Room for the bar under each page: .content below.) */
  .toast-wrap { left: 12px; right: 12px; bottom: calc(76px + env(safe-area-inset-bottom)); }

  /* Search: a magnifier in the header opens the box as a row under it. */
  .search-toggle { display: inline-flex; }
  body.search-open .header-center { display: block; position: absolute; left: 0; right: 0; top: 100%;
                                    max-width: none; padding: 8px 12px; background: var(--header-bg);
                                    border-bottom: 1px solid var(--border); }
  body.search-open .header-center .search-icon { left: 24px; }

  .nav-backdrop { display: block; position: fixed; inset: 0; background: rgba(10,13,20,0.45);
                  z-index: 45; opacity: 0; pointer-events: none; transition: opacity 0.22s; }
  .nav-backdrop.show { opacity: 1; pointer-events: auto; }

  .main { margin-left: 0; }

  /* The header carried a search box, a company name and three buttons across
     375px. The name and the theme word go; everything else gets tighter. */
  .header { padding: 10px 12px; gap: 8px; }
  .header-left { font-size: 15px; gap: 8px; min-width: 0; flex: 1; }
  #pageTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .header-center { display: none; }
  /* header-right was taking 270px of a 375px screen, which left 73px for the
     page title and rendered "Dashboard" as "D...". On a phone the account
     button is its initials alone; the name, role, theme and Sign out are all
     in its menu. (The old `.header-right .btn { font-size: 0 }` here, meant
     for the Sign out button, also blanked "Mark all read" in the bell.) */
  .header-right { gap: 6px; flex-shrink: 0; }
  .account-name, .account-caret { display: none; }
  .account-btn { padding: 4px; }

  .content { padding: 16px 14px calc(80px + env(safe-area-inset-bottom)); }   /* the bottom bar's room */
  /* A banner is its title and one line; a tap shows the rest (DISCOVERY D5:
     three of them took 63% of the operator's first screen). data-open, not a
     class: every banner rewrites its className when it refreshes. */
  .provider-banner { padding: 10px 12px; gap: 10px; cursor: pointer; flex-wrap: wrap; }
  .provider-banner .pb-detail { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
  .provider-banner[data-open] .pb-detail { display: block; }
  /* The text takes the room beside the icon; a button wraps under the text. */
  .provider-banner .pb-body { flex: 1 1 0; }
  .provider-banner > .btn { margin-left: 28px; }
  /* A list toolbar is two short rows, not six (DISCOVERY D3): the chips in one
     row that scrolls sideways, then the list's own filter, a ... menu holding
     Export, Columns, Import and Refresh, and the main action. */
  .table-toolbar > .filter-left { display: contents; }
  /* A list's own search box takes its row: at its laptop width its words were
     cut ("Search name, username or er", THIRD-EYE C4). */
  .table-toolbar .search-field { flex: 1 0 100%; }
  /* Tabs wrap rather than run off the edge -- "Branding" was cut to "Bra"
     with nothing to say there was more (C4). */
  .tab-nav { flex-wrap: wrap; overflow-x: visible; }
  .user-actions { flex-wrap: wrap; }
  .chip-row { display: flex; flex: 1 0 100%; gap: 6px; flex-wrap: nowrap; overflow-x: auto;
              scrollbar-width: none; margin: 0 -4px; padding: 0 4px; }
  .chip-row .chip { flex-shrink: 0; }
  .toolbar-search { flex: 1 1 0; min-width: 0; }   /* 0, not auto: it shares the row with the buttons */
  .toolbar-actions { position: relative; flex-wrap: nowrap; }
  .toolbar-more { display: inline-flex; font-size: 18px; line-height: 1; }
  .toolbar-secondary { display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
                       flex-direction: column; align-items: stretch; gap: 2px; min-width: 200px; padding: 6px;
                       background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
                       box-shadow: var(--pop-shadow); }
  .toolbar-actions.open .toolbar-secondary { display: flex; }
  .toolbar-secondary .btn { justify-content: flex-start; border-color: transparent; box-shadow: none; }
  /* The filters and the saved views share a row; the "Views" word gives way
     so "+ Add filter" and "+ Save view" fit side by side. */
  .list-filters .views-label { display: none; }
  /* ---- Lists as cards (DISCOVERY D1; the phone reviewer's design) ----
     The same cells, placed by name (data-col) -- never a second copy of a
     row. What every card shares first, then each list's own layout. Columns
     that do not fit a card (vessel, updated, progress, his own fields) stay on
     the laptop and on the box's or bill's page. */
  table.cards-on-phone, table.cards-on-phone tbody { display: block; }
  table.cards-on-phone thead { display: none; }
  table.cards-on-phone tr { display: grid; position: relative; align-items: start;
    grid-template-columns: auto minmax(0, 1fr) auto; gap: 4px 10px; padding: 12px 14px;
    border-bottom: 1px solid var(--border-light); }
  table.cards-on-phone td { display: block; padding: 0; border: none; background: none; min-width: 0; }
  table.cards-on-phone td[data-col="status"] { grid-area: status; text-align: right; }
  table.cards-on-phone td[data-col="route"] { grid-area: route; }
  table.cards-on-phone td[data-col="customer"] { grid-area: cust; font-size: 13px; }
  table.cards-on-phone td[data-col="eta"]::before { content: "ETA "; color: var(--text3); }
  table.cards-on-phone td[data-col^="cfid"], table.cards-on-phone td.is-empty { display: none; }
  /* The tone is the card's, not each cell's. */
  table.cards-on-phone tr.overdue { background: var(--red-bg); box-shadow: inset 3px 0 0 var(--red); }
  table.cards-on-phone tr.late { background: var(--amber-bg); }
  table.cards-on-phone tr.overdue td:first-child { box-shadow: none; }
  table.cards-on-phone tr.selected { background: var(--accent-bg); }
  /* A phone keeps :hover after a tap; the laptop's row hover would paint a
     grey block behind each cell of the card. */
  table.cards-on-phone tr:hover td { background: none; }

  /* Containers: number and status; the route and whose it is; then ETA -- or,
     once it is at the port, the last free day. The whole card opens the box;
     the tick-box, Retrack and the customer picker keep their own taps. */
  table.container-cards tr { grid-template-areas: "check num status" "check route route" "check cust cust" "check date date"; }
  table.container-cards td.cell-check { grid-area: check; padding-top: 2px; }
  table.container-cards td.cell-check .checkbox { width: 20px; height: 20px; }
  table.container-cards td[data-col="container_number"] { grid-area: num; }
  table.container-cards td[data-col="eta"], table.container-cards td[data-col="last_free_day"] { grid-area: date; font-size: 13px; }
  table.container-cards td[data-col="last_free_day"] .cell-main::before { content: "Last free day "; color: var(--text3); }
  table.container-cards td[data-col="last_free_day"] > div { display: inline; }
  table.container-cards td[data-col="last_free_day"] .cell-sub::before { content: " · "; }
  table.container-cards tr[data-stage="at_port"] td[data-col="eta"],
  table.container-cards tr:not([data-stage="at_port"]) td[data-col="last_free_day"],
  table.container-cards td[data-col="vessel_name"], table.container-cards td[data-col="updated"],
  table.container-cards td[data-col="progress"], table.container-cards td[data-col="carrier"],
  table.container-cards td[data-col="tags"] { display: none; }
  table.container-cards td[data-col="container_number"] .cell-link::after { content: ""; position: absolute; inset: 0; }
  table.container-cards .checkbox, table.container-cards .owner-cell,
  table.container-cards a.text-link { position: relative; z-index: 1; }

  /* Bills: reference and status; carrier and how many boxes; route, whose,
     ETA. The card opens and closes its boxes, as the row does on a laptop;
     the reference opens the bill's page. */
  table.bill-cards tr.ship-row { grid-template-areas: "caret ref status" "caret carrier count" "caret route route" "caret cust cust" "caret eta eta"; cursor: pointer; }
  table.bill-cards td.cell-caret { grid-area: caret; padding-top: 2px; }
  table.bill-cards td[data-col="reference"] { grid-area: ref; }
  table.bill-cards td[data-col="carrier"] { grid-area: carrier; font-size: 13px; color: var(--text2); }
  table.bill-cards td[data-col="containers"] { grid-area: count; font-size: 13px; text-align: right; }
  table.bill-cards td[data-col="containers"]::before { content: "Containers "; color: var(--text3); font-weight: 400; }
  table.bill-cards td[data-col="eta"] { grid-area: eta; font-size: 13px; }
  table.bill-cards td[data-col="vessel"] { display: none; }
  table.bill-cards tr.ship-kids { display: block; padding: 0 0 8px; }
  table.bill-cards tr.ship-kids > td { display: block; }

  /* ---- Every other table stacks (DISCOVERY D1) ----
     A row is a block and each cell a line: its column's name on the left, the
     value on the right. The name comes from the table's own <thead>, written
     onto each cell by labelStackTables(); the first cell is the row's title. */
  table.stack-on-phone, table.stack-on-phone tbody, table.stack-on-phone tfoot, table.stack-on-phone tr { display: block; }
  table.stack-on-phone thead { display: none; }
  table.stack-on-phone tr { padding: 10px 14px; border-bottom: 1px solid var(--border-light); }
  table.stack-on-phone td { display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
                            padding: 4px 0; border: none; text-align: right; min-width: 0; }
  table.stack-on-phone td::before { content: attr(data-label); flex-shrink: 0; text-align: left;
                                    color: var(--text3); font-size: 12px; font-weight: 600; font-family: var(--font-ui); }
  table.stack-on-phone td:first-child, table.stack-on-phone td[colspan] { display: block; text-align: left; }
  table.stack-on-phone td:first-child::before, table.stack-on-phone td[colspan]::before,
  table.stack-on-phone td:not([data-label])::before { content: none; }
  table.stack-on-phone tr:hover td { background: none; }
  /* A container's or a bill's page (DISCOVERY D; the phone reviewer): the
     facts two to a row, the timeline IN the page -- it was a scroll box inside
     the scrolling page, cut at the right -- and a shorter map. Each event is
     its rail, then when, what and where, one under another. */
  .fact-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .fact-card { padding: 12px; }
  .fact-value { font-size: 18px; overflow-wrap: anywhere; }
  .ev-scroll { max-height: none; overflow: visible; }
  .ev-table, .ev-table tbody { display: block; }
  .ev-table thead { display: none; }
  .ev-table tr { display: grid; grid-template-columns: 24px minmax(0, 1fr); column-gap: 10px;
                 border-bottom: 1px solid var(--border-light); }
  .ev-table tbody tr:last-child { border-bottom: 0; }
  .ev-table td { border-bottom: 0; padding: 2px 0; }
  .ev-table td.ev-rail { grid-row: 1 / span 3; }
  .ev-table td:not(.ev-rail) { grid-column: 2; }
  .ev-table td.ev-when { padding-top: 10px; }
  .ev-table td.ev-loc { padding-bottom: 10px; }
  /* The dashboard's routes and map, one under the other (RULES.md 4.60, D9). */
  .widget-row { grid-template-columns: minmax(0, 1fr); }
  .routes-table td:first-child { overflow-wrap: anywhere; }
  /* A block's title is a line of its own, not a grid of rail and cells. */
  .ev-table tr.ev-group { display: block; }
  .ev-table tr.ev-group td { display: block; padding: 14px 0 6px; }
  .map-box { height: 220px; }
  /* Tiles two to a row; an odd last tile takes the whole row (DISCOVERY D2). */
  .stat-row, .kpi-row, .stat-grid, .dp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .kpi-row.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-row > :last-child:nth-child(odd), .kpi-row > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  /* iOS zooms the whole page into any field whose text is under 16px
     (DISCOVERY D7), so on a phone none is. */
  input:not([type="checkbox"]):not([type="radio"]), select, textarea { font-size: 16px !important; }
  /* 44px to touch -- Apple's minimum, the phone reviewer's recommendation. */
  .btn:not(.btn-tiny), .chip, .tab-nav .tab, .nav-item, .bell-btn, .account-btn, .search-toggle, .back-btn, .preset-select, .date-select { min-height: 44px; }
  .btn-icon:not(.btn-tiny) { min-width: 44px; }
  /* ...so a password's eye is 44px wide, and the field's words stop short of it. */
  .form-group .pw-field input { padding-right: 48px; }
  /* A tag's × and the box to type one, on its page. */
  .tag-x { min-width: 44px; min-height: 44px; }
  .tag-input { min-height: 44px; }

  /* Dialogs rise from the bottom and use the full width, the way every other
     app on the phone behaves. */
  .modal-overlay { padding: 0; align-items: flex-end; }
  .modal, .modal.modal-wide, .modal.modal-sm, .modal.modal-lg { width: 100%; max-width: 100%; max-height: 92vh;
                              border-radius: 12px 12px 0 0; }
  /* The buttons are already pinned to the sheet's foot (DISCOVERY A9 / D9);
     on a phone they also clear the home bar. */
  .modal-footer { padding-bottom: calc(16px + env(safe-area-inset-bottom)); border-radius: 0; }

  /* Nothing may push the page sideways. Wide tables scroll inside themselves. */
  .table-scroll { -webkit-overflow-scrolling: touch; }

  /* The import window's "or enter them here" grid: its foot was pinned to the
     sheet's bottom, and on a phone that two-line bar sat over the grid, which
     showed as a strip (THIRD-EYE C6). Here the foot follows the grid, and the
     grid shows a few rows whatever is above it. The other steps' buttons stay
     pinned. */
  .pg-foot.import-actions { position: static; margin-bottom: 0; padding-bottom: 12px; }
  .pg-wrap { min-height: 190px; }
  /* Sign-up, reset, invite: the laptop's 40px each side left a phone's
     fields 279px, and the phone number's "7 9999 9999" was cut beside the
     country picker (C5). */
  .login-wrap:not(.login-split) .login-card { padding: 28px 20px; }
}

/* A report on paper (printReport): the report alone -- no menu, header,
   banners, tabs or buttons -- under its own heading, landscape, as a table.
   Narrower paper is under 768px wide, so the phone block above would stack
   it: the #view-reports rules outrank those. */
@media print {
  @page { size: landscape; margin: 12mm; }
  .sidebar, .header, .tabbar, .nav-backdrop, .system-strip, .provider-banner, .impersonation-bar,
  .page-lede, .toast-wrap, #reportTabs, #view-reports .table-toolbar { display: none !important; }
  #appShell, .main { display: block !important; margin: 0 !important; }
  .content { padding: 0 !important; overflow: visible !important; }
  #view-reports .report-print-head { display: block; margin-bottom: 16px; }
  .report-print-title { font-size: 20px; font-weight: 700; }
  .report-print-sub { font-size: 13px; color: var(--text2); margin-top: 4px; }
  #view-reports .stat-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  #view-reports .table-wrap { box-shadow: none; }
  #view-reports .table-scroll { overflow: visible; }
  #view-reports table.report-table { display: table; width: 100%; }
  #view-reports table.report-table thead { display: table-header-group; }
  #view-reports table.report-table tbody { display: table-row-group; }
  #view-reports table.report-table tr { display: table-row; padding: 0; page-break-inside: avoid; }
  #view-reports table.report-table th { cursor: default; }
  #view-reports table.report-table td { display: table-cell; text-align: left; padding: 6px 8px; }
  #view-reports table.report-table td::before { content: none; }
}
