/* ============================================================
   MTS — Maritime Transport Solutions  v2.1
   Fonts: IBM Plex Sans (UI/body) + IBM Plex Sans Condensed (headings)
          + IBM Plex Mono (reference codes, dates, tabular figures)
   Icons: Google Material Symbols Rounded
   Design: Shipping-register console — steel navy, hairline structure,
           flat surfaces, monospace manifest stamps. No gradients.
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Sans+Condensed:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Poppins:wght@300;400;500;600&family=Montserrat:wght@500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&family=Chakra+Petch:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Nunito:wght@400;500;600;700&family=Baloo+2:wght@500;600;700&family=Space+Mono:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');

:root {
  /* Brand palette — steel navy + muted brass signal. No bright/candy tones. */
  --primary:       #1C3D57;
  --primary-dark:  #0B2338;
  --primary-mid:   #24506F;
  --primary-light: #EBEEF1;
  --accent:        #A8631A;
  --accent-light:  #F5EEE2;
  --success:       #1F6B45;
  --success-light: #E8F0EA;
  --danger:        #A82A20;
  --danger-light:  #F6EAE8;
  --warning:       #92620C;
  --warning-light: #F5EFDD;
  --info:          #2B5872;
  --info-light:    #E9EFF2;

  /* Neutrals — cool graphite, flat (not blue-tinted slate) */
  --gray-50:  #F7F7F8;
  --gray-100: #EEEFF0;
  --gray-200: #DDDFE2;
  --gray-300: #C2C6CB;
  --gray-400: #8D939B;
  --gray-500: #656B73;
  --gray-600: #464C54;
  --gray-700: #2E333A;
  --gray-800: #1B1F24;
  --gray-900: #101318;

  /* Surfaces */
  --bg:        #EEF0F1;
  --surface:   #FFFFFF;
  --sidebar-bg:#0E2436;
  --sidebar-w: 248px;

  /* Typography */
  --font-body:    'IBM Plex Sans', sans-serif;
  --font-display: 'IBM Plex Sans Condensed', sans-serif;
  --font-mono:    'IBM Plex Mono', monospace;

  /* Spacing — machined corners, not bubbly */
  --radius:    3px;
  --radius-lg: 4px;
  --radius-xl: 6px;

  /* Shadows — near-hairline; structure comes from borders, not glow */
  --shadow-sm: 0 1px 0 rgba(16,19,24,.05);
  --shadow:    0 1px 2px rgba(16,19,24,.09);
  --shadow-md: 0 2px 5px rgba(16,19,24,.11);
  --shadow-lg: 0 6px 16px rgba(16,19,24,.14);

  /* ── MOTION ──────────────────────────────────────────────────────────
     One scale, five steps. Before this every rule picked its own duration
     and thirteen different ones were in use — .12, .13, .14, .15, .16, .18,
     .2, .25, .28, .3 — so two controls side by side settled at different
     times and nothing could be slowed down or sped up in one place.

     Pick by what MOVES, not by how it feels:
       --t-fast    a colour, a background, a border (nothing changes size)
       --t-base    the default for a control: a button, an input, a row
       --t-slow    something that moves or grows: a dot, a chip, a card
       --t-slower  a panel, a drawer, a page transition

     The easings are the Material pair plus one for leaving. --ease is the
     default; anything that enters the screen uses --ease-out so it settles
     rather than stops. */
  --t-instant:  80ms;
  --t-fast:    120ms;
  --t-base:    150ms;
  --t-slow:    200ms;
  --t-slower:  280ms;
  --ease:      cubic-bezier(.4, 0, .2, 1);
  --ease-out:  cubic-bezier(.2, .8, .3, 1);
  --ease-in:   cubic-bezier(.4, 0, 1, 1);

  /* ── CONTROLS ────────────────────────────────────────────────────────
     The three values every form control shares. Each of these was written
     out by hand in several places, which is how a themed install ended up
     with a navy input and a Tailwind-blue glow around it. */

  /* The ring around anything focused. color-mix, never a frozen rgba, or it
     stops following the theme the moment somebody changes the primary. */
  --focus-ring:     0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent);
  --control-border: 1.5px;

  /* The room a page leaves at the bottom for the floating buttons that sit over
     it. It was only ever a fallback inside one calc(), so changing it meant
     finding that calc(). */
  --fab-clearance: 76px;

  /* ── The console ─────────────────────────────────────────────────────
     A dark, fixed-width surface for text a machine wrote: a log, a JSON
     payload, a curl line, a device push body. Deliberately NOT themed — it is
     a terminal, it is the same on every install, and a log that changes colour
     with the brand is a log nobody trusts. It is a token anyway so the four
     pages that draw one stop each picking their own near-black. */
  --console-bg:  #0F172A;
  --console-ink: #C2C6CB;

  /* The chevron in a <select>. A data URI cannot inherit currentColor, so the
     colour is baked in and this is the ONE place it is baked: a theme that
     changes the neutrals redefines this line and every dropdown follows.
     %238D939B is --gray-400. */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cpath fill='%238D939B' d='M7 10l5 5 5-5H7z'/%3E%3C/svg%3E");
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--gray-800);
  line-height: 1.6;
  min-height: 100vh;
  display: flex;
  font-variant-numeric: tabular-nums;
}

/* ── MATERIAL ICONS ── */
.material-symbols-rounded {
  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
  vertical-align: middle;
  line-height: 1;
  font-size: 20px;
  flex-shrink: 0;
}
.icon-sm  { font-size: 16px; }
.icon-lg  { font-size: 24px; }
.icon-xl  { font-size: 32px; }
.icon-filled { font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; }

/* ────────────────────────────────────
   SIDEBAR
──────────────────────────────────── */
.sidebar {
  width: var(--sidebar-w);
  background: var(--sidebar-bg);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: 200;
  overflow: hidden;
  transition: transform var(--t-slower) var(--ease);
}

.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.45);
  z-index: 199;
}

.sidebar-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem 1.125rem 1rem;
  border-bottom: 1px solid rgba(255,255,255,0.07);
  flex-shrink: 0;
}

.sidebar-logo {
  height: 38px;
  width: auto;
  object-fit: contain;
}

.sidebar-brand {
  text-decoration: none;
  flex: 1;
}

.sidebar-brand-fallback { }
.sidebar-brand-name {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  color: white;
  letter-spacing: -0.005em;
  line-height: 1;
}
.sidebar-brand-name span { color: var(--accent); }
.sidebar-brand-sub {
  font-size: 0.6rem;
  color: var(--gray-500);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin-top: 0.2rem;
  font-weight: 500;
}

.sidebar-close {
  background: none;
  border: none;
  color: rgba(255,255,255,0.4);
  cursor: pointer;
  padding: 0.25rem;
  display: none;
  line-height: 1;
}
.sidebar-close:hover { color: white; }

.sidebar-nav-wrap {
  flex: 1;
  padding: 0.75rem 0;
  overflow-y: auto;
  /* Chrome's default scrollbar is a 15px inset track with arrow buttons, which
     next to a dark sidebar reads like a Windows 98 control. Firefox already
     draws a thin overlay bar; this asks Chrome for the same.

     Both declarations are needed: `scrollbar-width` is the standard and is
     what Chrome 121+ and Firefox honour, while the ::-webkit- rules below
     cover older Chrome and Safari, which ignore it. Chrome 121+ ignores the
     ::-webkit- rules once scrollbar-width is set, so they cannot conflict. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, .22) transparent;
}
.sidebar-nav-wrap::-webkit-scrollbar { width: 7px; }
.sidebar-nav-wrap::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav-wrap::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, .22);
  border-radius: 99px;
}
.sidebar-nav-wrap::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, .38); }

.sidebar-section { margin-bottom: 0.25rem; }
.sidebar-section-label {
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: rgba(255,255,255,0.28);
  padding: 0.875rem 1.125rem 0.4rem;
}

.sidebar-nav { list-style: none; }
.sidebar-nav li a {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1.125rem;
  color: rgba(255,255,255,0.58);
  text-decoration: none;
  font-size: 0.835rem;
  font-weight: 500;
  border-radius: 0;
  /* Colour, background and the left bar only. Not `all`: that animated the
     font weight too, and a label changing weight changes width. */
  transition: color var(--t-base), background-color var(--t-base), border-color var(--t-base);
  border-left: 3px solid transparent;
}
.sidebar-nav li a:hover {
  color: rgba(255,255,255,0.9);
  background: rgba(255,255,255,0.06);
}
.sidebar-nav li a.active {
  color: white;
  background: color-mix(in srgb, var(--primary) 40%, transparent);
  border-left-color: var(--accent);
  /* Same weight as every other item on purpose. 600 is wider than 500, so
     the active label re-flowed on every page change; the colour, background
     and accent bar already say which item you are on. */
}
.sidebar-nav li a .material-symbols-rounded { font-size: 19px; }

.sidebar-footer {
  flex-shrink: 0;
  padding: 1rem 1.125rem;
  border-top: 1px solid rgba(255,255,255,0.07);
  display: flex;
  align-items: center;
  gap: 0.75rem;
}


/* ── Sidebar market ticker ────────────────────────────────────────────────
   Two lines, no taller than the user block it replaced: stocks on top,
   currencies below. Each line scrolls its own row of quotes, so a strip 248px
   wide can carry more instruments than would ever fit standing still.

   Hovering stops it, because a number that is moving is a number you cannot
   read, and reduced-motion stops it for good. */
.market-strip {
  width: 100%;
  min-width: 0;
  flex-shrink: 0;
  /* 26px of floor clearance: the browser's link-target bubble is drawn in this
     corner and is about that tall, so without it the bubble covers the rates
     every time the pointer crosses a link. */
  padding: .55rem .9rem 26px;
  border-top: 1px solid rgba(255,255,255,.07);
}

.market-line {
  overflow: hidden;
  height: 17px;
  display: flex;
  align-items: center;
  mask-image: linear-gradient(90deg, transparent 0, #000 10px, #000 calc(100% - 10px), transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10px, #000 calc(100% - 10px), transparent 100%);
}

.market-track {
  display: inline-flex;
  flex-wrap: nowrap;
  gap: 0;
  white-space: nowrap;
  will-change: transform;
  /* The track holds the same run of quotes twice and travels exactly half its
     width, so the wrap lands on an identical frame.
     
     The gap belongs to the run, not to the track. With `gap` on the track the
     two runs were separated by one extra gap that half the width does not
     account for, and the loop jumped by that much on every repeat. */
  animation: marketScroll var(--market-dur, 34s) linear infinite;
}
.market-run {
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0 1.15rem;
  padding-right: 1.15rem;   /* the seam between one run and the next */
}
.market-strip:hover .market-track { animation-play-state: paused; }
@keyframes marketScroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .market-track { animation: none; }
}

/* A line that is standing still has no edge to fade into, and the mask was
   eating the first letters of the message. */
.market-line.is-static { mask-image: none; -webkit-mask-image: none; }

.market-item {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  font-size: .655rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.market-sym { color: rgba(255,255,255,.5); font-weight: 600; letter-spacing: .02em; }
.market-val { color: rgba(255,255,255,.88); font-weight: 600; }

/* Direction is a shape first and a colour second, so it still reads for
   anyone who cannot separate the two. */
.market-chg { font-weight: 600; display: inline-flex; align-items: center; gap: .15rem; }
.market-chg.up   { color: #4ADE80; }
.market-chg.down { color: #F87171; }
.market-chg.flat { color: rgba(255,255,255,.4); }
.market-arrow { font-size: .5rem; line-height: 1; }

.market-note {
  font-size: .6rem;
  color: rgba(255,255,255,.34);
  line-height: 1.35;
  white-space: nowrap;
}

.sidebar-user { display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; }
.sidebar-avatar {
  width: 34px; height: 34px;
  border-radius: var(--radius);
  background: var(--primary-mid);
  border: 1px solid rgba(255,255,255,0.14);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  color: white;
  flex-shrink: 0;
}
.sidebar-user-info { min-width: 0; }
.sidebar-user-name {
  font-size: 0.8rem;
  font-weight: 600;
  color: rgba(255,255,255,0.85);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidebar-user-role {
  font-size: 0.65rem;
  color: rgba(255,255,255,0.35);
  text-transform: capitalize;
}
.sidebar-logout {
  color: rgba(255,255,255,0.3);
  text-decoration: none;
  line-height: 1;
  padding: 0.25rem;
  flex-shrink: 0;
}
.sidebar-logout:hover { color: rgba(255,255,255,0.7); }

/* ────────────────────────────────────
   MAIN WRAP
──────────────────────────────────── */
.main-wrap {
  flex: 1;
  margin-left: var(--sidebar-w);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* ── TOPBAR ── */
.topbar {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--surface);
  border-bottom: 1px solid var(--gray-200);
  display: flex;
  align-items: center;
  padding: 0 1.5rem;
  height: 58px;
  gap: 1rem;
  box-shadow: var(--shadow-sm);
  flex-shrink: 0;
}

.topbar-menu-btn {
  background: none;
  border: none;
  color: var(--gray-500);
  cursor: pointer;
  padding: 0.35rem;
  display: none;
  line-height: 1;
  border-radius: var(--radius);
}
.topbar-menu-btn:hover { background: var(--gray-100); color: var(--gray-700); }

.topbar-title {
  font-family: var(--font-display);
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--gray-700);
  flex: 1;
}

.topbar-right { display: flex; align-items: center; gap: 0.75rem; }

.topbar-user { display: flex; align-items: center; gap: 0.5rem; }
.topbar-avatar {
  width: 30px; height: 30px;
  border-radius: var(--radius);
  background: var(--primary);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 700;
  color: white;
}
.topbar-username { font-size: 0.8rem; font-weight: 500; color: var(--gray-700); }
.topbar-logout {
  color: var(--gray-400);
  text-decoration: none;
  padding: 0.35rem;
  border-radius: var(--radius);
}
.topbar-logout:hover { background: var(--gray-100); color: var(--gray-700); }

/* ── NOTIFICATION BELL ── */
.notif-bell-wrap {
  position: relative;
}
.notif-bell-btn {
  position: relative;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--gray-400);
  padding: 0.35rem;
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--t-base), color var(--t-base);
}
.notif-bell-btn:hover { background: var(--gray-100); color: var(--gray-700); }
.notif-bell-btn .material-symbols-rounded { font-size: 22px; }
.notif-badge {
  position: absolute;
  top: 2px; right: 2px;
  background: var(--danger);
  color: white;
  font-size: 0.55rem;
  font-weight: 700;
  min-width: 16px;
  height: 16px;
  border-radius: 99px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 3px;
  line-height: 1;
  pointer-events: none;
  border: 2px solid white;
}
.notif-badge.hidden { display: none; }
.notif-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 320px;
  background: white;
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 999;
  overflow: hidden;
  display: none;
}
.notif-dropdown.open { display: block; }
.notif-drop-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--gray-100);
}
.notif-drop-title { font-size: 0.8rem; font-weight: 700; color: var(--gray-800); }
.notif-drop-markall {
  font-size: 0.7rem;
  color: var(--primary);
  cursor: pointer;
  background: none;
  border: none;
  font-family: var(--font-body);
  font-weight: 500;
  padding: 0;
}
.notif-drop-markall:hover { text-decoration: underline; }
.notif-list { max-height: 340px; overflow-y: auto; }
.notif-item {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  padding: 0.625rem 1rem;
  border-bottom: 1px solid var(--gray-50);
  cursor: pointer;
  transition: background var(--t-fast);
  text-decoration: none;
}
.notif-item:hover { background: var(--gray-50); }
.notif-item.unread { background: var(--primary-light); }
.notif-item.unread:hover { background: #E0E5E9; }
.notif-icon {
  width: 30px; height: 30px;
  border-radius: var(--radius);
  background: var(--primary-light);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  margin-top: 1px;
}
.notif-icon .material-symbols-rounded { font-size: 15px; color: var(--primary); }
.notif-body { flex: 1; min-width: 0; }
.notif-title { font-size: 0.75rem; font-weight: 600; color: var(--gray-800); line-height: 1.35; }
.notif-msg   { font-size: 0.7rem; color: var(--gray-500); margin-top: 1px; line-height: 1.4;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.notif-time  { font-size: 0.63rem; color: var(--gray-400); margin-top: 2px; }
.notif-unread-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--primary); flex-shrink: 0; margin-top: 5px;
}
.notif-empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--gray-400);
  font-size: 0.8rem;
}
.notif-drop-footer {
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--gray-100);
  text-align: center;
}
.notif-drop-footer a {
  font-size: 0.75rem;
  color: var(--primary);
  text-decoration: none;
  font-weight: 500;
}
.notif-drop-footer a:hover { text-decoration: underline; }

/* ── PAGE CONTENT ── */
.page-content {
  flex: 1;
  /* The extra bottom padding is floor clearance for the Help / Chat / Tasks
     buttons, which are fixed to the bottom-right corner.
     
     Measured on the Users page: the dock sits at y 796-838 in an 889px
     viewport and spans x 1572-1877, while the row Actions column spans
     x 1453-1610. They overlap, and with only 24px of padding the last rows
     could not be scrolled clear of it — so the eye, pencil and bin on the
     bottom rows were permanently under the buttons. Reported as the bottom
     buttons making it "hard to choose users options".
     
     Clearance, not decoration: it is the dock's height plus a gap, so every
     row can always be scrolled above it. */
  padding: 1.5rem 1.75rem calc(1.5rem + var(--fab-clearance));
  max-width: 1400px;
  width: 100%;
}

/* No dock for signed-out pages, so no clearance is needed there. */
body:not(.has-fab-dock) .page-content { padding-bottom: 1.5rem; }

/* A table with its own scroller needs the same clearance INSIDE it.
   
   .table-wrap caps at 600px and scrolls its own rows, so padding the page
   below it does not help: scrolled to the end, the last row still came to rest
   against the box's bottom edge, which is exactly where the dock is. Padding
   the scroller means the last row stops above the buttons instead of under
   them — measured on Users, the Actions cell ended at y822 against a dock
   starting at y819, so the bin icon on the final row sat beneath it. */
body.has-fab-dock .table-wrap { padding-bottom: 52px; }

/* A page that wants the whole window — a diagram, a map, a canvas. The
   1400px column is right for reading and wrong for anything you pan around
   in, where the wasted margin is the space the content needed.

   The wrap is normally `min-height: 100vh`, which lets it GROW with its
   content. That is right for a document and wrong here: a tall canvas would
   stretch the page to its own height and there would be nothing left to
   scroll against, so "full height" would silently become "as tall as the
   diagram". Pinning the wrap to exactly 100vh gives the canvas a fixed box
   to scroll inside.

   `min-height: 0` on the child matters for the same reason: without it a
   flex item's default `min-height: auto` lets content set the height, and
   the overflow never engages. */
.main-wrap:has(> .page-content-wide) {
  height: 100vh;
  min-height: 0;
}
.page-content.page-content-wide {
  max-width: none;
  padding: 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* ── FLASH MESSAGES ── */
.flash-wrap { padding: 0.75rem 1.75rem 0; }
.flash-alert {
  position: relative;
  animation: fadeDown 0.25s ease;
}
.flash-close {
  background: none;
  border: none;
  cursor: pointer;
  color: currentColor;
  opacity: 0.5;
  padding: 0;
  margin-left: auto;
  line-height: 1;
}
.flash-close:hover { opacity: 0.9; }

/* ── PAGE HEADER ── */
/* .page-header was the older header pattern. Every screen now opens with
   .page-header-row below, so its rules are gone rather than left to rot. */

.page-header-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
}
.page-header-row h1 {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--gray-900);
  letter-spacing: -0.005em;
}
.page-header-row p {
  font-size: 0.83rem;
  color: var(--gray-500);
  margin-top: 0.2rem;
}

/* ────────────────────────────────────
   CARDS
──────────────────────────────────── */
.card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  margin-bottom: 1.25rem;
}
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--gray-100);
  gap: 0.75rem;
}
.card-title {
  font-family: var(--font-display);
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--gray-800);
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.card-title .material-symbols-rounded { color: var(--primary); }
.card-body { padding: 1.25rem; }

/* ────────────────────────────────────
   STAT CARDS
──────────────────────────────────── */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.stat-card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  gap: 1rem;
}
.stat-icon-wrap {
  width: 46px; height: 46px;
  border-radius: var(--radius-lg);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.stat-icon-wrap .material-symbols-rounded { font-size: 22px; }
.stat-icon-wrap.blue   { background: var(--primary-light); color: var(--primary); }
.stat-icon-wrap.green  { background: var(--success-light); color: var(--success); }
.stat-icon-wrap.amber  { background: var(--accent-light);  color: var(--accent); }
.stat-icon-wrap.red    { background: var(--danger-light);  color: var(--danger); }
.stat-icon-wrap.gray   { background: var(--gray-100);      color: var(--gray-600); }
.stat-icon-wrap.teal   { background: var(--info-light);    color: var(--info); }

.stat-num {
  font-family: var(--font-display);
  font-size: 1.7rem;
  font-weight: 700;
  color: var(--gray-900);
  letter-spacing: -0.005em;
  line-height: 1;
}
.stat-label {
  font-size: 0.75rem;
  color: var(--gray-500);
  font-weight: 500;
  margin-top: 0.2rem;
}
.stat-trend {
  font-size: 0.7rem;
  font-weight: 600;
  margin-top: 0.2rem;
  display: flex;
  align-items: center;
  gap: 0.2rem;
}
.stat-trend.up   { color: var(--success); }
.stat-trend.down { color: var(--danger); }

/* ────────────────────────────────────
   FORMS
──────────────────────────────────── */
.form-group { margin-bottom: 1rem; }
.form-group label {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--gray-700);
  margin-bottom: 0.375rem;
}
.form-group label .req { color: var(--danger); margin-left: 2px; }

input[type=text], input[type=email], input[type=password],
input[type=number], input[type=date], input[type=tel],
input[type=url], input[type=search], input[type=time],
input[type=datetime-local], input:not([type]), select, textarea {
  width: 100%;
  padding: 0.55rem 0.875rem;
  border: var(--control-border) solid var(--gray-200);
  border-radius: var(--radius);
  background: var(--surface);
  font-family: var(--font-body);
  font-size: 0.85rem;
  color: var(--gray-800);
  transition: border-color var(--t-base), box-shadow var(--t-base);
  appearance: none;
  -webkit-appearance: none;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--primary);
  /* --focus-ring, not a value written here. It was a frozen rgba once (the
     default primary, written out by hand), so a themed install got a coloured
     border with a navy glow around it on every field in the product. The token
     is defined with color-mix so it follows whatever the primary becomes, and
     everything else that can be focused points at the same line. */
  box-shadow: var(--focus-ring);
}
/* Remove clunky number-input spinner arrows (cleaner look) */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type=number] { -moz-appearance: textfield; }
select {
  /* The arrow was a slate #94A3B8 that is not in this palette at all, so every
     dropdown in the product pointed at a colour from a different design. */
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right 0.625rem center;
  padding-right: 2rem;
}
textarea { resize: vertical; min-height: 90px; }

/* ── The check mark ──────────────────────────────────────────────────────
   A checkbox is the one control the browser draws itself, and it draws it in
   the browser's OWN accent colour. There was no rule for it here at all: of
   the checkboxes in this product, three components set accent-color locally
   and every other one rendered in Chrome blue or Safari blue, on a screen
   themed steel navy. This is that rule, in one place, for all of them.

   Sizing is deliberate: 15px reads at the same weight as the 0.85rem label
   beside it, and a check the same height as its text is what stops the row
   looking like a typo. */
input[type=checkbox], input[type=radio] {
  accent-color: var(--primary);
  width: 15px;
  height: 15px;
  cursor: pointer;
  flex-shrink: 0;          /* in a flex row it must not be squeezed into a slit */
  vertical-align: -2px;    /* sits on the text baseline, not above it */
}
input[type=checkbox]:focus-visible, input[type=radio]:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
input[type=checkbox]:disabled, input[type=radio]:disabled { cursor: not-allowed; opacity: .55; }
/* A check mark that means something stronger than "on": a permission that can
   destroy data, a setting that costs money. These were written inline, one
   colour at a time, on the pages that happened to need them — which is how the
   same idea ends up as three different shades. Named, they can be found. */
input[type=checkbox].check-danger,  input[type=radio].check-danger  { accent-color: var(--danger); }
input[type=checkbox].check-warning, input[type=radio].check-warning { accent-color: var(--warning); }
/* A label wrapping a check is a click target, and should say so. */
label:has(> input[type=checkbox]), label:has(> input[type=radio]) { cursor: pointer; }

/* ── A check mark and its wording ────────────────────────────────────────
   The commonest hand-rolled thing in the product: a hundred and two labels
   across forty-eight files, each writing out its own
   `display:flex;align-items:center;gap:…` and each choosing a slightly
   different gap and weight. Two shapes cover all of them.

   .check-inline  one line: "Dependant", "Active", "Trust this device"
   .check-block   a sentence that wraps, with the check held at the top line

   In a row of labelled fields the check has to line up with the INPUTS, not
   with their labels, which is why .field-row gives it the height of a field.
   Without that it hangs a third of a field below the boxes beside it — the
   Family row on the employee file is where this was noticed. */
/* Written `label.check-inline` as well as `.check-inline` on purpose: half of
   these sit inside a .form-group, whose `label` rule is display:block at the
   same specificity a bare class would have. Losing that race stacks the check
   above its own wording, which is what it looked like before this line. */
.check-inline, label.check-inline {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  cursor: pointer;
  font-weight: 400;
  font-size: .82rem;
  line-height: 1.35;
}
.check-block, label.check-block {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  cursor: pointer;
  font-weight: 400;
  font-size: .82rem;
  line-height: 1.5;
}
.check-block > input[type=checkbox], .check-block > input[type=radio] { margin-top: .18rem; }

/* Text a machine wrote, on the console surface. Six of these existed as six
   slightly different inline styles: three font sizes, two radii and two greys
   for the same idea. */
.code-block {
  background: var(--console-bg);
  color: var(--console-ink);
  font-family: var(--font-mono);
  font-size: .72rem;
  line-height: 1.65;
  border-radius: var(--radius);
  padding: .6rem .75rem;
  overflow: auto;
  white-space: pre;
  margin: 0;
}
/* A payload with no spaces in it — a token, a URL — has to be allowed to break
   or it pushes the card sideways. */
.code-block.is-wrapped { white-space: pre-wrap; word-break: break-all; }
.code-block .code-key { color: var(--gray-400); }

/* A row of labelled fields that reads across: Name, Relation, Born, Phone, and
   whatever controls belong beside them. Everything sits on the bottom edge, so
   the field boxes line up however tall their labels are. */
.field-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .6rem;
}
.field-row > .form-group { margin: 0; flex: 0 1 auto; min-width: 0; }
/* A field is label + input; a bare control is only as tall as itself. Matching
   the input's height is what puts the two on the same line. */
.field-row > .check-inline, .field-row > .btn { min-height: 2.3rem; }
.field-row > .check-inline { align-items: center; white-space: nowrap; }

.form-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.form-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
.col-span-2 { grid-column: span 2; }
.col-span-3 { grid-column: span 3; }

.form-hint { font-size: 0.72rem; color: var(--gray-400); margin-top: 0.3rem; }

/* ── SEARCH BAR ── */
.search-wrap {
  position: relative;
  flex: 1;
  max-width: 320px;
}
.search-wrap .material-symbols-rounded {
  position: absolute;
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 17px;
  color: var(--gray-400);
  pointer-events: none;
}
.search-wrap input {
  padding-left: 2.4rem;
}

/* ────────────────────────────────────
   BUTTONS
──────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 1.1rem;
  border: none;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 0.835rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  transition: all var(--t-base);
  white-space: nowrap;
  line-height: 1.4;
}
.btn .material-symbols-rounded { font-size: 17px; }
/* Four sizes, together. btn-xs lived two thousand lines further down beside the
   one feature that first needed it, which is how a fifth size gets added
   instead of an existing one being reused. */
.btn-xs  { padding: 2px 6px; font-size: .7rem; line-height: 1.4; }
.btn-sm  { padding: 0.35rem 0.75rem; font-size: 0.775rem; }
.btn-lg  { padding: 0.7rem 1.4rem; font-size: 0.9rem; }
.btn-block { width: 100%; justify-content: center; }
.btn-icon {
  padding: 0.5rem;
  background: none;
  border: none;
  color: var(--gray-500);
  border-radius: var(--radius);
  cursor: pointer;
  transition: all var(--t-base);
  line-height: 1;
}
.btn-icon:hover { background: var(--gray-100); color: var(--gray-700); }
.btn-icon.danger:hover { background: var(--danger-light); color: var(--danger); }

.btn-primary   { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-accent    { background: var(--accent); color: white; }
.btn-accent:hover { filter: brightness(0.92); }
.btn-success   { background: var(--success); color: white; }
.btn-success:hover { filter: brightness(0.9); }
.btn-danger    { background: var(--danger); color: white; }
.btn-danger:hover { filter: brightness(0.9); }
.btn-secondary { background: var(--gray-100); color: var(--gray-700); }
.btn-secondary:hover { background: var(--gray-200); }
.btn-ghost     { background: none; color: var(--gray-600); border: 1.5px solid var(--gray-200); }
.btn-ghost:hover { background: var(--gray-50); border-color: var(--gray-300); }
.btn-white     { background: white; color: var(--primary); font-weight: 600; }
.btn-white:hover { background: var(--gray-100); }
.btn-ghost-white { background: rgba(255,255,255,0.12); color: white; border: 1.5px solid rgba(255,255,255,0.22); }
.btn-ghost-white:hover { background: rgba(255,255,255,0.2); }

/* ────────────────────────────────────
   TABLES
──────────────────────────────────── */
.table-wrap { overflow-x: auto; }
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.835rem;
}
.data-table th {
  padding: 0.65rem 1rem;
  text-align: left;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--gray-500);
  border-bottom: 1.5px solid var(--gray-100);
  white-space: nowrap;
  background: var(--gray-50);
}
.data-table td {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--gray-100);
  color: var(--gray-700);
  vertical-align: middle;
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: var(--gray-50); }
.data-table .actions { display: flex; gap: 0.25rem; }

.ref-code {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--primary-dark);
  background: var(--primary-light);
  border: 1px solid var(--gray-200);
  padding: 0.16rem 0.5rem;
  border-radius: var(--radius);
  display: inline-block;
}

/* ────────────────────────────────────
   BADGES
──────────────────────────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.55rem 0.2rem 0.5rem;
  border-radius: var(--radius);
  border-left: 3px solid currentColor;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 600;
  white-space: nowrap;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.badge-success   { background: var(--success-light);  color: var(--success); }
.badge-danger    { background: var(--danger-light);   color: var(--danger); }
.badge-warning   { background: var(--warning-light);  color: var(--warning); }
.badge-delayed   { background: #F5EFDD;  color: #8A5A0C; }
.badge-critical  { background: var(--danger-light);  color: var(--danger); animation: badgePulse 1.6s ease-in-out infinite; }
@keyframes badgePulse { 0%,100%{opacity:1;} 50%{opacity:.55;} }
.badge-info      { background: var(--info-light);     color: var(--info); }
.badge-secondary { background: var(--gray-100);       color: var(--gray-600); }
.badge-primary   { background: var(--primary-light);  color: var(--primary); }
.badge-accent    { background: var(--accent-light);   color: var(--accent); }
.badge-express   { background: #F5EFDD; color: #8A5A0C; }
.badge-basic     { background: var(--primary-light);  color: var(--primary); }
.badge-collective{ background: var(--success-light);  color: var(--success); }

/* ────────────────────────────────────
   ALERTS
──────────────────────────────────── */
.alert {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.875rem 1rem;
  border-radius: var(--radius);
  font-size: 0.83rem;
  margin-bottom: 1rem;
}
.alert .material-symbols-rounded { font-size: 18px; flex-shrink: 0; margin-top: 1px; }
.alert-info    { background: var(--primary-light); color: var(--primary); }
.alert-success { background: var(--success-light); color: var(--success); }
.alert-warning { background: var(--warning-light); color: var(--warning); }
.alert-error   { background: var(--danger-light);  color: var(--danger); }

/* ═══════════════════════════════════════════════════════════════════════
   ACCENT BAR — a solid, static left rule for alerts / callouts. Deliberately
   plain: a flat colored edge that states the severity once, clearly, rather
   than drawing attention to itself.
   Usage: add class "accent-bar accent-<state>" to any alert/callout element.
   States: accent-warning / accent-danger / accent-info / accent-success / accent-accent
   ═══════════════════════════════════════════════════════════════════════ */
.accent-bar {
  position: relative;
  border-radius: var(--radius) !important;
  overflow: hidden;
}
.accent-bar::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
}
.accent-bar.accent-warning::before { background: var(--warning); }
.accent-bar.accent-danger::before  { background: var(--danger); }
.accent-bar.accent-info::before    { background: var(--primary); }
.accent-bar.accent-success::before { background: var(--success); }
.accent-bar.accent-accent::before  { background: var(--accent); }
/* Arbitrary colour via --accent-color (set inline): add class accent-custom */
.accent-bar.accent-custom::before  { background: var(--accent-color, var(--primary)); }
/* content needs a little breathing room from the bar */
.accent-bar > * { position: relative; }
.card.accent-bar > .card-header,
.card.accent-bar > .card-body,
.card.accent-bar > .table-wrap { padding-left: 1.15rem; }

/* Alerts get the same static bar, mapped from their -info/-warning/etc. */
.alert { border-radius: var(--radius); position: relative; overflow: hidden; padding-left: 1.15rem; border: 1px solid transparent; }
.alert::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
}
.alert-info::before    { background: var(--primary); }
.alert-success::before { background: var(--success); }
.alert-warning::before { background: var(--warning); }
.alert-error::before   { background: var(--danger); }
.alert-info    { border-color: color-mix(in srgb, var(--primary) 18%, transparent); }
.alert-success { border-color: color-mix(in srgb, var(--success) 18%, transparent); }
.alert-warning { border-color: color-mix(in srgb, var(--warning) 18%, transparent); }
.alert-error   { border-color: color-mix(in srgb, var(--danger) 18%, transparent); }

/* ────────────────────────────────────
   PAGINATION
──────────────────────────────────── */
.pagination {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.875rem 1.25rem;
  border-top: 1px solid var(--gray-100);
  flex-wrap: wrap;
}
.pagination .page-nav { padding: 0.3rem 0.4rem; }
.pagination .pagination-gap { border: none; background: none; padding: 0 0.15rem; color: var(--gray-400); }
.pagination a, .pagination span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  border-radius: var(--radius);
  font-size: 0.8rem;
  font-weight: 600;
  text-decoration: none;
  color: var(--gray-600);
  transition: all var(--t-base);
  border: 1.5px solid transparent;
}
.pagination a:hover { background: var(--primary-light); color: var(--primary); }
.pagination .active { background: var(--primary); color: white; }
.pagination .disabled { color: var(--gray-300); pointer-events: none; }
.pagination .pagination-info { font-size: 0.78rem; color: var(--gray-400); margin-left: auto; width: auto; height: auto; }
.pagination .pagination-gap { width: auto; height: auto; }

/* ────────────────────────────────────
   MODAL
──────────────────────────────────── */
.modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 500;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  animation: fadeIn 0.2s ease;
}
.modal-backdrop.open { display: flex; }
.modal {
  background: white;
  border-radius: var(--radius-xl);
  box-shadow: 0 12px 28px rgba(16,19,24,0.22);
  width: 100%;
  max-width: 560px;
  max-height: 90vh;
  overflow-y: auto;
  animation: slideUp 0.22s ease;
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem 1.5rem;
  border-bottom: 1px solid var(--gray-100);
}
.modal-header h3 {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  color: var(--gray-900);
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.modal-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--gray-400);
  padding: 0.25rem;
  border-radius: var(--radius);
  line-height: 1;
}
.modal-close:hover { background: var(--gray-100); color: var(--gray-700); }
.modal-body { padding: 1.5rem; }
.modal-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  padding: 1rem 1.5rem;
  border-top: 1px solid var(--gray-100);
}

/* ────────────────────────────────────
   PACKAGE CARDS (Estimator)
──────────────────────────────────── */
.packages-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 1rem; }
.pkg-card {
  border: 2px solid var(--gray-200);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  cursor: pointer;
  transition: all var(--t-base);
  background: white;
  position: relative;
}
.pkg-card:hover { border-color: var(--gray-300); box-shadow: var(--shadow-md); }
.pkg-card.selected { box-shadow: var(--shadow-md); }
.pkg-card.selected.express    { border-color: var(--accent); background: var(--accent-light); }
.pkg-card.selected.basic      { border-color: var(--primary); background: var(--primary-light); }
.pkg-card.selected.collective { border-color: var(--success); background: var(--success-light); }
.pkg-name {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  margin-bottom: 0.35rem;
}
.express    .pkg-name { color: var(--accent); }
.basic      .pkg-name { color: var(--primary); }
.collective .pkg-name { color: var(--success); }
.pkg-desc { font-size: 0.78rem; color: var(--gray-500); margin-bottom: 0.9rem; line-height: 1.5; }
.pkg-features { list-style: none; }
.pkg-features li {
  font-size: 0.75rem;
  color: var(--gray-600);
  padding: 0.2rem 0;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.pkg-features li .material-symbols-rounded { font-size: 14px; color: var(--success); }
.pkg-multiplier {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.75rem;
  padding: 0.2rem 0.5rem;
  border-radius: var(--radius);
  display: inline-block;
}
.express    .pkg-multiplier { background: var(--warning-light); color: var(--warning); }
.basic      .pkg-multiplier { background: var(--primary-light); color: var(--primary); }
.collective .pkg-multiplier { background: var(--success-light); color: var(--success); }
.pkg-radio {
  position: absolute;
  top: 1rem; right: 1rem;
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 2px solid var(--gray-300);
  background: white;
  transition: all var(--t-base);
  display: flex; align-items: center; justify-content: center;
}
.pkg-card.selected .pkg-radio { background: var(--success); border-color: var(--success); }
.pkg-card.selected .pkg-radio::after { content:''; width:8px; height:8px; background:white; border-radius:50%; }

/* ────────────────────────────────────
   RESULT BOX (Estimator)
──────────────────────────────────── */
.result-card {
  background: var(--gray-900);
  border-radius: var(--radius-lg);
  padding: 1.75rem;
  margin-top: 1.5rem;
  color: white;
}
.result-card h3 {
  font-family: var(--font-display);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin-bottom: 1.25rem;
}
.result-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px,1fr)); gap: 1.25rem; }
.result-item .r-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: rgba(255,255,255,0.4);
  margin-bottom: 0.3rem;
  font-weight: 600;
}
.result-item .r-val {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 700;
  color: white;
  line-height: 1.2;
}
.result-item .r-val.big { font-size: 1.7rem; color: var(--accent); }
.result-divider { border: none; border-top: 1px solid rgba(255,255,255,0.08); margin: 1.5rem 0; }
.breakdown-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.35rem 0;
  font-size: 0.8rem;
  border-bottom: 1px solid rgba(255,255,255,0.06);
}
.breakdown-row:last-child { border-bottom: none; }
.breakdown-row span { color: rgba(255,255,255,0.5); }
.breakdown-row strong { color: white; font-family: var(--font-display); font-weight: 600; }

/* ────────────────────────────────────
   TRACKING
──────────────────────────────────── */
.track-steps {
  display: flex;
  align-items: flex-start;
  margin: 1.5rem 0;
  overflow-x: auto;
  padding-bottom: 0.5rem;
}
.track-step {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  min-width: 80px;
}
.track-step:not(:last-child)::after {
  content: '';
  position: absolute;
  top: 15px;
  left: calc(50% + 16px);
  width: calc(100% - 32px);
  height: 2px;
  background: var(--gray-200);
}
.track-step.done:not(:last-child)::after   { background: var(--success); }
.track-step.active:not(:last-child)::after { background: linear-gradient(to right, var(--success), var(--gray-200)); }
.step-dot {
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 2px solid var(--gray-200);
  background: white;
  display: flex; align-items: center; justify-content: center;
  position: relative; z-index: 1;
  transition: all var(--t-slower);
}
.step-dot .material-symbols-rounded { font-size: 14px; color: var(--gray-300); }
.track-step.done   .step-dot { background: var(--success); border-color: var(--success); }
.track-step.done   .step-dot .material-symbols-rounded { color: white; font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.track-step.active .step-dot { background: var(--primary); border-color: var(--primary); box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 20%, transparent); }
.track-step.active .step-dot .material-symbols-rounded { color: white; }
.step-label {
  font-size: 0.65rem;
  font-weight: 600;
  color: var(--gray-400);
  text-align: center;
  margin-top: 0.5rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  line-height: 1.3;
}
.track-step.done   .step-label { color: var(--success); }
.track-step.active .step-label { color: var(--primary); }

/* Task status (tasks.php): the shipment stepper and nothing else, where every
   step other than the current one is a submit button. A <button> brings its own border,
   background and font, so they are reset here to look exactly like the
   shipment's steps — plus the hover and focus a clickable step needs. */
.task-status-block { margin-bottom: 1.25rem; padding-bottom: 1rem; border-bottom: 1px solid var(--gray-200); }
.task-steps button.track-step {
  appearance: none; background: transparent; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer; text-align: center;
}
.task-steps button.track-step:hover .step-dot { border-color: var(--primary); }
.task-steps button.track-step:hover .step-label { color: var(--primary); }
.task-steps button.track-step:focus-visible { outline: 2px solid var(--primary); outline-offset: 4px; border-radius: 8px; }


/* ── TIMELINE ── */
.timeline { position: relative; padding-left: 1.5rem; }
.timeline::before {
  content: '';
  position: absolute;
  left: 7px; top: 8px; bottom: 8px;
  width: 2px;
  background: var(--gray-200);
}
.tl-item { display: flex; gap: 1rem; padding: 0.75rem 0; position: relative; }
.tl-dot {
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--gray-200);
  border: 2px solid white;
  position: absolute;
  left: -1.5rem; top: 1rem;
  flex-shrink: 0;
  box-shadow: 0 0 0 2px var(--gray-200);
}
.tl-dot.done   { background: var(--success); box-shadow: 0 0 0 2px color-mix(in srgb, var(--success) 20%, transparent); }
.tl-dot.active { background: var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent); }
.tl-time  { font-size: 0.72rem; color: var(--gray-400); font-weight: 500; min-width: 130px; padding-top: 2px; }
.tl-event { font-size: 0.85rem; color: var(--gray-700); font-weight: 500; }
.tl-loc   { font-size: 0.75rem; color: var(--gray-400); margin-top: 0.1rem; display: flex; align-items: center; gap: 0.25rem; }
.tl-loc .material-symbols-rounded { font-size: 12px; }

/* ────────────────────────────────────
   DOCUMENT (Quotation / Invoice)
──────────────────────────────────── */
.doc-layout { display: grid; grid-template-columns: 1fr 420px; gap: 1.5rem; align-items: start; }
.doc-sticky { position: sticky; top: 76px; }
.doc-paper { background: white; border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); overflow: hidden; }
.doc-inner { padding: 2.5rem; }
.doc-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 2rem;
  padding-bottom: 1.5rem;
  border-bottom: 2px solid var(--gray-900);
}
.doc-brand-name {
  font-family: var(--font-display);
  font-size: 1.8rem;
  font-weight: 700;
  color: var(--gray-900);
  letter-spacing: -0.01em;
}
.doc-brand-sub { font-size: 0.7rem; color: var(--gray-400); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 0.25rem; }
.doc-type-label {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--primary);
  text-align: right;
  letter-spacing: -0.005em;
}
.doc-ref-label { font-family: var(--font-mono); font-size: 0.8rem; color: var(--gray-400); text-align: right; margin-top: 0.2rem; }
.doc-meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.75rem; }
.doc-meta-label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.09em; color: var(--gray-400); font-weight: 600; margin-bottom: 0.25rem; }
.doc-meta-val   { font-size: 0.875rem; color: var(--gray-800); font-weight: 500; }
.doc-meta-sub   { font-size: 0.78rem; color: var(--gray-500); }
.doc-table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; font-size: 0.82rem; }
.doc-table th { background: var(--gray-900); color: white; padding: 0.6rem 0.85rem; text-align: left; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.07em; font-weight: 600; }
.doc-table td { padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--gray-100); color: var(--gray-700); }
.doc-table tr:last-child td { border-bottom: none; }
.doc-table .tar { text-align: right; }
.doc-totals { max-width: 260px; margin-left: auto; margin-top: 0.5rem; }
.doc-total-row { display: flex; justify-content: space-between; padding: 0.35rem 0; font-size: 0.82rem; color: var(--gray-600); }
.doc-total-row.grand { font-weight: 700; font-size: 0.95rem; color: var(--gray-900); padding-top: 0.6rem; border-top: 2px solid var(--gray-900); margin-top: 0.3rem; }
.doc-total-row .amount { font-family: var(--font-display); font-weight: 600; }
.doc-footer-note { font-size: 0.75rem; color: var(--gray-400); margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-100); line-height: 1.7; }
.doc-empty { padding: 4rem 2rem; text-align: center; color: var(--gray-400); }
.doc-empty .material-symbols-rounded { font-size: 48px; margin-bottom: 0.75rem; display: block; color: var(--gray-300); }
.doc-empty p { font-size: 0.85rem; }

/* ────────────────────────────────────
   QUICK LINKS (Dashboard)
──────────────────────────────────── */
.quick-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 1rem; margin-bottom: 1.25rem; }
.quick-card {
  background: white;
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  text-decoration: none;
  color: var(--gray-800);
  box-shadow: var(--shadow);
  transition: all var(--t-base);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.quick-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.quick-icon-wrap {
  width: 42px; height: 42px;
  border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 0.25rem;
}
.quick-icon-wrap.blue   { background: var(--primary-light); color: var(--primary); }
.quick-icon-wrap.amber  { background: var(--accent-light);  color: var(--accent); }
.quick-icon-wrap.green  { background: var(--success-light); color: var(--success); }
.quick-icon-wrap.gray   { background: var(--gray-100);      color: var(--gray-600); }
.quick-icon-wrap.teal   { background: var(--info-light);    color: var(--info); }
.quick-icon-wrap.red    { background: var(--danger-light);  color: var(--danger); }
.quick-name { font-family: var(--font-display); font-size: 0.875rem; font-weight: 700; color: var(--gray-800); }
.quick-desc { font-size: 0.75rem; color: var(--gray-500); line-height: 1.4; }

/* ────────────────────────────────────
   DASHBOARD HERO
──────────────────────────────────── */
.dash-hero {
  background-color: var(--primary-dark);
  background-image:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.035) 0 1px, transparent 1px 48px),
    repeating-linear-gradient(0deg,  rgba(255,255,255,0.035) 0 1px, transparent 1px 48px);
  border-radius: var(--radius-xl);
  border-bottom: 3px solid var(--accent);
  padding: 2rem;
  color: white;
  margin-bottom: 1.25rem;
  position: relative;
  overflow: hidden;
}
.dash-hero-content { position: relative; z-index: 1; }
.dash-hero h1 {
  font-family: var(--font-display);
  font-size: 1.65rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-bottom: 0.3rem;
}
.dash-hero h1 span { color: var(--accent); }
.dash-hero p { font-size: 0.875rem; color: rgba(255,255,255,0.65); }
.dash-actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; flex-wrap: wrap; }

/* ────────────────────────────────────
   EMPTY STATE
──────────────────────────────────── */
.empty-state {
  padding: 4rem 2rem;
  text-align: center;
  color: var(--gray-400);
}
.empty-state .material-symbols-rounded { font-size: 52px; display: block; margin: 0 auto 1rem; color: var(--gray-300); }
.empty-state h3 { font-size: 1rem; font-weight: 600; color: var(--gray-500); margin-bottom: 0.35rem; }
.empty-state p  { font-size: 0.83rem; }

/* ────────────────────────────────────
   STATUS INDICATOR
──────────────────────────────────── */
.status-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  margin-right: 0.35rem;
  flex-shrink: 0;
}
.status-dot.active  { background: var(--success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 20%, transparent); }
.status-dot.idle    { background: var(--warning); }
.status-dot.offline { background: var(--gray-300); }
.status-dot.danger  { background: var(--danger); }

/* ────────────────────────────────────
   INFO ROW (detail view)
──────────────────────────────────── */
.info-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 1rem; }
.info-item .info-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--gray-400); margin-bottom: 0.2rem; }
.info-item .info-val   { font-size: 0.875rem; color: var(--gray-800); font-weight: 500; }

/* ────────────────────────────────────
   LOGIN
──────────────────────────────────── */
.login-page {
  min-height: 100vh;
  background: var(--primary-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
.login-split {
  display: grid;
  grid-template-columns: 1fr 420px;
  min-height: 100vh;
  width: 100%;
}
.login-left {
  background-color: var(--primary-dark);
  background-image:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.035) 0 1px, transparent 1px 48px),
    repeating-linear-gradient(0deg,  rgba(255,255,255,0.035) 0 1px, transparent 1px 48px);
  border-right: 3px solid var(--accent);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 4rem;
  position: relative;
  overflow: hidden;
}
.login-left-content { position: relative; z-index: 1; }
.login-left-logo { height: 50px; margin-bottom: 3rem; }
.login-left h1 {
  font-family: var(--font-display);
  font-size: 2.2rem;
  font-weight: 700;
  color: white;
  letter-spacing: -0.01em;
  line-height: 1.15;
  margin-bottom: 1rem;
}
.login-left h1 span { color: var(--accent); }
.login-left p { font-size: 0.925rem; color: rgba(255,255,255,0.55); line-height: 1.7; max-width: 380px; }
.login-left-features { margin-top: 2.5rem; display: flex; flex-direction: column; gap: 0.875rem; }
.login-feature {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  color: rgba(255,255,255,0.65);
  font-size: 0.875rem;
}
.login-feature .material-symbols-rounded { font-size: 20px; color: var(--accent); flex-shrink: 0; }

.login-right {
  background: white;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
.login-box {
  width: 100%;
  max-width: 360px;
}
.login-box-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--gray-900);
  margin-bottom: 0.35rem;
  letter-spacing: -0.005em;
}
.login-box-sub { font-size: 0.83rem; color: var(--gray-400); margin-bottom: 1.75rem; }
.login-error {
  background: var(--danger-light);
  color: var(--danger);
  border-radius: var(--radius);
  padding: 0.65rem 0.875rem;
  font-size: 0.8rem;
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* ────────────────────────────────────
   SITE FOOTER
──────────────────────────────────── */
.site-footer {
  background: white;
  border-top: 1px solid var(--gray-100);
  padding: 0.875rem 1.75rem;
  font-size: 0.75rem;
  color: var(--gray-400);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

/* ────────────────────────────────────
   UTILITIES
──────────────────────────────────── */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start  { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-end  { justify-content: flex-end; }
.flex-wrap { flex-wrap: wrap; }
.gap-xs { gap: 0.35rem; }
.gap-1  { gap: 0.5rem; }
.gap-2  { gap: 1rem; }
.gap-3  { gap: 1.5rem; }
.mb-1   { margin-bottom: 0.5rem; }
.mb-2   { margin-bottom: 1rem; }
.mb-3   { margin-bottom: 1.5rem; }
.mt-1   { margin-top: 0.5rem; }
.mt-2   { margin-top: 1rem; }
.mt-3   { margin-top: 1.5rem; }
.p-0    { padding: 0; }
.text-sm    { font-size: 0.8rem; }
.text-xs    { font-size: 0.72rem; }
.text-muted { color: var(--gray-400); }
.text-right { text-align: right; }
.text-center { text-align: center; }
.font-mono  { font-family: var(--font-mono); }
.font-display { font-family: var(--font-display); }
.fw-500 { font-weight: 500; }
.fw-600 { font-weight: 600; }
.fw-700 { font-weight: 700; }
.fw-800 { font-weight: 800; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }

/* ── A page, and the things beside it ────────────────────────────────────
   A record screen is not two equal halves: it is the record, with a column of
   context next to it — where they sit, their documents, what is outstanding.
   Written as .grid-2 the short context column left half the screen empty
   beside a tall form, which is what "too much empty space" on the employee
   file was. Written this way the aside is a margin note and the emptiness
   below it reads as breathing room rather than a hole.

   It was already being written by hand as `1fr 300px` on the user screens. */
.grid-main-aside {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: 1.25rem;
  align-items: start;
}
.grid-main-aside > * { min-width: 0; }
@media (max-width: 1100px) { .grid-main-aside { grid-template-columns: 1fr; } }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.25rem; }
.grid-4 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 1.25rem; }
@media (max-width: 768px) {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
}
.color-primary { color: var(--primary); }
.color-success { color: var(--success); }
.color-danger  { color: var(--danger); }
.color-accent  { color: var(--accent); }
.divider { border: none; border-top: 1px solid var(--gray-100); margin: 1rem 0; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ────────────────────────────────────
   ANIMATIONS
──────────────────────────────────── */
@keyframes fadeIn   { from { opacity:0; } to { opacity:1; } }
@keyframes fadeUp   { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
@keyframes fadeDown { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:translateY(0); } }
@keyframes slideUp  { from { opacity:0; transform:translateY(20px) scale(.98); } to { opacity:1; transform:translateY(0) scale(1); } }
@keyframes pulse    { 0%,100%{opacity:1} 50%{opacity:.5} }
.animate-pulse { animation: pulse 2s ease infinite; }

/* ── Somebody who asked for less motion ──────────────────────────────────
   One rule, for everything. There are nine other prefers-reduced-motion
   blocks further down this file, each written by whoever built one feature
   and each covering only that feature — so a person who had asked the
   operating system to stop animations still got the ones nobody had thought
   about. This is the floor underneath all of them.

   Durations are collapsed to a hair rather than set to `none`, deliberately:
   transitionend and animationend still fire, so any script waiting for one
   carries on instead of hanging. The specific blocks below stay, because each
   says something this cannot — that a marquee should not scroll at all, for
   instance, rather than scroll instantly. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ────────────────────────────────────
   RESPONSIVE
──────────────────────────────────── */
@media (max-width: 1200px) {
  .stats-grid { grid-template-columns: repeat(2,1fr); }
  .quick-grid  { grid-template-columns: repeat(2,1fr); }
}

@media (max-width: 1024px) {
  :root { --sidebar-w: 0px; }
  .sidebar { transform: translateX(-248px); width: 248px; }
  .sidebar.open { transform: translateX(0); }
  .sidebar-overlay.open { display: block; }
  .sidebar-close { display: block; }
  .main-wrap { margin-left: 0; }
  .topbar-menu-btn { display: flex; }
  .doc-layout { grid-template-columns: 1fr; }
  .packages-grid { grid-template-columns: 1fr; }
  .login-split { grid-template-columns: 1fr; }
  .login-left { display: none; }
  .login-right { padding: 2rem; }
  .info-grid { grid-template-columns: repeat(2,1fr); }
}

@media (max-width: 768px) {
  .page-content { padding: 1rem; }
  .page-header-row { flex-direction: column; align-items: flex-start; }
  .form-grid-2, .form-grid-3 { grid-template-columns: 1fr; }
  .col-span-2, .col-span-3 { grid-column: span 1; }
  .dash-hero { padding: 1.5rem; }
  .dash-hero h1 { font-size: 1.3rem; }
  .topbar-username { display: none; }
  .info-grid { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  .stats-grid { grid-template-columns: 1fr 1fr; }
  .quick-grid  { grid-template-columns: 1fr 1fr; }
}

@media print {
  .sidebar, .topbar, .btn, .doc-layout > form,
  .site-footer, .flash-wrap, .sidebar-overlay { display: none !important; }
  .main-wrap { margin-left: 0; }
  .doc-layout { grid-template-columns: 1fr; }
  .doc-paper { box-shadow: none; }
  body { background: white; }
}


/* ── Dropdown (status controls) ─────────────────────────── */
.dropdown { position: relative; }
.dropdown-menu {
  display: none; position: absolute; top: calc(100% + 4px); left: 0; z-index: 9999;
  min-width: 160px; background: var(--surface); border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg); box-shadow: 0 6px 16px rgba(16,19,24,.14); padding: 4px; overflow: hidden;
}
.dropdown-menu.show { display: block; }
.dropdown-item {
  display: flex; align-items: center; gap: .5rem; padding: .5rem .7rem;
  font-size: .82rem; color: var(--gray-800); border-radius: var(--radius); text-decoration: none;
  white-space: nowrap; transition: background var(--t-fast);
}
.dropdown-item:hover { background: var(--gray-50); }
.dropdown-item .material-symbols-rounded { font-size: 16px; }
/* Two-column detail layout (form left, related panels right) */
@media (max-width: 1024px) {
  .detail-grid { grid-template-columns: 1fr !important; }
}

/* ═══════════════════════════════════════════════════════════════════════
   BASIC MOOD — restores the exact original v2.0 look. Color/type/shape/
   shadow tokens are already handled by the Branding Mood system's :root
   overrides, but some structural details (gradients, pill shapes, the
   animated accent bar) were baked directly into component rules during the
   Professional redesign rather than left as swappable variables. This block
   restores those specific behaviors when Basic mood is active, so Basic is
   pixel-identical to the pre-redesign default rather than just recolored.
   ═══════════════════════════════════════════════════════════════════════ */
html[data-theme="basic"] .sidebar-avatar,
html[data-theme="basic"] .topbar-avatar {
  border-radius: 50%;
  background: linear-gradient(135deg, var(--primary-mid), var(--accent));
  border: none;
  font-family: var(--font-display);
  font-weight: 700;
}
html[data-theme="basic"] .notif-icon { border-radius: 50%; }
html[data-theme="basic"] .notif-item.unread { background: #EFF6FF; }
html[data-theme="basic"] .notif-item.unread:hover { background: #DBEAFE; }

html[data-theme="basic"] .ref-code {
  font-family: 'Courier New', monospace;
  font-weight: 700;
  color: var(--primary);
  border: none;
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
}

html[data-theme="basic"] .badge {
  border-radius: 99px;
  border-left: none;
  font-family: var(--font-body);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: none;
  padding: 0.22rem 0.6rem;
}
html[data-theme="basic"] .badge-delayed   { background: #FEF3C7; color: #B45309; }
html[data-theme="basic"] .badge-critical  { background: #FEE2E2; color: #B91C1C; }
html[data-theme="basic"] .badge-express   { background: #FEF3C7; color: #92400E; }

html[data-theme="basic"] .pkg-multiplier {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.06em;
  border-radius: 99px;
}
html[data-theme="basic"] .express    .pkg-multiplier { background: #FEF3C7; color: #92400E; }
html[data-theme="basic"] .basic      .pkg-multiplier { background: #DBEAFE; color: #1E40AF; }
html[data-theme="basic"] .collective .pkg-multiplier { background: #D1FAE5; color: #065F46; }

/* Animated drifting-gradient accent bar + alert (the original flourish) */
html[data-theme="basic"] .accent-bar {
  border-radius: 0 20px 20px 0 !important;
}
html[data-theme="basic"] .accent-bar::before {
  width: 6px;
  background-size: 100% 300%;
  animation: accentDriftBasic 3.2s ease-in-out infinite;
}
html[data-theme="basic"] .card.accent-bar > .card-header,
html[data-theme="basic"] .card.accent-bar > .card-body,
html[data-theme="basic"] .card.accent-bar > .table-wrap { padding-left: 1.35rem; }
html[data-theme="basic"] .accent-bar.accent-warning::before { background-image: linear-gradient(180deg, var(--warning), color-mix(in srgb, var(--warning) 45%, #fff), var(--warning)); }
html[data-theme="basic"] .accent-bar.accent-danger::before  { background-image: linear-gradient(180deg, var(--danger),  color-mix(in srgb, var(--danger) 45%, #fff),  var(--danger)); }
html[data-theme="basic"] .accent-bar.accent-info::before    { background-image: linear-gradient(180deg, var(--primary), color-mix(in srgb, var(--primary) 45%, #fff), var(--primary)); }
html[data-theme="basic"] .accent-bar.accent-success::before { background-image: linear-gradient(180deg, var(--success), color-mix(in srgb, var(--success) 45%, #fff), var(--success)); }
html[data-theme="basic"] .accent-bar.accent-accent::before  { background-image: linear-gradient(180deg, var(--accent),  color-mix(in srgb, var(--accent) 45%, #fff),  var(--accent)); }
html[data-theme="basic"] .alert {
  border-radius: 0 20px 20px 0;
  padding-left: 1.35rem;
  border: none;
}
html[data-theme="basic"] .alert::before {
  width: 6px;
  background-size: 100% 300%;
  animation: accentDriftBasic 3.2s ease-in-out infinite;
}
html[data-theme="basic"] .alert-info::before    { background-image: linear-gradient(180deg, var(--primary), color-mix(in srgb, var(--primary) 45%, #fff), var(--primary)); }
html[data-theme="basic"] .alert-success::before { background-image: linear-gradient(180deg, var(--success), color-mix(in srgb, var(--success) 45%, #fff), var(--success)); }
html[data-theme="basic"] .alert-warning::before { background-image: linear-gradient(180deg, var(--warning), color-mix(in srgb, var(--warning) 45%, #fff), var(--warning)); }
html[data-theme="basic"] .alert-error::before   { background-image: linear-gradient(180deg, var(--danger),  color-mix(in srgb, var(--danger) 45%, #fff),  var(--danger)); }
@keyframes accentDriftBasic {
  0%   { background-position: 0% 0%; }
  50%  { background-position: 0% 100%; }
  100% { background-position: 0% 0%; }
}
@media (prefers-reduced-motion: reduce) {
  html[data-theme="basic"] .accent-bar::before, html[data-theme="basic"] .alert::before { animation: none; }
}

/* Original gradient hero / login (with blurred decorative circles) */
html[data-theme="basic"] .dash-hero {
  background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-mid) 60%, #1a6fcf 100%);
  border-bottom: none;
}
html[data-theme="basic"] .dash-hero::before {
  content: ''; position: absolute; right: -60px; top: -60px;
  width: 300px; height: 300px; background: rgba(255,255,255,0.04); border-radius: 50%; pointer-events: none;
}
html[data-theme="basic"] .dash-hero::after {
  content: ''; position: absolute; right: 60px; bottom: -80px;
  width: 200px; height: 200px; background: rgba(255,255,255,0.03); border-radius: 50%; pointer-events: none;
}
html[data-theme="basic"] .login-page { background: linear-gradient(145deg, var(--primary-dark) 0%, #0a1e4a 60%, #0D1B3E 100%); }
html[data-theme="basic"] .login-left {
  background: linear-gradient(145deg, var(--primary-dark) 0%, #0a1e4a 100%);
  border-right: none;
}
html[data-theme="basic"] .login-left::before {
  content: ''; position: absolute; right: -100px; top: -100px;
  width: 500px; height: 500px; background: rgba(26,79,168,0.2); border-radius: 50%;
}

/* Original heavier, tighter display type */
html[data-theme="basic"] .sidebar-brand-name,
html[data-theme="basic"] .page-header-row h1,
html[data-theme="basic"] .stat-num,
html[data-theme="basic"] .doc-brand-name,
html[data-theme="basic"] .doc-type-label,
html[data-theme="basic"] .login-left h1,
html[data-theme="basic"] .login-box-title,
html[data-theme="basic"] .dash-hero h1,
html[data-theme="basic"] .quick-name,
html[data-theme="basic"] .card-title,
html[data-theme="basic"] .modal-header h3 {
  font-weight: 800;
}
html[data-theme="basic"] .sidebar-brand-name,
html[data-theme="basic"] .page-header-row h1,
html[data-theme="basic"] .stat-num,
html[data-theme="basic"] .doc-brand-name,
html[data-theme="basic"] .doc-type-label {
  letter-spacing: -0.02em;
}
html[data-theme="basic"] .doc-brand-name { letter-spacing: -0.03em; }
html[data-theme="basic"] .login-left h1 { letter-spacing: -0.03em; }

/* Field value suggestions (autocomplete from historical data) */
.field-suggest-dropdown {
  z-index: 3000;
  background: #fff;
  border: 1px solid var(--gray-200);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  max-height: 260px;
  overflow-y: auto;
  padding: 4px;
}
.field-suggest-item {
  padding: .45rem .6rem;
  font-size: .82rem;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.field-suggest-item:hover,
.field-suggest-item.active {
  background: var(--gray-100);
}
/* ═══════════════════════════════════════════════════════════════════════
   INTERFACE DIRECTIONS — component details the token system can't express.
   Palette/radius/shadow/type all come from themeMoodPresets() via
   settingsCss(); what follows is only the shape of a few components.
   Keys: industrial (evolve) · console (strip colour) · chartroom (soften)
   ═══════════════════════════════════════════════════════════════════════ */

/* ── A · INDUSTRIAL ─────────────────────────────────────────────────────
   Status stops shouting: the 3px left border and the all-caps mono become
   a quiet dot in the body face. Primary actions get one real lift.        */
html[data-theme="industrial"] .badge {
  border-left: none;
  border-radius: 999px;
  padding: 0.22rem 0.6rem 0.22rem 0.5rem;
  gap: 0.4rem;
  font-family: var(--font-body);
  font-size: 0.72rem;
  letter-spacing: 0;
  text-transform: none;
}
html[data-theme="industrial"] .badge::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
/* Brass rail reads on the navy sidebar; a navy rail would not. */
html[data-theme="industrial"] .sidebar-nav a.active::before { background: var(--accent); width: 2px; }
html[data-theme="industrial"] .btn-primary { box-shadow: 0 1px 0 rgba(16,19,24,.10); }
html[data-theme="industrial"] .btn-primary:hover { box-shadow: 0 2px 6px rgba(16,19,24,.18); }

/* ── B · CONSOLE ────────────────────────────────────────────────────────
   Chrome goes colourless so state is the only thing on screen with hue.
   Borders replace shadows; rows tighten for more voyages per screen.      */
html[data-theme="console"] .badge {
  border-left: none;
  border: 1px solid currentColor;
  background: transparent !important;   /* beats .badge-* fills */
  padding: 0.18rem 0.45rem;
  gap: 0.4rem;
}
html[data-theme="console"] .badge::before {
  content: '';
  width: 5px; height: 5px;
  background: currentColor;
  flex: none;
}
html[data-theme="console"] .card { border: 1px solid var(--gray-200); box-shadow: none; }
html[data-theme="console"] .btn { font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.02em; }
html[data-theme="console"] .data-table th { background: transparent; border-bottom: 1px solid var(--gray-300); }
html[data-theme="console"] .data-table th,
html[data-theme="console"] .data-table td { padding-top: 0.55rem; padding-bottom: 0.55rem; }
/* Primary is near-black here, so the active rail must be light to show. */
html[data-theme="console"] .sidebar-nav a.active::before { background: rgba(255,255,255,.92); }
html[data-theme="console"] .sidebar-nav a.active { background: rgba(255,255,255,.13); }

/* ── C · CHARTROOM ──────────────────────────────────────────────────────
   More air and softer corners for demos. Nav rows become pills, card
   headers pick up a tint, brass carries the one primary action.          */
html[data-theme="chartroom"] .badge {
  border-left: none;
  border-radius: 999px;
  padding: 0.28rem 0.7rem 0.28rem 0.6rem;
  gap: 0.45rem;
  font-family: var(--font-body);
  font-size: 0.73rem;
  letter-spacing: 0;
  text-transform: none;
}
html[data-theme="chartroom"] .badge::before {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
html[data-theme="chartroom"] .card-header { background: var(--gray-50); }
html[data-theme="chartroom"] .data-table td { padding-top: 0.95rem; padding-bottom: 0.95rem; }
html[data-theme="chartroom"] .btn-primary { background: var(--accent); }
html[data-theme="chartroom"] .btn-primary:hover { background: var(--primary); }
/* Pill rows instead of a rail. */
html[data-theme="chartroom"] .sidebar-nav a {
  margin: 0 0.5rem;
  padding-left: 0.75rem;
  border-radius: var(--radius);
}
html[data-theme="chartroom"] .sidebar-nav a.active::before { display: none; }
html[data-theme="chartroom"] .sidebar-nav a.active { background: rgba(255,255,255,.14); }

/* ═══════════════════════════════════════════════════════════════════════
   DRIFTING ACCENT BAR — now on every theme, not just "basic".
   The left status bar carries a soft vertical gradient that drifts up and
   down, so a flagged card or alert reads as live rather than static.

   Specificity note: the original variant rules (.accent-bar.accent-warning
   ::before etc.) set `background` as a SHORTHAND, which resets
   background-image. So these rules must match that specificity — a plain
   .accent-bar::before rule would lose no matter how late it appears.
   ═══════════════════════════════════════════════════════════════════════ */

/* 1 — each variant declares where its colour comes from */
.accent-bar.accent-info::before    { --bar: var(--primary); }
.accent-bar.accent-success::before { --bar: var(--success); }
.accent-bar.accent-warning::before { --bar: var(--warning); }
.accent-bar.accent-danger::before  { --bar: var(--danger);  }
.accent-bar.accent-accent::before  { --bar: var(--accent);  }
.accent-bar.accent-custom::before  { --bar: var(--accent-color, var(--primary)); }
.alert-info::before                { --bar: var(--primary); }
.alert-success::before             { --bar: var(--success); }
.alert-warning::before             { --bar: var(--warning); }
.alert-error::before               { --bar: var(--danger);  }

/* 2 — one shared treatment, applied at variant specificity so it wins */
.accent-bar.accent-info::before,
.accent-bar.accent-success::before,
.accent-bar.accent-warning::before,
.accent-bar.accent-danger::before,
.accent-bar.accent-accent::before,
.accent-bar.accent-custom::before,
.alert-info::before,
.alert-success::before,
.alert-warning::before,
.alert-error::before {
  background-color: transparent;
  background-image: linear-gradient(180deg,
      var(--bar),
      color-mix(in srgb, var(--bar) 42%, #fff),
      var(--bar));
  background-size: 100% 300%;
  background-repeat: no-repeat;
  animation: accentDrift 3.4s ease-in-out infinite;
}

@keyframes accentDrift {
  0%, 100% { background-position: 0% 0%; }
  50%      { background-position: 0% 100%; }
}

/* Respect the OS setting on every theme (the old rule only covered "basic"). */
@media (prefers-reduced-motion: reduce) {
  .accent-bar::before,
  .alert::before,
  .accent-bar.accent-info::before,
  .accent-bar.accent-success::before,
  .accent-bar.accent-warning::before,
  .accent-bar.accent-danger::before,
  .accent-bar.accent-accent::before,
  .accent-bar.accent-custom::before,
  .alert-info::before,
  .alert-success::before,
  .alert-warning::before,
  .alert-error::before { animation: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   SHARED UI PATTERN  (see includes/ui.php)
   One place that owns list toolbars, breadcrumbs, form footers, row actions
   and empty states — so no module can drift into its own spacing or sizing.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Page transition ─────────────────────────────────────────────────────
   Applies to the PJAX content swap only. The sidebar, top bar, notification
   badge and the SSE stream are outside #pjax-container and never animate —
   navigation feels settled without the chrome moving. Timings pair with
   NAV_MIN_MS / NAV_FADE_MS in pjax.js. */
/* A pure crossfade. An earlier version also slid the content a few pixels;
   that movement was itself read as a "blink", so nothing moves now — only
   opacity changes, and the page geometry is identical throughout. */
#pjax-container{
  opacity:1;
  transition:opacity var(--t-slow) ease;
}
#pjax-container.pjax-leaving{
  opacity:0;
  transition:opacity var(--t-base) ease;  /* leave briskly, arrive gently */
  pointer-events:none;           /* no clicking rows on their way out */
  will-change:opacity;           /* only while animating — not a permanent layer */
}
#pjax-container.pjax-entering{
  opacity:0;
  transition:none;               /* pinned hidden until the frames below release it */
}
/* Someone who asked for less motion gets the content immediately, no fade,
   no waiting — pjax.js also skips the settling delay for them. */
@media (prefers-reduced-motion: reduce){
  #pjax-container,
  #pjax-container.pjax-leaving,
  #pjax-container.pjax-entering{
    transition:none;opacity:1;transform:none;
  }
}

/* No breadcrumb: it existed on some pages and not others, so the <h1> landed
   at a different height as you moved around. The title is now the first thing
   on every page, at the same y position, on all of them. */

/* Page header actions — primary action always right-aligned */
.ui-actions{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
.ui-head-meta{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-top:.35rem;font-size:.8rem;color:var(--gray-500)}

/* List toolbar — search left, filters after it, primary action right. Always.
   One row: controls share a baseline and only wrap when the viewport really
   cannot hold them. */
.ui-toolbar{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:1rem}
.ui-toolbar-left{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;flex:1 1 auto;min-width:0}
.ui-toolbar-right{display:flex;align-items:center;gap:.5rem;margin-left:auto;flex-wrap:nowrap;flex:0 0 auto}

/* Search reads as an input, not a shaded panel — it is the control people
   reach for first, so it carries the same visible border as the filters. */
.ui-search{display:flex;align-items:center;gap:.45rem;background:var(--surface);
  border:1.5px solid var(--gray-200);border-radius:var(--radius);padding:.34rem .7rem;
  flex:1 1 240px;max-width:340px;min-width:200px;transition:border-color var(--t-base),box-shadow var(--t-base)}
.ui-search:hover{border-color:var(--gray-300)}
.ui-search:focus-within{border-color:var(--primary);box-shadow:var(--focus-ring)}
.ui-search .material-symbols-rounded{font-size:17px;color:var(--gray-400);flex:none}
.ui-search input{border:0;background:none;outline:none;font:inherit;font-size:.83rem;color:var(--gray-800);width:100%;padding:.18rem 0}
.ui-search input::placeholder{color:var(--gray-400)}

/* Filters sit in a row beside the search and share its height. nowrap keeps
   two selects side by side instead of stacking them into a narrow column. */
.ui-filters{display:flex;align-items:center;gap:.5rem;flex-wrap:nowrap;flex:0 1 auto;min-width:0}
.ui-filters select,.ui-filters input{font-size:.82rem;padding:.42rem .6rem;border:1.5px solid var(--gray-200);
  border-radius:var(--radius);background:var(--surface);color:var(--gray-700);
  height:34px;min-width:130px;max-width:210px;width:auto;text-overflow:ellipsis}
.ui-filters select:hover,.ui-filters input:hover{border-color:var(--gray-300)}
.ui-filters select:focus,.ui-filters input:focus{outline:none;border-color:var(--primary);box-shadow:var(--focus-ring)}
.ui-toolbar .btn-sm{height:34px}

@media(max-width:1100px){ .ui-filters{flex-wrap:wrap} }
@media(max-width:720px){
  .ui-toolbar-right{margin-left:0;width:100%;flex-wrap:wrap}
  .ui-toolbar-right .btn{flex:1;justify-content:center}
  .ui-search{min-width:0;max-width:none;flex:1 1 100%}
  .ui-filters{flex:1 1 100%}
  .ui-filters select{flex:1;max-width:none}
}

/* Tables — identical row rhythm everywhere */
.ui-table table{width:100%}
.ui-table .data-table td{vertical-align:middle}

/* Row actions — view, edit, delete, in that order, on every table */
.ui-row-actions{display:flex;align-items:center;gap:.15rem;justify-content:flex-end}

/* The actions column is ALWAYS hard right — header cell included. Detected by
   content (:has) rather than by a class, so tables whose last column is real
   data keep their own alignment and nothing had to be hand-tagged. */
.data-table:has(td:last-child .ui-row-actions) th:last-child,
.data-table:has(td:last-child .ui-row-actions) td:last-child,
.data-table:has(td:last-child .btn-icon) th:last-child,
.data-table:has(td:last-child .btn-icon) td:last-child{text-align:right;white-space:nowrap}
.data-table:has(td:last-child .ui-row-actions) td:last-child .ui-row-actions,
.data-table:has(td:last-child .btn-icon) td:last-child .ui-row-actions{justify-content:flex-end}

/* Form footer — Cancel then Save, Save always primary and last */
.ui-form-actions{display:flex;align-items:center;gap:.75rem;margin-top:1.25rem;padding-top:1rem;
  border-top:1px solid var(--gray-100);flex-wrap:wrap}
.ui-form-left{margin-right:auto}
.ui-form-right{display:flex;align-items:center;gap:.5rem;margin-left:auto}
@media(max-width:560px){
  .ui-form-right{width:100%}
  .ui-form-right .btn{flex:1;justify-content:center}
}


.ui-card-aside{display:flex;align-items:center;gap:.5rem;font-size:.8rem;color:var(--gray-500)}

/* Cards now span their container instead of being capped at ~680px, so the
   surface fills the page. Field rows still cap at a readable measure — a text
   input 900px wide is no easier to use than one at 500px. */
.card-body > .form-grid-2,
.card-body > .form-grid-3,
.card-body > form > .form-grid-2,
.card-body > form > .form-grid-3{max-width:1180px}

/* Settings form — cards flow into as many columns as the viewport affords, so
   a tab of narrow cards never strands half the screen. Anything that already
   owns its layout spans the full width instead of being packed into a column. */
/* ── Settings tab strip ───────────────────────────────────
   A wrapping grid, not a scroller. Every tab is one grid cell, so they are all
   exactly the same width and height no matter which row they land on, and a
   fourteenth tab simply flows onto the next line without anything being
   resized or hidden behind a scrollbar.
   The version before this was a single scrolling row — tabs off the right edge
   were invisible until you dragged, which read as broken. The version before
   THAT wrapped but shared one border-bottom, so the active marker only existed
   on row one. The pill carries its own state, so wrapping costs nothing. */
.settings-tabs{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(176px,1fr));
  gap:.35rem;margin:0 0 1.5rem;padding:.35rem;
  background:var(--gray-50);border:1px solid var(--gray-200);border-radius:var(--radius-lg);
}
.settings-tab{
  display:flex;align-items:center;gap:.4rem;min-width:0;min-height:42px;
  padding:.5rem .75rem;border-radius:var(--radius);
  font-size:.82rem;font-weight:600;line-height:1.2;text-decoration:none;
  color:var(--gray-500);transition:background var(--t-base),color var(--t-base);
}
/* The label ellipsises rather than wrapping, so no tab is ever taller than the
   rest — equal height is what makes a wrapped grid look deliberate. */
.settings-tab > span:not(.material-symbols-rounded),
.settings-tab{white-space:nowrap}
.settings-tab{overflow:hidden;text-overflow:ellipsis}
.settings-tab .material-symbols-rounded{flex:0 0 auto}
.settings-tab:hover{background:var(--gray-100);color:var(--gray-700)}
.settings-tab.is-active{background:var(--primary);color:#fff;box-shadow:var(--shadow-sm)}
.settings-tab.is-active .material-symbols-rounded{color:#fff}
@media(max-width:560px){ .settings-tabs{grid-template-columns:repeat(auto-fit,minmax(130px,1fr))} }

/* ── Navigation menu editor ───────────────────────────────
   Group headers read as section headers, and every row is the same height with
   the same hit target, so a 55-row list scans as a list instead of a pile. */
.nav-editor-group-title{
  display:flex;align-items:center;gap:.5rem;
  font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
  color:var(--gray-500);margin-bottom:.6rem;
}
.nav-editor-group-title::after{content:"";flex:1;height:1px;background:var(--gray-200)}
.nav-editor-subgroup-title{
  font-size:.68rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  color:var(--gray-400);margin:0 0 .35rem;
}
.nav-editor-row{min-height:42px;transition:border-color var(--t-base),box-shadow var(--t-base)}
.nav-editor-row:hover{border-color:var(--gray-300);box-shadow:var(--shadow-sm)}
.nav-editor-row:hover .drag-handle{color:var(--gray-500)}
.nav-editor-row .drag-handle{transition:color var(--t-base)}

/* Cards stretch to their row rather than sitting at the top of it. With
   align-items:start a short card next to a tall one left a ragged void; now the
   row has one height and the slack lives inside the cards, where it reads as
   padding instead of a hole. */
.settings-form{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));
  gap:1.25rem;align-items:stretch}
.settings-form > .card{max-width:none;margin-top:0;margin-bottom:0;min-width:0}
.settings-form > .grid-2,
.settings-form > .grid-3,
.settings-form > .alert,
.settings-form > .settings-actions,
.settings-form > .settings-wide,
.settings-form > table,
.settings-form > .table-wrap{grid-column:1/-1}
.settings-form > script,.settings-form > style{display:none}

/* A card with nothing to sit beside it spans instead of leaving a gap.
   The Theme tab was the worst case: "Branding Mood" took one of three columns
   and the very next sibling was a full-width block, so two columns' worth of
   nothing sat next to it — while the mood presets inside were being clipped for
   want of the width right there. Both problems, one rule. */
.settings-form > .card:has(+ .grid-2),
.settings-form > .card:has(+ .grid-3),
.settings-form > .card:has(+ .settings-wide),
.settings-form > .card:has(+ .table-wrap),
.settings-form > .card:has(+ table),
.settings-form > .card:has(+ .alert),
.settings-form > .card:has(+ .settings-actions),
.settings-form > .card:has(+ .ui-form-actions),
.settings-form > .card:only-of-type{grid-column:1/-1}
.settings-actions{grid-column:1/-1;margin-top:.25rem}
/* One column once two no longer fit — same breakpoint the cards imply. */
@media(max-width:920px){ .settings-form{grid-template-columns:1fr} }

/* Settings toggle row — full width, state on the right, detail collapsed.
   Replaces the pattern of a narrow card stranded beside dead space. */
.setting-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}
.setting-row-main{display:flex;align-items:flex-start;gap:.75rem;cursor:pointer;flex:1 1 320px;min-width:0}
.setting-row-title{display:flex;align-items:center;gap:.4rem;font-weight:600;font-size:.88rem;color:var(--gray-800)}
.setting-row-desc{display:block;font-size:.78rem;color:var(--gray-500);margin-top:.15rem;line-height:1.5}
.setting-row-aside{display:flex;align-items:center;gap:.5rem;flex:none}
.setting-row-help{border-top:1px solid var(--gray-100);font-size:.78rem;color:var(--gray-500)}
.setting-row-help summary{cursor:pointer;padding:.6rem 1.25rem;font-weight:600;color:var(--gray-600);
  list-style:none;display:flex;align-items:center;gap:.35rem;user-select:none}
.setting-row-help summary::-webkit-details-marker{display:none}
.setting-row-help summary::before{content:"chevron_right";font-family:'Material Symbols Rounded';
  font-size:18px;line-height:1;transition:transform var(--t-base)}
.setting-row-help[open] summary::before{transform:rotate(90deg)}
.setting-row-help summary:hover{color:var(--primary)}
.setting-row-help > div{padding:0 1.25rem 1rem 2.6rem;line-height:1.6}

/* Switch — an on/off control for a setting that takes effect as a state,
   where a tick ("I have marked this") reads wrong.

   Built on a real <input type="checkbox">, only visually replaced: it keeps
   keyboard focus, space to toggle, form submission and screen-reader
   semantics, and it still renders as its checked state before any JS runs.
   Lives here rather than in a page so the next screen that needs one reuses
   it instead of inventing a second look. */
.switch{position:relative;display:inline-flex;flex-shrink:0;width:34px;height:19px;cursor:pointer}
.switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
.switch span{position:absolute;inset:0;background:var(--gray-300);border-radius:999px;
  transition:background var(--t-slow)}
.switch span::before{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;
  background:#fff;border-radius:50%;box-shadow:0 1px 2px rgba(12,33,46,.28);transition:transform var(--t-slow)}
.switch input:checked + span{background:var(--success)}
.switch input:checked + span::before{transform:translateX(15px)}
.switch input:focus-visible + span{box-shadow:var(--focus-ring)}
.switch input:disabled + span{opacity:.5;cursor:not-allowed}

/* Shared delete confirmation — one dialog for the whole product */
.ui-confirm-backdrop{position:fixed;inset:0;background:rgba(10,20,30,.5);z-index:9998;
  display:flex;align-items:center;justify-content:center;padding:1rem}
.ui-confirm{background:var(--surface);border-radius:var(--radius-xl);box-shadow:var(--shadow-lg);
  max-width:420px;width:100%;overflow:hidden}
.ui-confirm-head{display:flex;align-items:center;gap:.65rem;padding:1.1rem 1.25rem .35rem}
.ui-confirm-head .material-symbols-rounded{color:var(--danger);font-size:24px}
.ui-confirm-head h3{margin:0;font-size:1rem;font-family:var(--font-display);color:var(--gray-800)}
.ui-confirm-body{padding:.35rem 1.25rem 1.1rem;font-size:.87rem;color:var(--gray-600);line-height:1.6}
.ui-confirm-foot{display:flex;justify-content:flex-end;gap:.5rem;padding:.9rem 1.25rem;
  background:var(--gray-50);border-top:1px solid var(--gray-100)}

/* ── Card body height cap ─────────────────────────────────
   Modelled on logs_viewer's log pane (max-height + overflow:auto), promoted to
   a shared rule so a long card scrolls inside itself instead of pushing the
   rest of the page — SMTP logs, audit trails, liner lists and the like.

   Deliberately opt-OUT rather than opt-in, so a new long card behaves without
   anyone remembering to ask. The exclusions below are the cases where a scroll
   container is actively wrong:
     .mbx-card      — the mailbox pane already manages its own scroll regions
     :has(details)  — a popper (the attach-document picker) would be clipped by
                      the scroll container instead of overflowing the card
     :has(#navEditor), :has(.nav-sortable-container)
                    — dragging an item to a target that is scrolled out of view
                      is far worse than a tall page
     :has(.modal)   — modals size themselves
     .card-tall     — explicit escape hatch for anything else */
.card-body{
  max-height:600px;overflow-y:auto;overflow-x:hidden;
  scrollbar-width:thin;scrollbar-color:var(--gray-300) transparent;
  /* Chains to the page at the ends. `contain` looked tidier but trapped the
     wheel: any body overflowing by even a pixel — no visible scrollbar —
     swallowed every scroll event, so the page would not move until the
     pointer was dragged off the card. Reaching the end of a pane and
     continuing down the page is what people expect. */
  overscroll-behavior:auto;
  scroll-behavior:smooth;
}
.card-body::-webkit-scrollbar{width:8px}
.card-body::-webkit-scrollbar-track{background:transparent}
.card-body::-webkit-scrollbar-thumb{background:var(--gray-300);border-radius:4px;
  border:2px solid transparent;background-clip:content-box}
.card-body::-webkit-scrollbar-thumb:hover{background:var(--gray-400);background-clip:content-box}

/* Exclusions — a scroll container here would clip or trap something. */
.card-tall > .card-body,
.mbx-card .card-body,
/* Forms are never capped. A height cap suits things you READ — logs, lists,
   long reference tables — where scrolling past is the normal case. A form is
   something you WORK THROUGH: capping it buries the submit button in a nested
   scroller, so you reach the bottom of the page and the Save button is not
   there. Long forms should make the page long. */
.card-body:has(form),
.card-body:has(input:not([type=search])),
.card-body:has(textarea),
.card-body:has(details),
.card-body:has(#navEditor),
.card-body:has(.nav-sortable-container),
.card-body:has(.modal),
/* An iframe is already a scrolling viewport of its own. Capping the body that
   holds it does not shorten the document inside — it just parks a second
   scrollbar next to the first, so reaching the foot of the page means
   scrolling the card down and then the frame down again. Only a frame that IS
   the body's content qualifies; an iframe nested in other markup (the mailbox
   message list) is one item among many and the cap still applies there. */
.card-body:has(> iframe),
/* A .table-wrap is the same case as an iframe, and by far the commonest one:
   it is ALREADY a capped, scrolling viewport with its own sticky header. Capping
   the body around it does not shorten anything — it parks a second scrollbar
   beside the first, and because the outer container also scrolls, the table's
   sticky header sticks to the CARD rather than to the table, so the first row
   slides underneath it and is clipped. That is the "why are there two
   scrollbars, and why is the top row cut in half" that this shape produced on
   81 pages. The inner cap still does the real work. */
.card-body:has(> .table-wrap),
.card-body:has(select[multiple]){max-height:none;overflow:visible}

/* A capped body sits flush to the card edge, so its own padding has to hold the
   content off the scrollbar rather than the card doing it. */
.card-body{scroll-padding-block:.5rem}

@media(max-width:700px){ .card-body{max-height:70vh} }

/* ── Table height cap ─────────────────────────────────────
   .table-wrap already scrolled horizontally for wide tables; it now caps height
   too, so a long log (SMTP email log, audit trail, webhook deliveries) scrolls
   inside its card instead of running the page to several thousand pixels.

   The header row sticks while the body scrolls — without that, capping a table
   is a downgrade: you scroll two screens down and no longer know which column
   is which. This is what makes it usable rather than merely shorter. */
.table-wrap{
  max-height:600px;overflow:auto;
  scrollbar-width:thin;scrollbar-color:var(--gray-300) transparent;
  overscroll-behavior:auto;   /* see .card-body — contain trapped the wheel */
}
.table-wrap::-webkit-scrollbar{width:8px;height:8px}
.table-wrap::-webkit-scrollbar-track{background:transparent}
.table-wrap::-webkit-scrollbar-thumb{background:var(--gray-300);border-radius:4px;
  border:2px solid transparent;background-clip:content-box}
.table-wrap::-webkit-scrollbar-thumb:hover{background:var(--gray-400);background-clip:content-box}

.table-wrap > .data-table > thead > tr > th,
.table-wrap > table > thead > tr > th{
  position:sticky;top:0;z-index:2;
  background:var(--gray-50);
  /* A sticky cell cannot carry a border that survives scrolling — the border
     scrolls away with the row box. box-shadow is painted on the sticky layer. */
  box-shadow:inset 0 -1px 0 var(--gray-200);
}

.table-tall > .table-wrap,
.table-wrap.table-tall,
.table-wrap:has(input),
.table-wrap:has(select),
.table-wrap:has(textarea){max-height:none;overflow-x:auto}

@media(max-width:700px){ .table-wrap{max-height:70vh} }

/* ── B2B customers: type picker, contact permissions, modal ──────────────
   Added with the Retail/Business customer split. Uses the existing tokens
   only — no new colours — so it inherits any theme change automatically. */

.badge-muted{background:var(--gray-100);color:var(--gray-600)}

/* Retail vs Business chooser on the customer form */
.ctype-picker{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.ctype-opt{display:flex;align-items:flex-start;gap:.6rem;padding:.75rem .875rem;cursor:pointer;
  border:1.5px solid var(--gray-200);border-radius:var(--radius);background:var(--surface);
  transition:border-color var(--t-base),background var(--t-base)}
.ctype-opt:hover{border-color:var(--primary-light)}
/* .is-on is the server-rendered initial state; :has() keeps it live as the
   user clicks, without a JS handler. main.css already relies on :has(). */
.ctype-opt.is-on,
.ctype-opt:has(input:checked){border-color:var(--primary);background:var(--primary-light)}
.ctype-opt:not(:has(input:checked)){border-color:var(--gray-200);background:var(--surface)}
.ctype-opt input{margin-top:.15rem}
.ctype-opt>.material-symbols-rounded{color:var(--primary);font-size:1.25rem;flex-shrink:0}
.ctype-txt{display:flex;flex-direction:column;gap:.15rem;min-width:0}
.ctype-txt strong{font-size:.85rem}
.ctype-txt em{font-style:normal;font-size:.72rem;color:var(--gray-500);line-height:1.35}
@media(max-width:700px){ .ctype-picker{grid-template-columns:1fr} }

/* Checkbox rows shared by the flags block and the permission grid */
.chk{display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-size:.82rem}
.chk input{margin-top:.1rem}
.chk span{display:flex;flex-direction:column;gap:.1rem;min-width:0}
.ctype-flags{display:flex;gap:1.25rem;flex-wrap:wrap;padding:.5rem 0}

.perm-group{margin-bottom:1rem}
.perm-group-head{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
  color:var(--gray-500);padding-bottom:.35rem;margin-bottom:.5rem;border-bottom:1px solid var(--gray-200)}
.perm-row{padding:.3rem 0}

/* Lightweight modal — matches the app's card language rather than importing
   a dialog library for one form. */
.ui-modal{position:fixed;inset:0;z-index:900;background:rgba(15,23,42,.55);
  display:flex;align-items:flex-start;justify-content:center;padding:2rem 1rem;overflow-y:auto}
.ui-modal[hidden]{display:none}
.ui-modal-box{background:var(--surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);
  width:100%;max-width:640px;max-height:calc(100vh - 4rem);display:flex;flex-direction:column}
.ui-modal-box form{display:flex;flex-direction:column;min-height:0}
.ui-modal-head{display:flex;align-items:center;justify-content:space-between;
  padding:.875rem 1.125rem;border-bottom:1px solid var(--gray-200)}
.ui-modal-body{padding:1.125rem;overflow-y:auto}
.ui-modal-foot{display:flex;justify-content:flex-end;gap:.5rem;
  padding:.875rem 1.125rem;border-top:1px solid var(--gray-200)}

/* ── Sailing schedule search ─────────────────────────────────────────────
   Result-row layout follows the liner-schedule convention shippers already
   read fluently: ETD · transit · ETA across the top, ports beneath, booking
   gates on the right. Tokens only — inherits any theme change. */

.sch-row{display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap}
.sch-loc{flex:1 1 220px;margin:0}
.sch-date{flex:0 0 170px;margin:0}
.sch-swap{margin-bottom:.35rem;flex-shrink:0}
.sch-actions{display:flex;gap:.5rem;margin-bottom:.05rem}
.sch-adv{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--gray-200)}
.sch-adv[hidden]{display:none}
.sch-haul{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.sch-haul>div{display:flex;flex-direction:column;gap:.4rem}
.sch-flags{display:flex;gap:1.25rem;align-items:center;flex-wrap:wrap;flex:1 1 260px;padding-bottom:.4rem}
.sch-toggle{display:inline-flex;align-items:center;gap:.3rem;margin-top:.875rem;
  font-size:.82rem;font-weight:600;color:var(--primary);text-decoration:none}
.sch-toggle:hover{text-decoration:underline}

.sch-resbar{display:flex;justify-content:space-between;align-items:center;gap:1rem;
  flex-wrap:wrap;margin:1rem 0 .625rem;font-size:.85rem}
.sch-facets{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}
.sch-facet{display:inline-flex;align-items:center;gap:.25rem;padding:.3rem .7rem;border-radius:999px;
  border:1px solid var(--gray-300);background:var(--surface);color:var(--gray-700);
  font-size:.76rem;font-weight:600;text-decoration:none}
.sch-facet:hover{border-color:var(--primary)}
.sch-facet.is-on{background:var(--primary);border-color:var(--primary);color:#fff}
.sch-facet.sch-clear{border-style:dashed;color:var(--gray-500)}

.sch-card{margin-bottom:.875rem;overflow:hidden}
.sch-main{display:grid;grid-template-columns:1fr 190px 190px;gap:0}
.sch-journey{padding:1rem 1.125rem;border-right:1px solid var(--gray-200)}
.sch-jhead{display:flex;justify-content:space-between;align-items:center;gap:1rem}
.sch-date-big{font-size:.95rem;font-weight:700;color:var(--gray-900)}
.sch-transit{display:inline-flex;align-items:center;gap:.25rem;padding:.15rem .6rem;border-radius:999px;
  background:var(--info-light);color:var(--info);font-size:.74rem;font-weight:600;white-space:nowrap}
.sch-line{position:relative;height:10px;margin:.5rem 0}
.sch-line::before{content:"";position:absolute;top:4px;left:4px;right:4px;height:2px;background:var(--gray-200)}
.sch-line span{position:absolute;top:1px;width:8px;height:8px;border-radius:50%;background:var(--gray-300)}
.sch-line span:first-child{left:0}
.sch-line span:last-child{right:0}
.sch-jports{display:flex;justify-content:space-between;gap:1rem}
.sch-hl{font-size:.7rem;color:var(--gray-500)}
.sch-port{font-size:.85rem;font-weight:700;color:var(--gray-900)}
.sch-tags{display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.75rem;padding-top:.625rem;
  border-top:1px solid var(--gray-100)}
.sch-tag{display:inline-flex;align-items:center;gap:.25rem;padding:.2rem .55rem;border-radius:var(--radius);
  border:1px solid var(--gray-200);background:var(--gray-50);font-size:.72rem;color:var(--gray-700)}
.sch-tag-svc{border-color:var(--accent);background:var(--accent-light);color:var(--accent);font-weight:700}
.sch-tag-ded{border-color:var(--info);background:var(--info-light);color:var(--info);font-weight:600}

.sch-cuts{padding:1rem .875rem;border-right:1px solid var(--gray-200);
  display:flex;flex-direction:column;gap:.6rem;justify-content:center}
.sch-cuts span{display:block;font-size:.7rem;color:var(--gray-500)}
.sch-cuts strong{font-size:.82rem;color:var(--gray-900)}
.sch-cta{padding:1rem;display:flex;flex-direction:column;gap:.4rem;justify-content:center}
.sch-cta .btn{width:100%;justify-content:center}

.sch-detail{border-top:1px solid var(--gray-200);background:var(--gray-50);padding:1rem 0 1.25rem}
.sch-detail[hidden]{display:none}
.sch-detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:1rem 1.125rem 0}
.sch-stop,.sch-hop{display:flex;gap:.75rem;align-items:flex-start;padding:.5rem .75rem;
  background:var(--surface);border-radius:var(--radius);margin-bottom:.4rem}
.sch-hop{background:transparent;padding-left:.75rem}
.sch-pin{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;
  border-radius:50%;background:var(--primary);color:#fff;flex-shrink:0}
.sch-pin-ship{background:var(--gray-400)}
.sch-stop-body{min-width:0}
.sch-panel-head{display:flex;align-items:center;gap:.4rem;font-weight:700;font-size:.85rem;
  color:var(--gray-900);margin-bottom:.5rem}
.sch-cut-table{width:100%;border-collapse:collapse;font-size:.8rem}
.sch-cut-table td{padding:.32rem 0;color:var(--gray-700)}
.sch-pill{display:inline-block;padding:.15rem .6rem;border-radius:var(--radius);
  background:var(--gray-100);color:var(--gray-700);font-size:.74rem;white-space:nowrap}

@media(max-width:900px){
  .sch-main{grid-template-columns:1fr}
  .sch-journey,.sch-cuts{border-right:none;border-bottom:1px solid var(--gray-200)}
  .sch-cuts{flex-direction:row;flex-wrap:wrap;gap:1.25rem}
  .sch-detail-grid{grid-template-columns:1fr}
  .sch-haul{grid-template-columns:1fr}
}

/* ── Voyage revenue projection ───────────────────────────────────────── */
.rev-head{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem}
.rev-head>div{display:flex;flex-direction:column;gap:.15rem;min-width:0}
.rev-head span{font-size:.7rem;color:var(--gray-500);text-transform:uppercase;letter-spacing:.03em}
.rev-head strong{font-size:.88rem;color:var(--gray-900)}

.rev-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.875rem;margin-bottom:1rem}
.rev-card{background:var(--surface);border:1px solid var(--gray-200);border-radius:var(--radius-lg);
  padding:1rem 1.125rem;display:flex;flex-direction:column;gap:.2rem}
.rev-card span{font-size:.72rem;color:var(--gray-500);text-transform:uppercase;letter-spacing:.03em;font-weight:600}
.rev-card strong{font-size:1.5rem;font-weight:700;color:var(--gray-900);line-height:1.15}
.rev-card em{font-style:normal;font-size:.72rem;color:var(--gray-500)}
.rev-projected>strong{color:var(--primary)}
.rev-loss>strong{color:var(--danger)}
.rev-pos{color:var(--success)}
.rev-neg{color:var(--danger)}

.rev-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem}
.rev-stats>div{display:flex;flex-direction:column;gap:.15rem}
.rev-stats span{font-size:.72rem;color:var(--gray-500)}
.rev-stats strong{font-size:1.05rem;color:var(--gray-900)}

.rev-bar{height:7px;border-radius:999px;background:var(--gray-200);overflow:hidden}
.rev-bar-fill{height:100%;border-radius:999px;background:var(--primary);transition:width var(--t-slower)}
.rev-bar-fill.is-good{background:var(--success)}
.rev-bar-fill.is-ok{background:var(--info)}
.rev-bar-fill.is-low{background:var(--warning)}
.rev-bar-fill.is-over{background:var(--danger)}
tr.rev-over td{background:var(--danger-light)}

.rev-chart-row{display:flex;align-items:center;gap:.75rem;margin-bottom:.45rem}
.rev-chart-label{flex:0 0 165px;font-size:.78rem;color:var(--gray-700)}
.rev-chart-track{flex:1;height:20px;background:var(--gray-100);border-radius:var(--radius);overflow:hidden}
.rev-chart-bar{height:100%;border-radius:var(--radius);background:var(--primary)}
.rev-b-confirmed{background:var(--success)}
.rev-b-cons{background:var(--warning)}
.rev-b-exp{background:var(--primary)}
.rev-b-opt{background:var(--info)}
.rev-b-pot{background:var(--gray-400)}
.rev-b-target{background:repeating-linear-gradient(45deg,var(--accent),var(--accent) 6px,transparent 6px,transparent 12px)}
.rev-chart-val{flex:0 0 130px;text-align:right;font-size:.8rem;font-weight:600;color:var(--gray-900)}

.rev-block{border:1px solid var(--gray-200);border-radius:var(--radius);margin-bottom:.5rem;overflow:hidden}
.rev-block-head{display:flex;align-items:center;gap:.6rem;width:100%;padding:.7rem .875rem;
  background:var(--gray-50);border:none;cursor:pointer;text-align:left;font-family:inherit}
.rev-block-head:hover{background:var(--gray-100)}
.rev-block-label{font-weight:600;font-size:.85rem;color:var(--gray-900)}
.rev-block-amt{margin-left:auto;font-size:.95rem;color:var(--gray-900)}
.rev-block-body{padding:.5rem .875rem .875rem}
.rev-block-body[hidden]{display:none}
.rev-total{display:flex;justify-content:space-between;align-items:baseline;padding:.875rem;
  margin-top:.5rem;border-top:2px solid var(--gray-300)}
.rev-total span{font-weight:600;color:var(--gray-700)}
.rev-total strong{font-size:1.3rem;color:var(--primary)}

@media(max-width:700px){
  .rev-chart-label{flex-basis:110px;font-size:.72rem}
  .rev-chart-val{flex-basis:95px;font-size:.72rem}
}

/* Revenue Forecast summary card on the voyage detail page */
.vr-mini{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.5rem}
.vr-mini>div{display:flex;flex-direction:column;gap:.1rem;min-width:0}
.vr-mini span{font-size:.65rem;color:var(--gray-500);text-transform:uppercase;letter-spacing:.03em}
.vr-mini strong{font-size:.8rem;color:var(--gray-900);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vr-mini-hi{color:var(--primary)}
@media(max-width:1200px){ .vr-mini{grid-template-columns:1fr} }

/* Historical cost picker */
.rev-hist-opt{display:flex;align-items:flex-start;gap:.6rem;padding:.7rem .8rem;margin-bottom:.4rem;
  border:1.5px solid var(--gray-200);border-radius:var(--radius);cursor:pointer;background:var(--surface)}
.rev-hist-opt:hover{border-color:var(--primary-light)}
.rev-hist-opt:has(input:checked){border-color:var(--primary);background:var(--primary-light)}
.rev-hist-opt input{margin-top:.2rem}
.rev-hist-opt>span{display:flex;flex-direction:column;gap:.15rem;min-width:0;flex:1}
.rev-hist-opt em{font-style:normal;line-height:1.4}
.rev-hist-lines{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.3rem}
.rev-hist-lines>span{font-size:.68rem;padding:.12rem .45rem;border-radius:var(--radius);
  background:var(--gray-100);color:var(--gray-600);white-space:nowrap}
.rep-saved{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center;margin-bottom:.625rem}
.rep-chip-on{background:var(--primary);color:#fff}
.rep-chip-del{background:var(--danger-light);color:var(--danger);border-color:var(--danger);cursor:pointer;font-family:inherit}

/* ══ Reports & Analytics ═══════════════════════════════════════════════════
   Enterprise-BI density: compact controls, generous whitespace around data,
   numbers right-aligned and tabular so columns line up when scanning. */

/* ── Filter bar: a compact inline grid, not a stack of full-width rows ── */
.rep-filterbar{display:grid;gap:.625rem .75rem;align-items:end;
  grid-template-columns:repeat(auto-fit,minmax(148px,1fr))}
.rep-filterbar .form-group{margin:0;min-width:0}
.rep-filterbar label{font-size:.66rem;text-transform:uppercase;letter-spacing:.04em;
  color:var(--gray-500);font-weight:700;margin-bottom:.2rem;display:block}
.rep-filterbar select,.rep-filterbar input{height:34px;font-size:.8rem;padding:.25rem .5rem}
.rep-filter-actions{display:flex;gap:.4rem;align-items:end;grid-column:1/-1;justify-content:flex-end}
@media(min-width:900px){ .rep-filter-actions{grid-column:auto;justify-content:flex-start} }

/* ── Chips ─────────────────────────────────────────────────────────────── */
.rep-saved{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center;margin-bottom:.625rem}
.rep-chips{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center;margin-bottom:.875rem}
.rep-chip{display:inline-flex;align-items:center;gap:.25rem;padding:.24rem .62rem;border-radius:999px;
  background:var(--surface);color:var(--gray-700);border:1px solid var(--gray-300);
  font-size:.72rem;font-weight:600;text-decoration:none;line-height:1.5;transition:all var(--t-fast)}
.rep-chip:hover{border-color:var(--primary);color:var(--primary)}
.rep-chip .material-symbols-rounded{font-size:14px}
.rep-chip-on{background:var(--primary);border-color:var(--primary);color:#fff}
.rep-chip-on:hover{color:#fff}
.rep-chip-drill{background:var(--info-light);color:var(--info);border-color:var(--info)}
.rep-chip-del{background:transparent;color:var(--danger);border-color:var(--gray-300);cursor:pointer;font-family:inherit}
.rep-chip-del:hover{border-color:var(--danger);background:var(--danger-light)}

/* ── KPI cards ─────────────────────────────────────────────────────────── */
/* Flex, not grid: with 7 metrics a 6-column grid strands the last card beside
   a row of empty space. Flex lets whatever lands on the final row grow to fill
   it, so there is never a gap regardless of how many metrics are selected. */
.rep-kpis{display:flex;flex-wrap:wrap;gap:.75rem;margin-bottom:.5rem}
.rep-kpi{flex:1 1 168px}
.rep-kpi-foot{font-size:.7rem;color:var(--gray-500);margin:0 0 1rem}
.rep-kpi{background:var(--surface);border:1px solid var(--gray-200);border-radius:var(--radius-lg);
  padding:.9rem 1rem;display:flex;flex-direction:column;gap:.3rem;min-width:0;position:relative;
  transition:box-shadow var(--t-base),border-color var(--t-base)}
.rep-kpi{flex:1 1 168px}
.rep-kpi:hover{box-shadow:var(--shadow-sm);border-color:var(--gray-300)}
.rep-kpi>span{font-size:.65rem;color:var(--gray-500);text-transform:uppercase;
  letter-spacing:.05em;font-weight:700;line-height:1}
/* No ellipsis: the value is abbreviated in PHP, so it always fits. Clipping a
   headline number makes it unreadable, which is worse than rounding it. */
.rep-kpi>strong{font-size:1.3rem;font-weight:700;color:var(--gray-900);line-height:1.15;
  font-variant-numeric:tabular-nums;white-space:nowrap;cursor:help}
.rep-kpi>em{font-style:normal;font-size:.72rem;font-weight:600;line-height:1.2;
  display:inline-flex;align-items:center;gap:.22rem;white-space:nowrap;cursor:help}
.rep-kpi>em::before{content:"";width:0;height:0;flex-shrink:0}
.rep-kpi>em.rep-up::before{border-left:4px solid transparent;border-right:4px solid transparent;
  border-bottom:5px solid var(--success)}
.rep-kpi>em.rep-down::before{border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid var(--danger)}
.rep-kpi>em.rep-up{color:var(--success)}
.rep-kpi>em.rep-down{color:var(--danger)}
.rep-kpi>em.rep-flat{color:var(--gray-400)}
.rep-up{color:var(--success)} .rep-down{color:var(--danger)} .rep-flat{color:var(--gray-400)}

/* ── Charts ────────────────────────────────────────────────────────────── */
.rep-chart-wrap{position:relative;padding-top:.25rem}
.rep-chart{width:100%;height:230px;display:block;overflow:visible}
.rep-axis{display:flex;justify-content:space-between;font-size:.68rem;color:var(--gray-500);
  margin-top:.35rem;font-variant-numeric:tabular-nums}
.rep-legend{display:flex;gap:1rem;flex-wrap:wrap;margin-top:.6rem;font-size:.72rem;color:var(--gray-600)}
.rep-legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:.35rem;vertical-align:middle}

/* Column chart, used when there are too few points for a meaningful line */
.rep-cols{display:flex;align-items:flex-end;justify-content:center;gap:1.25rem;height:210px;
  padding:.5rem .25rem 0;border-bottom:1px solid var(--gray-200)}
.rep-col{flex:1 1 64px;max-width:132px;display:flex;flex-direction:column;justify-content:flex-end;
  align-items:center;gap:.35rem;min-width:0;height:100%}
.rep-col-bar{width:100%;max-width:56px;background:linear-gradient(180deg,var(--primary-light),var(--primary));
  border-radius:5px 5px 0 0;min-height:3px;transition:opacity var(--t-base)}
.rep-col:hover .rep-col-bar{opacity:.82}
.rep-col-val{font-size:.7rem;font-weight:700;color:var(--gray-900);font-variant-numeric:tabular-nums;
  white-space:nowrap}
.rep-col-lbl{font-size:.66rem;color:var(--gray-500);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;max-width:100%}

/* ── Horizontal bar lists ──────────────────────────────────────────────── */
.rep-bars{display:flex;flex-direction:column;gap:.55rem}
.rep-bar-row{display:grid;grid-template-columns:minmax(90px,1.1fr) 2fr auto;gap:.7rem;align-items:center;
  text-decoration:none;color:inherit;padding:.15rem 0;border-radius:var(--radius)}
.rep-bar-row:hover .rep-bar-label{color:var(--primary)}
.rep-bar-row:hover .rep-bar-fill{filter:brightness(1.08)}
.rep-bar-label{font-size:.76rem;color:var(--gray-700);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;font-weight:500}
.rep-bar-track{height:10px;background:var(--gray-100);border-radius:999px;overflow:hidden}
.rep-bar-fill{display:block;height:100%;border-radius:999px;
  background:linear-gradient(90deg,var(--primary-light),var(--primary))}
.rep-bar-val{font-size:.75rem;font-weight:700;color:var(--gray-900);white-space:nowrap;
  font-variant-numeric:tabular-nums;cursor:help;min-width:64px;text-align:right}

/* ── Panels + tables ───────────────────────────────────────────────────── */
.rep-grid{display:grid;gap:.875rem;margin-bottom:1rem;
  grid-template-columns:repeat(auto-fit,minmax(310px,1fr))}
.rep-table td,.rep-table th{font-variant-numeric:tabular-nums}
.rep-table thead th{font-size:.66rem;text-transform:uppercase;letter-spacing:.04em}
.rep-table th a{color:inherit;text-decoration:none;display:inline-flex;align-items:center;gap:.2rem}
.rep-table th a:hover{color:var(--primary)}
.rep-table tfoot th{background:var(--gray-50);border-top:2px solid var(--gray-300);font-size:.8rem}
.rep-table tbody tr:hover{background:var(--gray-50)}
.rep-drill{display:inline-flex;align-items:center;gap:.1rem;text-decoration:none;
  color:var(--primary);font-weight:600}
.rep-drill:hover{text-decoration:underline}
.rep-drill .material-symbols-rounded{font-size:15px}

.rep-metric-grid{display:grid;gap:.35rem .75rem;
  grid-template-columns:repeat(auto-fit,minmax(168px,1fr))}

/* ── Unallocated / integrity ───────────────────────────────────────────── */
.rep-unal{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(158px,1fr))}
.rep-unal>div{display:flex;flex-direction:column;gap:.15rem;padding:.65rem .8rem;
  background:var(--warning-light);border-radius:var(--radius);border:1px solid var(--warning)}
.rep-unal span{font-size:.66rem;color:var(--gray-600);text-transform:uppercase;
  letter-spacing:.04em;font-weight:700}
.rep-unal strong{font-size:1.05rem;color:var(--gray-900);font-variant-numeric:tabular-nums}
.rep-unal em{font-style:normal;font-size:.72rem;color:var(--gray-600);font-variant-numeric:tabular-nums}

@media(max-width:700px){
  .rep-bar-row{grid-template-columns:1fr auto}
  .rep-bar-track{display:none}
  .rep-kpi>strong{font-size:1.15rem}
  .rep-cols{height:170px}
}
/* Custom from/to fields. Explicit rather than relying on the UA default for
   [hidden] — any future rule setting display on .form-group would beat it. */
.rep-custom[hidden]{display:none}

/* ── Insights + data quality ───────────────────────────────────────────── */
.rep-tips{display:flex;flex-direction:column;gap:.55rem}
.rep-tip{display:flex;gap:.7rem;align-items:flex-start;padding:.7rem .85rem;
  border-radius:var(--radius);border:1px solid var(--gray-200);background:var(--surface)}
.rep-tip>.material-symbols-rounded{font-size:20px;flex-shrink:0;margin-top:.05rem}
.rep-tip strong{display:block;font-size:.83rem;color:var(--gray-900);margin-bottom:.1rem}
.rep-tip p{margin:0;font-size:.76rem;color:var(--gray-600);line-height:1.45}
.rep-tip-info>.material-symbols-rounded{color:var(--info)}
.rep-tip-success>.material-symbols-rounded{color:var(--success)}
.rep-tip-warning{background:var(--warning-light);border-color:var(--warning)}
.rep-tip-warning>.material-symbols-rounded{color:var(--warning)}
.rep-tip-danger{background:var(--danger-light);border-color:var(--danger)}
.rep-tip-danger>.material-symbols-rounded{color:var(--danger)}

.rep-quality-score{font-size:1.05rem;font-weight:800;padding:.15rem .6rem;border-radius:999px;
  font-variant-numeric:tabular-nums}
.rep-q-good{background:var(--success-light);color:var(--success)}
.rep-q-fair{background:var(--warning-light);color:var(--warning)}
.rep-q-poor{background:var(--danger-light);color:var(--danger)}
.rep-q-unknown{background:var(--gray-100);color:var(--gray-500)}
.rep-quality{display:flex;flex-direction:column;gap:.5rem}
.rep-q-row{display:grid;grid-template-columns:minmax(120px,1.1fr) 2fr 56px minmax(120px,1.4fr);
  gap:.7rem;align-items:center}
.rep-q-label{font-size:.76rem;color:var(--gray-700);font-weight:500}
.rep-q-val{font-size:.75rem;font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
.rep-q-note{font-size:.68rem;color:var(--gray-500)}
.rep-qf-good{background:var(--success)}
.rep-qf-fair{background:var(--warning)}
.rep-qf-poor{background:var(--danger)}
.rep-qf-na{background:var(--gray-300)}
@media(max-width:760px){
  .rep-q-row{grid-template-columns:1fr 56px}
  .rep-q-row .rep-bar-track,.rep-q-note{display:none}
}

/* Report help panel.

   Prose, not a data pane — so it opts out of the global .card-body max-height
   that was giving it an inner scrollbar, and lays out in balanced columns so a
   narrow measure does not strand two thirds of a full-width card. */
.card-body.rep-help{max-height:none;overflow:visible}
.rep-help{font-size:.83rem;line-height:1.6;color:var(--gray-700);
  column-width:29rem;column-gap:2.5rem;column-fill:balance}
.rep-help-sec{break-inside:avoid;page-break-inside:avoid;display:block;margin-bottom:1.15rem}
.rep-help h4{font-size:.83rem;font-weight:700;color:var(--gray-900);margin:0 0 .35rem;
  padding-bottom:.25rem;border-bottom:1px solid var(--gray-200)}
.rep-help p{margin:0 0 .55rem}
.rep-help :last-child{margin-bottom:0}
.rep-help ul,.rep-help ol{margin:0 0 .6rem 1.1rem;padding:0}
@media(max-width:900px){ .rep-help{column-width:auto;column-count:1} }
.rep-help li{margin-bottom:.3rem}
.rep-help-formula{display:block;background:var(--gray-50);border:1px solid var(--gray-200);
  border-radius:var(--radius);padding:.5rem .7rem;font-family:var(--font-mono);font-size:.76rem;
  color:var(--gray-800);margin:.5rem 0}
.rep-help-table{width:100%;border-collapse:collapse;margin:.35rem 0 .7rem}
.rep-help-table td{padding:.35rem .5rem;border-bottom:1px solid var(--gray-200);vertical-align:top}
.rep-help-table td:first-child{width:130px;white-space:nowrap;color:var(--gray-900)}

/* Per-section export control */
.rep-secx { position: relative; display: inline-block; margin-left: auto; }
.rep-secx-menu {
  display: none; position: absolute; right: 0; top: 100%; z-index: 40;
  min-width: 150px; padding: 5px; margin-top: 4px;
  background: var(--surface, #fff); border: 1px solid var(--gray-200);
  border-radius: var(--radius); box-shadow: 0 6px 22px rgba(15,23,42,.14);
}
.rep-secx.open .rep-secx-menu { display: block; }
.rep-secx-menu a {
  display: block; padding: 5px 9px; font-size: .76rem; color: var(--gray-700);
  text-decoration: none; border-radius: 5px;
}
.rep-secx-menu a:hover { background: var(--gray-100); color: var(--gray-900); }
.rep-secx-grp {
  padding: 5px 9px 3px; font-size: .62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--gray-400);
}
.rep-secx-grp + a { margin-top: 0; }

/* Voyage P&L summary strip.

   grid-auto-flow:column with equal auto-columns gives every tile the SAME
   width regardless of how many there are or how long their contents. Flexbox
   could not: a flex item will not shrink below its content, so the tile
   holding "IDR 4,957,087,647" grew while the one holding "6" stayed narrow,
   and the strip looked broken. */
.rep-kpi-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 8px;
}
@media (max-width: 1180px) {
  .rep-kpi-row { grid-auto-flow: row; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}
.rep-mini {
  min-width: 0;                       /* lets the tile shrink; without it the grid overflows */
  padding: 9px 11px;
  background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: var(--radius);
}
.rep-mini-lbl {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em; color: var(--gray-500);
  /* One line always, so every value in the strip sits on the same baseline. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rep-mini-val {
  font-size: .88rem; font-weight: 700; color: var(--gray-900); margin-top: 3px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Voyage P&L table: keep identifiers on one line and let only the route list
   truncate, so rows stay a uniform height instead of some being double. */
.rep-vpnl td, .rep-vpnl th { white-space: nowrap; }
.rep-vpnl td.rep-routes {
  max-width: 210px; overflow: hidden; text-overflow: ellipsis;
}
.rep-vpnl td.rep-vname { max-width: 150px; overflow: hidden; text-overflow: ellipsis; }

/* Catalog level chips */
.rep-chip {
  display: inline-block; padding: 1px 7px; margin-right: 3px; border-radius: 999px;
  background: var(--gray-100); color: var(--gray-600);
  font-size: .62rem; font-weight: 600; letter-spacing: .03em;
}

/* Multi-select filter — a dropdown of checkboxes.

   Replaces a native <select multiple>, which needs an undiscoverable
   ctrl+click and whose selection highlight reads as an error state. The
   toggle is styled to match the sibling <select> filters exactly so the row
   stays visually uniform. */
.ms { position: relative; }
.ms .ms-toggle {
  display: flex; align-items: center; gap: 6px; width: 100%;
  text-transform: none; letter-spacing: normal; font-weight: 400;
  padding: 0.5rem 0.7rem; text-align: left; cursor: pointer;
  background: var(--surface, #fff); color: var(--gray-800);
  border: 1px solid var(--gray-300); border-radius: var(--radius);
  font-family: inherit; font-size: 0.82rem; line-height: 1.4;
}
.ms .ms-toggle:hover { border-color: var(--gray-400); }
.ms .ms-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.ms .ms-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ms-caret { font-size: 17px; color: var(--gray-400); transition: transform var(--t-base); }
.ms.open .ms-caret { transform: rotate(180deg); }

.ms-panel {
  display: none; position: absolute; z-index: 60; left: 0; top: calc(100% + 4px);
  min-width: 100%; max-width: 260px; max-height: 260px; overflow-y: auto;
  padding: 5px; background: var(--surface, #fff);
  border: 1px solid var(--gray-200); border-radius: var(--radius);
  box-shadow: 0 8px 26px rgba(15,23,42,.15);
}
.ms.open .ms-panel { display: block; }

.ms-actions { display: flex; gap: 4px; padding: 2px 3px 6px; border-bottom: 1px solid var(--gray-100); }
.ms-actions button {
  flex: 1; padding: 3px 6px; cursor: pointer;
  background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 5px;
  font-family: inherit; font-size: .68rem; color: var(--gray-600);
}
.ms-actions button:hover { background: var(--gray-100); color: var(--gray-900); }

/* An option is a <label>, so it inherits the filter bar's FIELD-label styling:
   `.form-group label {display:block}` (0,1,1) and
   `.rep-filterbar label {text-transform:uppercase}` both outrank a bare
   `.ms-opt` (0,1,0) — which stacked the checkbox above its text and shouted
   every brand name in caps. `.ms .ms-opt` (0,2,0) wins, and each inherited
   property is reset explicitly rather than left to luck. */
.ms .ms-opt {
  display: flex; align-items: center; gap: 8px;
  margin: 1px 0; padding: 6px 8px;
  border-radius: 5px; cursor: pointer;
  font-size: .78rem; font-weight: 400; color: var(--gray-700);
  text-transform: none; letter-spacing: normal; line-height: 1.35;
}
.ms .ms-opt:hover { background: var(--gray-100); }
.ms .ms-opt input { margin: 0; cursor: pointer; flex-shrink: 0; }
.ms .ms-opt span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-transform: none; letter-spacing: normal;
}
.ms .ms-hint {
  margin: 6px 8px 2px; font-size: .64rem; color: var(--gray-400);
  text-transform: none; letter-spacing: normal; font-weight: 400;
}

/* Money and counts must not wrap mid-figure in the wide report tables. */
.data-table td.text-right, .data-table th.text-right { white-space: nowrap; }
.rep-secx-menu { white-space: nowrap; }

/* ── Dropdowns must not be clipped by their own card ────────────────────
   .card sets overflow:hidden so content stays inside its rounded corners.
   That also clips anything absolutely positioned that opens DOWNWARD out of
   a filter row or a card header — which is exactly what the brand filter and
   the per-section export menus do: only a sliver of the panel was visible.

   Cards containing such a control opt out. Their tables keep their own
   clipping via .table-wrap, so nothing else escapes. */
.card:has(.ms),
.card:has(.rep-secx) { overflow: visible; }

/* The filter bar is also a scroll container by default (.card-body caps at
   600px); a dropdown inside a scroll box is clipped by it too. */
.card-body.rep-filterbar { max-height: none; overflow: visible; }

/* The header holding an export menu must not clip it either. */
.card-header:has(.rep-secx) { overflow: visible; }

/* ── Searchable select (long port lists) ────────────────────────────────
   Wraps an existing <select>, which stays in the DOM carrying the name,
   value, `required` and every listener already bound to it. */
.ss { position: relative; }
.ss .ss-native {
  /* Visually hidden but still focusable/validatable: display:none would make
     `required` unvalidatable and browsers refuse to submit with no message. */
  position: absolute; opacity: 0; pointer-events: none;
  width: 100%; height: 100%; left: 0; top: 0; z-index: -1;
}
/* The closed box IS a field. It replaces every <select> in the product now, so
   any difference between the two — a heavier border, a tighter padding, a
   different focus ring — shows up as two kinds of dropdown on one form. These
   values are the ones on the input rule, deliberately, and both follow the same
   tokens. */
.ss .ss-toggle {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 0.55rem 0.875rem; cursor: pointer; text-align: left;
  background: var(--surface); color: var(--gray-800);
  border: var(--control-border) solid var(--gray-200); border-radius: var(--radius);
  font-family: var(--font-body); font-size: 0.85rem; line-height: 1.4;
  text-transform: none; letter-spacing: normal; font-weight: 400;
  transition: border-color var(--t-base), box-shadow var(--t-base);
}
.ss .ss-toggle:hover { border-color: var(--gray-300); }
.ss .ss-toggle:focus-visible,
.ss.open .ss-toggle { outline: none; border-color: var(--primary); box-shadow: var(--focus-ring); }

/* A disabled dropdown has to read as disabled, or somebody clicks it and
   nothing happens, which is how a form looks broken. */
.ss-disabled .ss-toggle { cursor: not-allowed; opacity: .6; background: var(--gray-50); }
.ss-disabled .ss-toggle:hover { border-color: var(--gray-200); }

/* Below eight options the search box is hidden, not the dropdown: the control
   stays the same control. */
.ss .ss-search[hidden] { display: none; }
.ss .ss-value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ss .ss-value.ss-muted { color: var(--gray-400); }
.ss .ss-caret { font-size: 17px; color: var(--gray-400); flex-shrink: 0; }

.ss .ss-panel {
  display: none; position: absolute; z-index: 70; left: 0; top: calc(100% + 4px);
  width: 100%; min-width: 250px; padding: 6px;
  background: var(--surface, #fff); border: 1px solid var(--gray-200);
  border-radius: var(--radius); box-shadow: 0 10px 30px rgba(15,23,42,.16);
}
.ss.open .ss-panel { display: block; }
.ss .ss-search {
  width: 100%; padding: 6px 8px; margin-bottom: 5px;
  border: 1px solid var(--gray-200); border-radius: 6px;
  font-family: inherit; font-size: .8rem;
}
.ss .ss-list { max-height: 240px; overflow-y: auto; }

/* Where a select was sized by its surroundings rather than by an inline width,
   the wrapper has to inherit that sizing — the native select is invisible now,
   so a rule aimed at it no longer decides anything a person can see. */
.ui-filters .ss { flex: 0 1 auto; min-width: 130px; max-width: 210px; }
.ui-filters .ss .ss-toggle { font-size: .82rem; padding: .42rem .6rem; height: 34px; }
.field-row .ss, .form-group .ss { width: 100%; }
.ss-native { width: 100% !important; }   /* it is invisible; never let it size the row */
.ss .ss-group {
  padding: 6px 8px 3px; font-size: .62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--gray-400);
}
.ss .ss-opt {
  padding: 6px 8px; border-radius: 5px; cursor: pointer;
  font-size: .8rem; color: var(--gray-700); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.ss .ss-opt:hover, .ss .ss-opt.ss-active { background: var(--gray-100); color: var(--gray-900); }
/* Multiple choice and remote search (searchable-select.js). A multi row carries
   a tick that is invisible until chosen, so rows do not shift when toggled; a
   remote row may carry a second, quieter line of context on the right. */
.ss .ss-opt { display: flex; align-items: center; gap: .45rem; }
.ss .ss-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ss .ss-sub { flex: 0 1 auto; max-width: 45%; font-size: .72rem; color: var(--gray-400);
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ss .ss-check { font-size: 16px; width: 16px; flex-shrink: 0; color: transparent; }
/* A single choice fills its row with the primary colour (below). A multiple
   choice must not: several solid-blue rows read as a block, and the tick —
   primary on primary — vanishes. Chosen rows there get a light wash instead. */
.ss.ss-multi .ss-opt.ss-sel { background: var(--primary-light); color: var(--gray-900); font-weight: 600; }
.ss.ss-multi .ss-opt.ss-sel:hover, .ss.ss-multi .ss-opt.ss-sel.ss-active { background: var(--gray-100); }
.ss.ss-multi .ss-opt.ss-sel .ss-check { color: var(--primary); }
.ss .ss-opt.ss-sel .ss-sub { color: rgba(255,255,255,.8); }
.ss.ss-multi .ss-opt.ss-sel .ss-sub { color: var(--gray-500); }
.ss .ss-opt.ss-sel { background: var(--primary); color: #fff; font-weight: 600; }
.ss .ss-empty { padding: 10px 8px; font-size: .76rem; color: var(--gray-400); }

/* A card must not clip an open picker. */
.card:has(.ss) { overflow: visible; }

/* ── Import: CSV template list ──────────────────────────────────────────
   .card-body caps at 600px and scrolls, which silently hid 9 of the 18
   modules — the list simply stopped mid-row and nothing said so. This is a
   reference list, not a log: it should be complete. */
.card-body.imp-templates {
  max-height: none; overflow: visible;
  display: flex; flex-direction: column; gap: 0.25rem;
}
.imp-tpl-row {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.625rem 0; border-bottom: 1px solid var(--gray-100);
  /* The row is ALSO a flex item (of the column card-body), and a flex item's
     automatic minimum size is its min-content width. With nowrap column lists
     that is ~1900px, so the row refused to shrink and pushed the whole page
     into a horizontal scroll. min-width:0 is needed on both levels, not just
     on the text container. */
  min-width: 0;
}
.imp-tpl-row:last-child { border-bottom: none; }
/* min-width:0 is what lets the column list shrink. Without it a flex item
   refuses to go below its content width, so the long header strings pushed
   the Template button off the row instead of truncating. */
.imp-tpl-meta { min-width: 0; flex: 1; }
.imp-tpl-cols {
  /* WRAP, do not truncate. nowrap gives this text a min-content width of the
     whole column list — ~1900px for Vessels — and a flex item cannot shrink
     below its min-content, so min-width:0 alone could not save it: the row
     stayed wide, pushed the Template button off screen and put the whole page
     into a horizontal scroll. Wrapping makes min-content the longest single
     word instead, and it has the side benefit of showing the full header
     rather than hiding most of it behind an ellipsis. */
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;
  line-height: 1.5;
  margin-top: 3px;
}
.imp-tpl-row .btn { flex-shrink: 0; }

/* ── Template Builder live preview ────────────────────────
   The preview is a rendered document in an iframe, so the frame scrolls
   itself and the card body above opts out of the height cap. Sizing the
   frame to the viewport (rather than a fixed 800px that overhangs most
   screens) keeps the whole preview visible beside the block list, which is
   sticky-positioned and stays put while you edit. */
.tpl-preview-frame {
  display: block;
  width: 100%;
  height: calc(100vh - 13rem);
  min-height: 420px;
  border: none;
  background: #fff;
}
@media (max-width: 900px) {
  /* Stacked layout: the preview follows the editor instead of sitting beside
     it, so a viewport-tall frame would push the page down by a screenful. */
  .tpl-preview-frame { height: 70vh; }
}

/* ── Switch row ───────────────────────────────────────────
   A checkbox and its wording on one line, used wherever a setting reads as a
   sentence rather than a field: "Reopen if already signed off", "Show the
   market ticker". Four pages used this class and none of them had a rule, so
   the checkbox and its label sat as plain inline text with the label's own
   baseline — legible, but not aligned with anything around it. */
.switch-row {
  display: flex; align-items: center; gap: .5rem;
  font-weight: 400; cursor: pointer;
}
.switch-row input[type=checkbox] { flex: 0 0 auto; margin: 0; }
.switch-row > span { line-height: 1.35; }

/* The login hero's wordmark, shown when no logo has been uploaded. It stands in
   for an image, so it needs the size an image would have had. */
.login-left-brand {
  font-family: var(--font-display, inherit);
  font-size: 2rem; font-weight: 800; letter-spacing: -.02em;
  line-height: 1.1; margin-bottom: .75rem; color: inherit;
}
.login-left-brand span { color: var(--primary); }

/* ── Module guide pages ───────────────────────────────────
   Shared by every "how this module works" page: B2B Help, Finance & Accounting
   Help, Warehouse Help. A guide is prose-width and diagram-led rather than a
   data pane.

   These were the gd-* classes, named for the one page that had them. Nothing
   in here is about B2B, so the second guide page would have had to either
   duplicate the block or import a class named after another module. Renamed to
   gd-* once, rather than growing a third copy later. */
.gd-flow {
  display: flex; align-items: stretch; gap: .35rem;
  flex-wrap: wrap;                     /* the journey wraps rather than scrolls */
}
.gd-step {
  flex: 1 1 8.5rem; min-width: 8.5rem;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .3rem; text-align: center; text-decoration: none;
  padding: .8rem .5rem; border: 1px solid var(--gray-200); border-radius: 10px;
  background: var(--surface); color: var(--gray-700);
  transition: border-color var(--t-fast), color var(--t-fast), transform var(--t-fast);
}
.gd-step:hover { border-color: var(--primary); color: var(--primary); transform: translateY(-1px); }
.gd-step .material-symbols-rounded { font-size: 22px; }
.gd-step strong { font-size: .76rem; font-weight: 600; line-height: 1.25; }
.gd-arrow { align-self: center; color: var(--gray-300); font-size: 18px; }
@media (max-width: 700px) { .gd-arrow { display: none; } }

/* Diagrams scroll inside their own box; the page itself never scrolls sideways. */
.gd-svgwrap { overflow-x: auto; padding: .25rem 0 .5rem; }
.gd-svg {
  width: 100%; min-width: 640px;
  /* Capped at the viewBox width. Stretching a 860-unit diagram across a
     1300px card scaled every label to ~18px, which read as clip-art beside
     the page's 13px body text. At 1:1 the type in the diagram matches the
     type around it. */
  max-width: 880px; margin: 0 auto;
  height: auto; display: block; color: var(--gray-400);
}
.gd-node rect { fill: var(--gray-50); stroke: var(--gray-300); stroke-width: 1; }
.gd-node text { font-size: 12px; font-weight: 600; fill: var(--gray-800); text-anchor: middle; }
.gd-node text.gd-sub { font-size: 10px; font-weight: 400; fill: var(--gray-500); }
.gd-q rect   { fill: var(--surface); stroke: var(--gray-300); stroke-dasharray: 4 3; }
.gd-ok rect  { fill: var(--success-light, #ecfdf5); stroke: var(--success, #10b981); }
.gd-bad rect { fill: var(--danger-light, #fef2f2); stroke: var(--danger, #dc2626); }
.gd-line path { fill: none; stroke: currentColor; stroke-width: 1.4; }
.gd-lbl text  { font-size: 10px; fill: var(--gray-500); text-anchor: middle; }
.gd-cmp td:first-child { width: 92px; }

/* ── Surveillance camera wall ─────────────────────────────
   A responsive grid of latest stills — the "see every camera at once" view.
   Fixed 16:9 tiles so a wall of mixed sources still lines up. */
.cam-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: .85rem;
}
.cam-tile {
  display: block; text-decoration: none; color: inherit;
  border: 1px solid var(--gray-200); border-radius: 10px; overflow: hidden;
  background: #000; transition: border-color var(--t-fast), transform var(--t-fast);
}
.cam-tile:hover { border-color: var(--primary); transform: translateY(-1px); }
.cam-tile img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.cam-tile-cap { padding: .45rem .6rem; background: var(--surface); }
.cam-tile-cap strong { display: block; font-size: .82rem; }
.cam-tile-cap span { font-size: .72rem; color: var(--gray-500); }

/* Geostamp chip on a camera-wall tile — a located still is marked so you can
   tell it apart from an unplaced one. Amber when the underlying fix is stale. */
.cam-tile { position: relative; }
.cam-tile-geo {
  position: absolute; top: 6px; right: 6px; z-index: 1;
  width: 22px; height: 22px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(10,30,60,.72); color: #fff;
}
.cam-tile-geo.stale { background: rgba(180,83,9,.85); }
.cam-tile-geo .material-symbols-rounded { font-size: 15px; }


/* ────────────────────────────────────
   COMPY — sprite helper character
   Rendered by compy() in includes/compy.php, driven by assets/js/compy.js.

   The wrapper clips; the inner layer carries the sheet and is moved across it.
   Two elements, because one cannot both clip and be offset.

   Frame selection uses PERCENTAGE background-position, not pixels. With a
   6-column sheet the six positions are 0/20/40/60/80/100%, which the browser
   resolves against the scaled backdrop — so Compy renders identically at 48px
   and at 256px with no per-size arithmetic and no sub-pixel seam where a
   pixel offset would round into the neighbouring frame.
──────────────────────────────────── */
.compy {
  display: inline-block;
  position: relative;
  overflow: hidden;          /* the clip — nothing outside one frame is visible */
  flex: none;
  line-height: 0;            /* no inline descender gap under the box */
  vertical-align: middle;
}
.compy-frame {
  display: block;
  width: 100%;
  height: 100%;
  background-repeat: no-repeat;
  /* Set by the driver: background-image, background-size, background-position. */
  transform: translateZ(0);  /* own layer, so stepping the frame does not repaint around it */
}

/* Before the driver runs (and if JS never does), Compy is simply absent
   rather than a broken box. */
.compy:not([data-compy-ready]) .compy-frame { opacity: 0; }
.compy[data-compy-ready] .compy-frame { opacity: 1; }

/* Reduced motion is handled in the driver, which parks Compy on his rest
   frame and never schedules another. This is the belt-and-braces half: even
   if a future animation is ever moved into CSS, it stops here too. */
@media (prefers-reduced-motion: reduce) {
  .compy-frame { animation: none !important; transition: none !important; }
}


/* ── Help panel with Compy ──
   Compy sits beside the explanation, not above it, so the first line of the
   answer is still the first thing read. He is flex:none — the text takes the
   remaining width and Compy never squashes. */
.help-body { display: flex; align-items: flex-start; gap: 1rem; }
.help-body .help-compy { margin-top: -.25rem; }

/* Narrow screens: the text matters more than the character, so Compy steps
   aside rather than eating half the width. */
@media (max-width: 480px) {
  .help-body { gap: .75rem; }
  .help-body .help-compy { width: 56px !important; height: 50px !important; }
}

/* ── Compy's help bubble ──
   Compy pops up bottom-right with a speech bubble, beside Chat and Tasks.

   The button matches those two exactly — same height, radius, shadow and
   type — because it belongs to that row. Its horizontal position is set by
   script rather than hard-coded here: Chat already assumes Tasks is exactly
   100px wide, and both grow when their badge appears, so a third guess would
   be a third thing to get wrong. */
/* Pre-script fallback for the bottom-right button row.
   compyFabRow() measures the real buttons and overrides this on the first
   frame; it exists so the row is not stacked on top of itself if the script is
   slow, blocked or broken. ONE labelled approximation, in one place — the two
   copies of a hard-coded "100px" that used to live in msg_widget.php were a
   guess about the Tasks button's width, and being 2px out made the row uneven. */
:root { --fab-anchor-w: 98px; }

#compyHelpBtn {
  position: fixed;
  bottom: calc(1.75rem + var(--fab-lift, 0px));
  right: 1.75rem;                       /* replaced by script once measured */
  z-index: 8000;
  height: 42px;
  padding: 0 1.125rem;
  border-radius: 99px;
  background: var(--gray-800);
  color: #fff;
  border: none;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0,0,0,0.28);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-display);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  transition: transform var(--t-slow), box-shadow var(--t-slow), background var(--t-slow);
}
#compyHelpBtn:hover { background: var(--gray-900); transform: scale(1.05); }
#compyHelpBtn.open  { background: var(--primary); }
#compyHelpBtn[hidden] { display: none !important; }

.compy-help {
  position: fixed;
  bottom: calc(5.5rem + var(--fab-lift, 0px));   /* clear of the button row */
  right: 1.75rem;
  z-index: 8001;                                  /* above the FABs */
  width: min(360px, calc(100vw - 3rem));
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .35rem;
  /* No animation on the container itself — the two halves arrive separately,
     Compy first and then the bubble, so it reads as him turning up and THEN
     speaking rather than a panel appearing with a picture in it. */
}
.compy-help[hidden] { display: none !important; }

/* ── Arriving and leaving ──
   He comes in from the right, off the edge of the window, and goes back the
   same way. Both halves travel; the bubble is a beat behind on the way in and
   a beat ahead on the way out, which is the difference between a character
   arriving and a box fading in. */
@keyframes compyEnter {
  from { opacity: 0; transform: translateX(120%); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes compyLeave {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(120%); }
}

.compy-help.is-in .compy-help-char {
  /* Unhurried on the way in — he is arriving, not being fired out of a
     cannon. The overshoot at the end of the curve is what makes it read as
     him settling rather than sliding to a stop. */
  animation: compyEnter .78s cubic-bezier(.16, .84, .28, 1.06) both;
}
.compy-help.is-in .compy-help-bubble {
  animation: compyEnter .62s cubic-bezier(.16, .84, .28, 1.06) .30s both;
}
/* Leaving is quicker than arriving. Once you have dismissed something you
   want it gone, and a slow exit reads as the panel arguing with you. */
.compy-help.is-out .compy-help-bubble {
  animation: compyLeave .30s ease-in both;
}
.compy-help.is-out .compy-help-char {
  animation: compyLeave .34s ease-in .10s both;
}

.compy-help-bubble {
  position: relative;
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0,0,0,.18);
  padding: .9rem 1rem .75rem;
  /* NO overflow here. The tail is drawn by ::before/::after sitting just
     outside the bottom edge, and any overflow value other than visible clips
     them away — the bubble loses its tail and stops reading as speech.
     Long help scrolls in the list below instead, which also keeps the title
     and the Got it button in view. */
}
.compy-help-bubble h4 {
  margin: 0 1.5rem .5rem 0;
  font-size: .9rem;
  font-weight: 700;
  color: var(--gray-800);
}
#compyHelpIntro {
  margin: 0 0 .55rem;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--gray-800);
  font-weight: 600;
}
#compyHelpIntro[hidden] { display: none; }

#compyHelpPoints {
  margin: 0;
  padding-left: 1.05rem;
  font-size: .82rem;
  line-height: 1.5;
  color: var(--gray-700);
  max-height: min(50vh, 340px);
  overflow-y: auto;
  /* overscroll containment, so flicking a long help list does not then start
     scrolling the page underneath it */
  overscroll-behavior: contain;
}
#compyHelpPoints li { margin-bottom: .5rem; }

/* Each point fades in just after the one above, so the panel reads as Compy
   saying them rather than a wall of text appearing at once. Backwards fill, so
   a point is invisible during its delay instead of flashing then fading. */
/* ── Compy's observations ─────────────────────────────────────────────────
   An observation is a claim, so it gets a marker showing how loud it is and a
   control that shows its arithmetic. Both stay quiet until wanted: the point
   is the sentence, and the working is for the moment somebody doubts it. */
.compy-signal { position: relative; }
.compy-signal::before {
  content: '';
  position: absolute;
  left: -.85rem; top: .5em;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--gray-400);
}
.compy-signal[data-severity="act"]::before   { background: var(--danger); }
.compy-signal[data-severity="watch"]::before { background: var(--warning); }
.compy-signal[data-severity="good"]::before  { background: var(--success); }

.compy-why-wrap { display: block; }

.compy-why-btn {
  display: inline-block;
  margin-top: .3rem;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  font-size: .72rem;
  color: var(--gray-500);
  cursor: pointer;
  border-bottom: 1px dotted var(--gray-400);
  line-height: 1.2;
}
.compy-why-btn:hover { color: var(--primary); border-bottom-color: var(--primary); }
.compy-why-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Animated from a measured pixel height, so it can actually transition;
   `auto` cannot. Hidden overflow keeps the collapsed state at exactly zero. */
.compy-why-panel {
  display: block;
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--t-slower) var(--ease), opacity var(--t-slow);
  opacity: 0;
  font-size: .74rem;
  line-height: 1.5;
  color: var(--gray-600);
}
.compy-why-panel.is-open { opacity: 1; margin-top: .4rem; }

.compy-why-text { display: block; margin-bottom: .4rem; }

.compy-why-figures {
  display: block;
  border-left: 2px solid var(--gray-200);
  padding-left: .6rem;
}
.compy-why-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .12rem 0;
}
.compy-why-k { color: var(--gray-500); }
.compy-why-v { font-weight: 600; color: var(--gray-700); font-variant-numeric: tabular-nums; }

.compy-why-link {
  display: inline-block;
  margin-top: .45rem;
  font-size: .74rem;
  font-weight: 600;
  color: var(--primary);
  text-decoration: none;
}
.compy-why-link:hover { text-decoration: underline; }

@media (prefers-reduced-motion: reduce) {
  .compy-why-panel { transition: none; }
}

.compy-say {
  opacity: 0;
  animation: compySay .32s ease-out forwards;
}
@keyframes compySay {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Someone who asked for less motion gets the text immediately and Compy
   holding still — the staggered reveal is decoration, and decoration is
   exactly what that setting is about. */
@media (prefers-reduced-motion: reduce) {
  .compy-say { opacity: 1; animation: none; }
  /* No sliding entrance either — he is simply there, and simply gone. */
  .compy-help.is-in .compy-help-char,
  .compy-help.is-in .compy-help-bubble,
  .compy-help.is-out .compy-help-char,
  .compy-help.is-out .compy-help-bubble { animation: none; }
}
#compyHelpPoints li:last-child { margin-bottom: 0; }
/* Who said it. Placed with the words rather than under the Got it button, so
   it reads as a sign-off on the speech and not as a label on the panel. */
.compy-help-sig {
  margin: .55rem 0 0;
  text-align: right;
  font-size: .74rem;
  font-weight: 600;
  color: var(--gray-400);
  font-style: italic;
  /* It is also the control that opens Compy's own introduction, so it has to
     look pressable without turning a sign-off into a button. */
  cursor: pointer;
  user-select: none;
  transition: color var(--t-base) ease;
}
.compy-help-sig:hover,
.compy-help-sig:focus-visible {
  color: var(--primary);
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  outline: none;
}

/* Step controls. Only rendered when the help is long enough to need them —
   see compyHelpFill(). */
.compy-help-nav {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem; margin-top: .7rem; padding-top: .6rem;
  border-top: 1px solid var(--gray-100);
}
.compy-help-nav[hidden] { display: none !important; }
.compy-step {
  width: 28px; height: 28px; border-radius: 50%;
  border: 1px solid var(--gray-200); background: var(--surface);
  color: var(--gray-600); cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex: none;
}
.compy-step:hover:not(:disabled) { background: var(--gray-50); color: var(--gray-800); }
.compy-step:disabled { opacity: .35; cursor: default; }
.compy-dots { display: flex; gap: .3rem; align-items: center; }
.compy-dots i {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--gray-300); display: block;
}
.compy-dots i.on { background: var(--primary); transform: scale(1.25); }

.compy-help-more { margin-top: .7rem; text-align: right; }
.compy-help-close {
  position: absolute;
  top: .5rem; right: .5rem;
  background: none; border: 0; cursor: pointer;
  color: var(--gray-400);
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
}
.compy-help-close:hover { background: var(--gray-100); color: var(--gray-700); }

/* The tail. Two triangles — the outer one is the border, the inner one the
   fill sitting 1px inside it — because a CSS triangle cannot have a border
   of its own. Right-aligned, so it points down at Compy's head. */
.compy-help-bubble::before,
.compy-help-bubble::after {
  content: "";
  position: absolute;
  right: 30px;
  border: 10px solid transparent;
  border-top-color: var(--gray-200);
  border-bottom: 0;
}
.compy-help-bubble::before { bottom: -10px; }
.compy-help-bubble::after  { bottom: -9px; border-top-color: var(--surface); }

.compy-help-char { margin-right: 6px; line-height: 0; }

/* Narrow screens: the bubble spans the width and Compy shrinks, so the
   answer stays readable rather than the character taking the room. */
@media (max-width: 480px) {
  .compy-help { right: 1rem; bottom: calc(5rem + var(--fab-lift, 0px)); }
  .compy-help-char .compy { width: 60px !important; height: 53px !important; }
  #compyHelpBtn { right: 1rem; bottom: calc(1.25rem + var(--fab-lift, 0px)); }
}

/* ── Help that is about right now ────────────────────────────────────────
   A line generated from the state of the screen ("no payment provider is
   switched on") sits in the same list as the written help and has to read
   differently: one describes the page, the other is a thing to go and do.

   Colour the MARKER, nothing else. The first attempt drew its own dot with
   list-style:none, a negative margin and an absolutely positioned ::before —
   which put it at the list's padding edge, noticeably further left than the
   real bullets beside it, so the emphasis read as a broken row instead. ::marker
   keeps the browser's own geometry exactly, so these lines sit on the same
   vertical line as every other point and differ only in colour. */
.help-points li.help-now::marker,
#compyHelpPoints li.compy-now::marker {
  color: var(--primary);
}
.help-points li.help-now,
#compyHelpPoints li.compy-now {
  color: var(--gray-800);
}

/* ── Compy's reactions ───────────────────────────────────────────────────
   One sprite sheet, one gesture. Everything he expresses beyond that has to
   be movement, so a nod, a shake and a perk of attention are transforms on
   the FRAME layer. Not the wrapper: CompyLife rewrites the wrapper's
   transform every animation frame for the breath and the lean, so anything
   declared there is overwritten before it can be seen. The frame layer only
   has its background-position touched, which leaves transform free. */
@keyframes compyNod {
  0%   { transform: translateY(0) rotate(0deg); }
  25%  { transform: translateY(3px) rotate(2deg); }
  50%  { transform: translateY(-1px) rotate(-1deg); }
  75%  { transform: translateY(2px) rotate(1deg); }
  100% { transform: translateY(0) rotate(0deg); }
}
@keyframes compyShake {
  0%, 100% { transform: translateX(0) rotate(0deg); }
  15%  { transform: translateX(-3px) rotate(-3deg); }
  35%  { transform: translateX(3px) rotate(3deg); }
  55%  { transform: translateX(-2px) rotate(-2deg); }
  75%  { transform: translateX(1px) rotate(1deg); }
}
/* A shift of weight, not a performance: this is the one that fires when he has
   been left alone, and it has to be small enough to catch the eye without
   demanding anything. */
@keyframes compyPerk {
  0%   { transform: translateY(0) scale(1); }
  30%  { transform: translateY(-3px) scale(1.03); }
  60%  { transform: translateY(0) scale(1); }
  75%  { transform: translateY(-1px); }
  100% { transform: translateY(0); }
}
.compy-frame.compy-react-nod   { animation: compyNod   .55s ease-in-out; }
.compy-frame.compy-react-shake { animation: compyShake .45s ease-in-out; }
.compy-frame.compy-react-perk  { animation: compyPerk  .75s ease-in-out; }

@media (prefers-reduced-motion: reduce) {
  .compy-frame.compy-react-nod,
  .compy-frame.compy-react-shake,
  .compy-frame.compy-react-perk { animation: none; }
}

/* ── Compy on the Help button ────────────────────────────────────────────
   He used to exist only inside an open bubble, which made him something you
   summoned rather than someone who is there. Perched on the button he is
   present the whole time.

   HE DOES NOT LEAVE WHEN THE BUBBLE OPENS. Two earlier attempts both failed,
   in opposite directions: fading him out left a blank 24px circle beside the
   word "Help", and collapsing the box as well fixed the hole but made the
   button visibly change width every time the bubble opened. A control that
   resizes as you use it is worse than a redundant character, so he simply
   stays, the button is one size always, and the row never moves. */
#compyHelpBtn .compy-mini {
  transform-origin: 50% 100%;
}

/* A small bob per spoken line. Much smaller than the other reactions: it fires
   once for every point he says, so anything with real amplitude turns a three
   line answer into a bouncing toy. */
@keyframes compyTalk {
  0%   { transform: translateY(0) rotate(0deg); }
  40%  { transform: translateY(-2px) rotate(-1.2deg); }
  100% { transform: translateY(0) rotate(0deg); }
}
.compy-frame.compy-react-talk { animation: compyTalk .26s ease-in-out; }

/* Raising a hand: one soft pulse on the Help button when the screen has
   something live to say. No colour change — a button that turns amber reads as
   an error, and "you have four overdue invoices" is not an error, it is a
   Tuesday. */
@keyframes compyNudge {
  0%, 100% { box-shadow: 0 4px 16px rgba(0,0,0,0.28); }
  30%      { box-shadow: 0 4px 16px rgba(0,0,0,0.28),
                         0 0 0 6px color-mix(in srgb, var(--primary) 26%, transparent); }
  60%      { box-shadow: 0 4px 16px rgba(0,0,0,0.28),
                         0 0 0 11px color-mix(in srgb, var(--primary) 0%, transparent); }
}
#compyHelpBtn.compy-nudge { animation: compyNudge 1.5s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .compy-frame.compy-react-talk,
  #compyHelpBtn.compy-nudge { animation: none; }
}

/* ── The draft restore offer ─────────────────────────────────────────────
   Sits at the top of the form it belongs to, not as a floating toast: it is
   about THIS form, it needs to survive long enough to be read, and its two
   buttons are a decision rather than a dismissal. */
.draft-prompt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .75rem 1rem;
  margin-bottom: 1.1rem;
  padding: .8rem 1rem;
  border: 1px solid var(--primary);
  border-left-width: 3px;
  border-radius: 8px;
  background: var(--primary-light);
  font-size: .84rem;
  line-height: 1.5;
  color: var(--gray-800);
}
.draft-prompt.is-conflict {
  border-color: var(--warning);
  background: var(--warning-light);
}
.draft-prompt-text { flex: 1 1 22rem; min-width: 0; }
.draft-prompt-text strong { font-weight: 600; }
.draft-warn { display: block; margin-top: .2rem; color: var(--warning); font-weight: 500; }
.draft-prompt.is-conflict .draft-warn { color: var(--danger); }
.draft-prompt-actions { display: flex; gap: .5rem; flex-shrink: 0; }

/* ── The work strip ──────────────────────────────────────────────────────
   Shaped like browser tabs, and behaving like them: a row of folders sitting
   on the bottom edge of the top bar, the active one continuous with the page
   below it. No motion on switching — a browser does not animate a tab change,
   and neither should this. The strip is furniture, not an effect. */
.topbar-tabs {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: flex-end;
  gap: 1px;
  /* Sits ON the bottom edge of the topbar so the tabs meet the page. */
  align-self: stretch;
  margin-bottom: -1px;
  padding-top: .55rem;
  /* Visible on BOTH axes, which matters more than it looks.
     
     This used to scroll horizontally, and CSS will not let one axis scroll
     while the other stays visible — set overflow-x:auto and overflow-y
     computes to a clipping value whatever you write. That clipped the overflow
     menu, which hangs below the strip: the element was display:block, 230x278,
     inside the viewport, and simply not painted.
     
     The rail no longer needs to scroll anyway. It holds at most
     TABS_VISIBLE_MAX chips plus the counter, and those shrink to fit. */
  overflow: visible;
}
.tab-chip {
  display: inline-flex;
  align-items: center;
  /* EQUAL WIDTHS, like Chrome and Firefox. Tabs do not size themselves to
     their titles there; they share the row and narrow together as more open.
     Content-sized tabs make the row jump around as you move between screens,
     because every tab resizes when any label changes. */
  flex: 1 1 0;
  min-width: 0;
  max-width: 13rem;
  height: 30px;
  padding: 0 .2rem 0 .1rem;
  /* A folder: rounded at the top, square where it meets the page. */
  border: 1px solid var(--gray-200);
  border-bottom: 1px solid var(--gray-200);
  border-radius: 7px 7px 0 0;
  background: var(--gray-100);
  color: var(--gray-500);
  font-family: var(--font-display);
  font-size: .76rem;
  font-weight: 500;
  white-space: nowrap;
}
.tab-chip:hover { background: var(--gray-50); color: var(--gray-700); }

/* The active tab IS the page: same surface, and its bottom edge is opened so
   the two read as one sheet. That is what a browser tab does, and it needs no
   coloured underline to say which one you are on. */
/* Nothing here may change the tab's SIZE. Chrome and Firefox do not bold the
   active tab, and for a good reason: a heavier weight is wider, so the label
   that fitted a moment ago starts clipping the instant you land on it, and the
   row appears to shift as you move about. The active tab is told apart by its
   surface and its open bottom edge — the same two things a browser uses. */
.tab-chip.active {
  background: var(--surface);
  border-color: var(--gray-200);
  border-bottom-color: var(--surface);
  color: var(--gray-800);
}

.tab-chip-link {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  min-width: 0;
  flex: 1 1 auto;
  /* The clip lives HERE so the label inside can slide under it on hover. */
  overflow: hidden;
  padding: 0 .15rem 0 .55rem;
  color: inherit;
  text-decoration: none;
  font: inherit;
  line-height: 30px;
}
/* A clipped name fades out rather than stopping dead, the way a browser's own
   tab titles do. A hard cut mid-letter reads as a rendering fault; a fade reads
   as "there is more here". Only the tabs that are actually clipped get it, so
   a short name keeps its crisp edge.

   The mask sits on the LINK, which is the element doing the clipping. The
   close button is a sibling, so it never fades with the text. */
/* ── The overflow counter ─────────────────────────────────────────────────
   Past four tabs the rail stops shrinking and the rest move in here. Sized and
   shaped like a chip so the row still reads as one strip. */
.tab-more { position: relative; display: flex; align-items: flex-end; flex: 0 0 auto; }

.tab-more-btn {
  display: flex;
  align-items: center;
  gap: .1rem;
  height: 30px;
  padding: 0 .45rem 0 .55rem;
  border: 1px solid var(--gray-200);
  border-bottom: none;
  border-radius: 7px 7px 0 0;
  background: var(--gray-50);
  color: var(--gray-600);
  font: inherit;
  font-size: .78rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.tab-more-btn:hover { background: var(--gray-100); color: var(--gray-800); }
.tab-more-btn .material-symbols-rounded { font-size: 15px; }
.tab-more-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.tab-more-menu {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 300;
  min-width: 230px;
  max-width: 320px;
  max-height: 60vh;
  overflow-y: auto;
  padding: .3rem;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 10px;
  box-shadow: 0 12px 28px rgba(15, 23, 42, .16);
  scrollbar-width: thin;
}
[data-tab-more].is-open .tab-more-menu { display: block; }

.tab-more-row { display: flex; align-items: center; border-radius: 6px; }
.tab-more-row:hover { background: var(--gray-100); }

.tab-more-link {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .35rem;
  padding: .4rem .5rem;
  font-size: .8rem;
  color: var(--gray-700);
  text-decoration: none;
  /* The whole point of the list is that the name is readable, so it wraps to
     the ellipsis rather than being chopped mid-word as it was on the rail. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tab-more-link .tab-pin-mark { font-size: 14px; opacity: .65; flex-shrink: 0; }

.tab-more-x {
  flex: 0 0 auto;
  background: none;
  border: none;
  padding: .25rem;
  margin-right: .15rem;
  border-radius: 50%;
  font-size: 15px;
  line-height: 1;
  color: var(--gray-400);
  cursor: pointer;
  opacity: 0;
}
.tab-more-row:hover .tab-more-x { opacity: 1; }
.tab-more-x:hover { background: var(--gray-200); color: var(--gray-700); }
.tab-more-x:focus-visible { opacity: 1; outline: 2px solid var(--primary); }

.tab-chip.is-long .tab-chip-link {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent 100%);
          mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent 100%);
}
/* Once it has slid, the beginning is the part that is cut off, so the fade
   moves to that edge and the end of the name lands crisp. */
.tab-chip.is-long:hover .tab-chip-link {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 20px);
          mask-image: linear-gradient(to right, transparent 0, #000 20px);
}

/* No ellipsis. A name cut off at "Quotatio…" tells you no more than the space
   it occupies; a name that slides when you point at it tells you all of it.
   The slide happens only on hover, so the row is still when you are not
   asking it anything. */
.tab-chip-label {
  overflow: visible;
  white-space: nowrap;
  transform: translateX(0);
  /* No transition at rest. Anything declared here animates when the strip is
     drawn, which is movement nobody asked for on every page load. */
}
/* The slide is declared only on hover and only on the way back, so the row is
   completely still until you point at something. */
.tab-chip.is-long:hover .tab-chip-label {
  transform: translateX(var(--label-shift, 0px));
  /* Paced by distance rather than a fixed duration, so a long name and a
     barely-clipped one travel at the same readable speed. */
  transition: transform var(--label-time, .6s) linear var(--t-slower);
}
.tab-chip.is-long .tab-chip-label { transition: transform var(--t-base) linear; }
@media (prefers-reduced-motion: reduce) {
  .tab-chip-label,
  .tab-chip.is-long:hover .tab-chip-label { transition: none; transform: none; }
  /* Nothing slides, so the cut stays on the right and the fade stays with it. */
  .tab-chip.is-long:hover .tab-chip-link {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent 100%);
            mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent 100%);
  }
}
.tab-pin-mark { font-size: 13px; opacity: .6; flex-shrink: 0; }

.tab-chip-x {
  font-family: 'Material Symbols Rounded', sans-serif;
  font-size: 14px;
  line-height: 1;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  border-radius: 50%;
  padding: 2px;
  opacity: 0;
  flex-shrink: 0;
}
.tab-chip:hover .tab-chip-x,
.tab-chip.active .tab-chip-x { opacity: .5; }
.tab-chip-x:hover { opacity: 1; background: var(--gray-200); }
.tab-chip-x:focus-visible { opacity: 1; outline: 2px solid var(--primary); outline-offset: 1px; }
/* A pinned tab keeps its place until it is unpinned, so it offers no close
   control at all rather than one that quietly does nothing. */
.tab-chip.pinned { min-width: 0; }
.tab-chip.pinned .tab-chip-x { display: none; }
.tab-chip.pinned .tab-chip-link { padding-right: .55rem; }

@media (max-width: 780px) {
  .topbar-tabs { display: none; }   /* the strip is desk furniture */
}

/* ── Every MapLibre map on the platform ─────────────────────
   MapLibre leaves .maplibregl-canvas-container position:static with no
   explicit size. The <canvas> escapes it, so tiles look right — but every
   MARKER is a child of this container and is positioned by transform
   relative to ITS box, which collapses to a stray auto-height. Markers then
   land well below where they belong, and the attribution and scale controls
   sit outside the map's rounded box.

   map.php discovered this and fixed it in its own inline CSS; the second map
   (Attendance → Map) hit it again, which is how you know the rule belongs
   here rather than in one page. map.php keeps its own copy and the long
   version of this story. */
.maplibregl-canvas-container { position: absolute !important; inset: 0 !important; height: 100% !important; }

/* ── The live staff map (Attendance → Map) ──────────────────
   These live here rather than in a <style> on the page for one specific
   reason: MapLibre measures its container the moment it is constructed, and
   a <style> block further down the page has not been parsed yet at that
   point. With no height the canvas falls back to its intrinsic 300px, the
   style never finishes loading, and neither tiles nor markers ever appear.
   A stylesheet in <head> is in force before any of it runs.

   A fixed height, not a min-height: the card must not change size when the
   tiles arrive or when nobody is on site. */
.hrms-map { height: 30rem; border-radius: .5rem; overflow: hidden; }
/* The failure state keeps the same box, so nothing moves when the map cannot
   be loaded at all. */
.hrms-map-failed { display: flex; align-items: center; justify-content: center;
                   background: var(--gray-100, #f1f5f9); color: var(--gray-400, #94a3b8);
                   font-size: .85rem; }
/* Hollow is where somebody clocked in; filled is a position since then. */
.hrms-pin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #1D5FBF;
            background: transparent; box-shadow: 0 0 0 2px rgba(255,255,255,.9); cursor: pointer; }
.hrms-pin.is-live { background: #1D5FBF; }

/* ── The clock in the top bar ───────────────────────────────
   Sits with the other top-bar buttons and uses the same one, so it inherits
   every hover and focus rule rather than re-stating them. Only the popup and
   the little unread-style dot are new. */
.clockw { position: relative; display: inline-flex; }
.clockw-btn { position: relative; }
/* The dot says "there is something to do" without a number, because the only
   counts here are one and none. */
.clockw-dot {
  position: absolute; top: 6px; right: 6px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--warning);
  box-shadow: 0 0 0 2px var(--surface, #fff);
}
.clockw-btn.is-in .material-symbols-rounded { color: var(--success); }
.clockw-pop {
  position: absolute; top: calc(100% + .5rem); right: 0; z-index: 300;
  width: 17rem; padding: .85rem;
  background: var(--surface, #fff); border: 1px solid var(--gray-200);
  border-radius: 14px; box-shadow: 0 12px 30px rgba(15, 23, 42, .14);
  text-align: left;
}
.clockw-head { display: flex; flex-direction: column; gap: .1rem; margin-bottom: .5rem; }
.clockw-state { font-size: .95rem; font-weight: 700; color: var(--gray-900); }
.clockw-sub   { font-size: .74rem; color: var(--gray-500); }
/* A FIXED height, never min-height: this line changes as the state changes,
   and a popup that grows from one line to two makes everything under it jump. */
.clockw-note {
  height: calc(1.35em * 2); margin: 0 0 .6rem; font-size: .74rem; line-height: 1.35;
  color: var(--gray-500);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.clockw-go { width: 100%; justify-content: center; }
.clockw-ot { margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--gray-200); }
.clockw-ot label { display: block; font-size: .72rem; font-weight: 600; color: var(--gray-600); margin-bottom: .3rem; }
.clockw-otrow { display: grid; grid-template-columns: 5.5rem 1fr; gap: .4rem; margin-bottom: .45rem; }
.clockw-hint { margin: .4rem 0 0; font-size: .68rem; color: var(--gray-400); line-height: 1.35; }
.clockw-link {
  display: block; margin-top: .7rem; padding-top: .6rem; border-top: 1px solid var(--gray-200);
  font-size: .74rem; color: var(--primary); text-decoration: none;
}
.clockw-link:hover { text-decoration: underline; }
@media (max-width: 640px) {
  .clockw-pop { width: min(17rem, calc(100vw - 2rem)); right: -.5rem; }
}

/* ── Bill of lading: assigning containers ─────────────────────────────────
   Sits between the card header and the scrolling list of boxes, so the
   controls stay put while the list moves under them. */
.bl-assign-tools {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .6rem .9rem;
  border-bottom: 1px solid var(--gray-200);
  background: var(--gray-50);
}
.bl-assign-all {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .78rem; font-weight: 600; color: var(--gray-700);
  cursor: pointer; user-select: none;
}
.bl-assign-tools input[type="search"] { font-size: .78rem; padding: .3rem .5rem; }

/* ── B2B: what the customer said ──────────────────────────────────────────
   One block per point. A settled point stays on the page — the record of what
   was asked for and what was done about it is the useful part — but it steps
   back so the open ones read first. */
.b2b-fb {
  border-left: 3px solid var(--warning);
  background: var(--gray-50);
  border-radius: 0 6px 6px 0;
  padding: .6rem .8rem;
  margin-bottom: .6rem;
}
.b2b-fb.is-done { border-left-color: var(--gray-300); opacity: .72; }
.b2b-fb-body { font-size: .85rem; color: var(--gray-800); line-height: 1.45; }
.b2b-fb-meta { font-size: .7rem; color: var(--gray-500); margin-top: .25rem; }
.b2b-fb-act  { display: flex; gap: .4rem; align-items: center; margin-top: .5rem; }
.b2b-fb-act input[type="text"] { font-size: .75rem; padding: .25rem .45rem; }

/* ── HR schedule: the rota grid ───────────────────────────────────────────
   Dense by nature — thirty people across a fortnight — so the cells are tight
   and the name column stays put while the dates scroll under it. */
.rota-tools {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .6rem .9rem; border-bottom: 1px solid var(--gray-200); background: var(--gray-50);
}
.rota-table th, .rota-table td { padding: .3rem .35rem; text-align: center; vertical-align: middle; }
.rota-table th.is-weekend, .rota-table td.is-weekend { background: var(--gray-50); }
.rota-who {
  position: sticky; left: 0; z-index: 2; background: var(--surface, #fff);
  text-align: left !important; min-width: 11rem; white-space: nowrap;
}
.rota-table thead .rota-who { z-index: 3; }
.rota-dow { display: block; font-size: .72rem; font-weight: 600; }
.rota-dom { display: block; font-size: .66rem; color: var(--gray-500); font-weight: 400; }
.rota-hol { display: block; font-size: .6rem; color: var(--warning); }
.rota-cell { font-size: .72rem; padding: .15rem .2rem; min-width: 4.6rem; }
.rota-people {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: .3rem .8rem; max-height: 18rem; overflow-y: auto;
  border: 1px solid var(--gray-200); border-radius: 6px; padding: .6rem .7rem;
}
.rota-people label { display: flex; gap: .4rem; align-items: center; }
.rota-cell {
  width: 100%; min-width: 3.4rem; text-align: center; text-transform: uppercase;
  font-size: .74rem; padding: .18rem .1rem; border: 1px solid var(--gray-200); border-radius: 4px;
}
.rota-cell:focus { outline: 2px solid var(--primary); outline-offset: -1px; border-color: var(--primary); }
.rota-cell[data-dirty] { background: color-mix(in srgb, var(--warning) 16%, transparent); font-weight: 600; }
.rota-cell[data-differs]::placeholder { color: var(--gray-400); font-style: italic; }
.rota-legend { display: flex; gap: .7rem; flex-wrap: wrap; font-size: .68rem; color: var(--gray-500); }
.rota-legend b { color: var(--gray-700); }

/* ── HR schedule: the rota, read at a glance ──────────────────────────────
   A rota is scanned, not read. The colour says what kind of day it is before
   anybody parses the code, and the six shift tints repeat rather than growing
   without limit: past six an install has more shifts than a person can tell
   apart by colour, and the code is doing the work anyway. */
.rota-legend-row {
  display: flex; gap: .4rem; flex-wrap: wrap;
  padding: .5rem .9rem; border-bottom: 1px solid var(--gray-200);
}
.rota-key {
  font-size: .66rem; color: var(--gray-600); padding: .12rem .4rem; border-radius: 4px;
  background: var(--gray-100); white-space: nowrap;
}
.rota-key b { color: var(--gray-900); }
.rota-key em { color: var(--gray-500); font-style: normal; }
.rota-key.is-off   { background: var(--gray-200); }
.rota-key.is-leave { background: color-mix(in srgb, var(--info, #378ADD) 18%, transparent); }
.rota-key.is-hol   { background: color-mix(in srgb, var(--warning) 20%, transparent); }
.rota-key.is-blank { background: transparent; border: 1px dashed var(--gray-300); }

.rota-cellwrap { position: relative; border-radius: 4px; padding: 1px; }
.rota-cellwrap.is-off     { background: var(--gray-200); }
.rota-cellwrap.is-leave   { background: color-mix(in srgb, var(--info, #378ADD) 16%, transparent); }
.rota-cellwrap.is-holiday { background: color-mix(in srgb, var(--warning) 18%, transparent); }
.rota-cellwrap.is-clash   { box-shadow: inset 0 0 0 2px var(--danger); }
.rota-s0 { background: color-mix(in srgb, var(--primary) 14%, transparent); }
.rota-s1 { background: color-mix(in srgb, var(--success) 15%, transparent); }
.rota-s2 { background: color-mix(in srgb, #7F77DD 15%, transparent); }
.rota-s3 { background: color-mix(in srgb, #D85A30 14%, transparent); }
.rota-s4 { background: color-mix(in srgb, #1D9E75 15%, transparent); }
.rota-s5 { background: color-mix(in srgb, #D4537E 14%, transparent); }
.rota-cellwrap .rota-cell { background: transparent; border-color: transparent; }
.rota-cellwrap .rota-cell:focus { background: var(--surface, #fff); border-color: var(--primary); }
.rota-sub {
  display: block; font-size: .58rem; line-height: 1.1; color: var(--gray-500);
  text-align: center; padding-bottom: 1px; white-space: nowrap; overflow: hidden;
}
.rota-read { display: block; text-align: center; font-size: .74rem; padding: .18rem 0; }
.rota-tally { font-size: .64rem; color: var(--gray-500); }
.rota-tally .is-short { color: var(--danger); font-weight: 600; }
.rota-table th.is-holiday { background: color-mix(in srgb, var(--warning) 12%, transparent); }
.rota-table th.is-weekstart, .rota-table td.is-weekstart { border-left: 2px solid var(--gray-300); }

.rota-checks { padding: .6rem .9rem; border-bottom: 1px solid var(--gray-200); background: var(--gray-50); }
.rota-check {
  display: flex; gap: .4rem; align-items: flex-start; font-size: .76rem;
  padding: .25rem 0; color: var(--gray-700);
}
.rota-check.is-clash     { color: var(--danger); }
.rota-check.is-shortfall { color: var(--warning); }

/* ── HR: a person's file ──────────────────────────────────────────────────
   One block per reference, with the check that was actually made under it. */
.pf-ref { border-left: 3px solid var(--gray-200); padding: .5rem .7rem; margin-bottom: .7rem; }
.pf-photo {
  width: 96px; height: 96px; border-radius: 8px; object-fit: cover;
  border: 1px solid var(--gray-200); background: var(--gray-100);
}
.pf-photo-empty {
  display: flex; align-items: center; justify-content: center;
  color: var(--gray-400); font-size: 2rem;
}

/* ── HR schedule: building a rota ─────────────────────────────────────────
   One question, then its own controls. The four ways used to sit on screen
   together with fine print explaining which cancelled which. */
.bw-modes { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: .5rem; margin-bottom: 1rem; }
.bw-mode {
  display: flex; gap: .5rem; align-items: flex-start; cursor: pointer;
  border: 1px solid var(--gray-200); border-radius: 8px; padding: .6rem .7rem;
  font-size: .78rem; color: var(--gray-600); background: var(--surface, #fff);
}
.bw-mode:hover { border-color: var(--gray-300); }
.bw-mode:has(input:checked) {
  border-color: var(--primary); background: color-mix(in srgb, var(--primary) 7%, transparent);
}
.bw-mode span { display: block; line-height: 1.35; }
.bw-mode strong { display: block; color: var(--gray-900); font-size: .84rem; }
.bw-row { display: flex; gap: .9rem; flex-wrap: wrap; align-items: flex-start; margin-bottom: .8rem; }
.bw-days { display: flex; gap: .5rem; flex-wrap: wrap; padding-top: .3rem; font-size: .74rem; }
.bw-days label { display: inline-flex; gap: .25rem; align-items: center; }
.bw-note { font-size: .76rem; color: var(--gray-500); margin: 0 0 .8rem; line-height: 1.5; }
/* The chosen mode's own controls. No display rule on purpose: the script
   shows and hides these with the `hidden` attribute, and a display here would
   beat it and leave every mode on screen at once. */
.bw-opts { margin-bottom: .2rem; }
.bw-more { margin-bottom: .6rem; }
.bw-more > summary { font-size: .78rem; color: var(--primary); cursor: pointer; padding: .2rem 0; }

/* ── The design system gallery ─────────────────────────────────────────────
   Settings → Design System. The page that shows every control this product
   has, rendered by the same stylesheet the product uses, so a control that
   looks wrong here looks wrong everywhere and a control that is missing here
   does not exist.

   Its own classes live HERE rather than in a <style> block on the page,
   because a design system page that carries its own private CSS is the first
   thing that proves the rule is optional. */

/* The page is one full-width column of sections. It is NOT a settings tab made
   of cards: .settings-form packs its children into a 430px grid, which stood
   three of these side by side and left a two-column reference 200px wide. */
.ds-page > .card { margin-bottom: .875rem; }
.ds-page .card-title { font-size: .82rem; }
.ds-lead { font-size: .82rem; color: var(--gray-600); line-height: 1.7; max-width: 70ch; }

/* A labelled row: what it is called on the left, the live thing on the right.
   The label column is fixed so every row in every section lines up — a
   catalogue that does not line up reads as a pile. */
.ds-row {
  display: grid;
  grid-template-columns: 15rem 1fr;
  gap: 1.5rem;
  padding: 1rem 0;
  border-top: 1px solid var(--gray-100);
  align-items: start;
}
.ds-row:first-of-type { border-top: 0; padding-top: .25rem; }
.ds-row > dt { min-width: 0; font-size: .8rem; color: var(--gray-700); font-weight: 600; }
.ds-row > dd { min-width: 0; }

/* The name of the thing. Quiet: there is one on nearly every line, and a page
   of grey chips reads as a page of grey chips. */
.ds-name {
  font-family: var(--font-mono);
  font-size: .73rem;
  font-weight: 500;
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 7%, transparent);
  border-radius: var(--radius);
  padding: .05rem .3rem;
  word-break: break-word;
}
.ds-note { font-size: .74rem; color: var(--gray-500); margin-top: .35rem; line-height: 1.6; font-weight: 400; }
.ds-demo { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; min-width: 0; }
.ds-demo.is-stacked { flex-direction: column; align-items: stretch; gap: .5rem; }

/* The rules, as two columns of short paragraphs rather than one long list. */
.ds-rules {
  margin: .9rem 0 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: .75rem 1.5rem;
  font-size: .78rem; color: var(--gray-600); line-height: 1.6;
}
.ds-rules li { padding-left: 1rem; position: relative; }
.ds-rules li::before {
  content: ''; position: absolute; left: 0; top: .5em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--primary);
}
.ds-rules strong { color: var(--gray-800); font-weight: 600; }

/* Colour */
.ds-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: .5rem; }
.ds-swatch { border: 1px solid var(--gray-200); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.ds-swatch-chip { height: 2.4rem; border-bottom: 1px solid var(--gray-200); }
.ds-swatch-body { padding: .3rem .4rem; }
.ds-swatch-name { font-family: var(--font-mono); font-size: .67rem; color: var(--gray-700); display: block; }
.ds-swatch-val  { font-family: var(--font-mono); font-size: .64rem; color: var(--gray-400); }
.ds-group { margin-bottom: 1rem; }
.ds-group-label { font-size: .68rem; font-weight: 700; letter-spacing: .06em; color: var(--gray-400); margin-bottom: .4rem; }

/* Shape and elevation */
.ds-shape {
  width: 4.5rem; height: 3rem;
  background: var(--surface);
  border: 1px solid var(--gray-300);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: .62rem; color: var(--gray-500);
}
.ds-shadow { width: 5rem; height: 3rem; background: var(--surface); border-radius: var(--radius); }

/* Motion — a dot that travels the track, so two durations can be compared by
   eye rather than by reading a number. Replaying is a class the script toggles;
   with reduced motion asked for, the global floor collapses it and the dot
   simply appears at the end, which is the honest result. */
.ds-motion { display: flex; align-items: center; gap: .6rem; width: 100%; }
.ds-motion-track {
  position: relative; flex: 1; max-width: 20rem; height: 6px;
  background: var(--gray-100); border-radius: 999px;
}
.ds-motion-dot {
  position: absolute; top: -4px; left: 0;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--primary);
  transition: left var(--t-base) var(--ease);
}
.ds-motion.is-running .ds-motion-dot { left: calc(100% - 14px); }
.ds-motion[data-t="instant"] .ds-motion-dot { transition-duration: var(--t-instant); }
.ds-motion[data-t="fast"]    .ds-motion-dot { transition-duration: var(--t-fast); }
.ds-motion[data-t="base"]    .ds-motion-dot { transition-duration: var(--t-base); }
.ds-motion[data-t="slow"]    .ds-motion-dot { transition-duration: var(--t-slow); }
.ds-motion[data-t="slower"]  .ds-motion-dot { transition-duration: var(--t-slower); }
.ds-motion[data-e="out"] .ds-motion-dot { transition-timing-function: var(--ease-out); }
.ds-motion[data-e="in"]  .ds-motion-dot { transition-timing-function: var(--ease-in); }

/* A demo that needs a dark background to be visible at all (a button meant for
   the sidebar). Named, so it is not three inline styles. */
.ds-on-dark { background: var(--sidebar-bg); padding: .4rem .5rem; border-radius: var(--radius); }

/* The surfaces that do not follow the rules. */
.ds-outside td:first-child { font-family: var(--font-mono); font-size: .72rem; white-space: nowrap; }
.ds-outside td { vertical-align: top; }

@media (max-width: 760px) {
  .ds-row { grid-template-columns: 1fr; gap: .45rem; }
}
