/* Orbitron (variable) — self-hosted, per the Vorperian style guide. */
@font-face {
  font-family: 'Orbitron';
  src: url('../fonts/orbitron.woff2') format('woff2');
  font-weight: 300 900; font-display: swap;
}
:root {
  --vc-dark:#1b1b1f;
  /* Brand palette — Vorperian gold on black by default; base.html overrides these
     per-team from the owner's chosen primary/secondary colors. */
  --brand-primary:#ff9900;
  --brand-secondary:#0b0b0d;
}
body { font-family: "Liberation Sans", Arial, Helvetica, sans-serif; }

/* Cyberpunk headings + logo/brand: Orbitron, uppercase (Vorperian style guide). */
h1,h2,h3,h4,h5,h6,.display-1,.display-2,.display-3,.display-4,.display-5,.display-6,
.navbar-brand,.navbar-brand .fw-semibold,.bb-lockup span {
  font-family:'Orbitron', system-ui, -apple-system, sans-serif;
  text-transform:uppercase; letter-spacing:.03em; font-weight:800;
}
.navbar-brand,.bb-lockup span,.display-1,.display-2,.display-3,.display-4,.display-5,.display-6 { font-weight:900; }

/* Vorperian-by-default theming, all keyed off the brand vars so a team's own
   colors flow through when set. */
.navbar[data-bs-theme="dark"]{ background: var(--brand-secondary) !important; }
/* Buttons stay in the body font (never the Orbitron/uppercase heading treatment,
   even when nested in a heading context). */
.btn { font-family: "Liberation Sans", Arial, Helvetica, sans-serif; text-transform: none; }
/* Slim the top bar on desktop too (the mobile media query trims it further). */
.navbar { padding-top:.35rem; padding-bottom:.35rem; }
.navbar-brand { line-height:1.05; }
.navbar-brand img { height:38px; }
.btn-dark{ background:var(--brand-primary); border-color:var(--brand-primary); color:#0b0b0d; }
.btn-dark:hover,.btn-dark:focus{ background:var(--brand-primary); border-color:var(--brand-primary); color:#0b0b0d; filter:brightness(1.08); }
a:not(.btn):not(.bb-btn):not(.nav-link):not(.navbar-brand):not(.dropdown-item){ color:var(--brand-primary); }
a:not(.btn):not(.bb-btn):not(.nav-link):not(.navbar-brand):hover{ filter:brightness(1.12); }
.form-control:focus,.form-select:focus,.form-check-input:focus{
  border-color:var(--brand-primary);
  box-shadow:0 0 0 .2rem color-mix(in srgb, var(--brand-primary) 22%, transparent);
}
.form-check-input:checked{ background-color:var(--brand-primary); border-color:var(--brand-primary); }
.link-primary,.text-primary{ color:var(--brand-primary) !important; }

/* ===== Mobile responsiveness =====
   Safety net so no page scrolls sideways: clip stray horizontal overflow (clip,
   unlike hidden, doesn't create a scroll container so position:sticky still
   works), keep media within their box, and let wide content scroll inside its
   own table-responsive box rather than pushing the page wide. */
/* --ui-scale compensates for a browser laying the page out at the wrong size for the hardware —
   see static/js/uiscale.js. 1 (the default) is a no-op, so nothing here changes for a device that
   is already correct. Every rem in the app, and Bootstrap's, follows this. */
html {
  -webkit-text-size-adjust: 100%;
  font-size: calc(16px * var(--ui-scale, 1));
}
body { overflow-x: clip; }
img, canvas, iframe, svg, video { max-width: 100%; }
.table-responsive { -webkit-overflow-scrolling: touch; }

/* Long unbreakable values (emails, links, tokens) wrap instead of forcing width. */
a[href^="mailto:"], a[href^="tel:"], .text-wrap-any, code { overflow-wrap: anywhere; }

/* Sideways-scroll hint. scrollhint.js wraps any box that really is scrollable
   (a wide .table-responsive, the Settings tab strip) in .xscroll; we fade its
   right edge so the hidden columns read as "there's more", not as missing. The
   fade sits on the WRAPPER because it must not scroll away with the content, and
   because opaque table cells paint over anything drawn on the box itself. It's a
   veil rather than a fade-to-background so it works whatever colour the card is,
   and it clears once you've scrolled to the end. The veil INVERTS per theme — the
   app is dark by default, and a dark shadow on a dark card is invisible. */
.xscroll { position: relative; }
.xscroll::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 2.25rem;
  background: linear-gradient(to left, rgba(0, 0, 0, .38), rgba(0, 0, 0, 0));
  pointer-events: none; transition: opacity .15s linear;
}
[data-bs-theme="dark"] .xscroll::after {
  background: linear-gradient(to left, rgba(255, 255, 255, .16), rgba(255, 255, 255, 0));
}
.xscroll.xscroll-end::after { opacity: 0; }
/* On a phone the veil alone is easy to miss and the swipe is the only way to
   reach those columns, so add a brand edge rule. On desktop it would just be
   noise — the window can be widened instead — so the veil carries it there. */
@media (max-width: 767.98px) {
  .xscroll::after { border-right: 2px solid var(--brand-primary); }
}

/* E-sign document preview: stacked page images in a native-scroll box. iOS Safari
   won't scroll a multi-page PDF inside an <iframe> (it sticks on page 1), but a
   plain scroll container of <img> pages scrolls fine on every browser. */
.doc-preview {
  max-height: 70vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  background: #525659;
  padding: 8px;
}
/* A viewer whose page COUNT arrives over the network (data-count-url) can't reserve its space
   from its content: at first paint it holds one line of "Loading preview…", and it only learns
   how tall it will be a round trip later — so everything below it jumped down when the pages
   landed. max-height alone can't fix that (it constrains growth, it doesn't reserve). The pane
   therefore takes a DEFINITE height, the way the template-manager pane already did — which is
   why that page never jumped and this one did. Safari and the installed app show it worst
   because they paint the short box before the scripts at the end of <body> have even run. */
.doc-preview[data-count-url] { height: 70vh; }
/* EVERY page image, not only the ones inside .doc-preview. The public invoice, the e-sign pages
   and the contract field-marker all render .doc-page outside that wrapper, so they reserved no
   space at all and the page jumped as each one arrived — which is exactly what the owner saw. */
.doc-page {
  display: block;
  width: 100%;
  max-width: 760px;
  margin: 0 auto 8px;
  /* Reserve the letter-page footprint BEFORE the image loads, so the page never jumps
     when it pops in. Until it loads, a grey rectangle of exactly this size shows
     "Loading document"; the page image then draws over it. */
  aspect-ratio: 8.5 / 11;
  object-fit: contain;
  background: #d9d9d9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='30'%3E%3Ctext x='130' y='20' font-family='sans-serif' font-size='15' fill='%23808080' text-anchor='middle'%3ELoading document%3C/text%3E%3C/svg%3E") center no-repeat;
}

/* A page image that isn't inside the dark viewer still needs its footprint reserved; it just
   doesn't want the viewer's drop shadow fighting the page background. */
.doc-preview .doc-page { box-shadow: 0 0 5px rgba(0, 0, 0, .4); }

/* Page/card header action rows: wrap their buttons on narrow screens instead of
   overflowing. Any .d-flex holding .btn children is treated as a toolbar. */
.d-flex.gap-2 > .btn, .d-flex.gap-2 > form { flex: 0 0 auto; }
/* ...except a form that is itself a CLUSTER of controls (the overdue-reminder
   form: button + ordinal + "BCC me" + "Test to me"). Pinned at flex:0 0 auto it
   can't shrink below its max-content width, so on a phone it forced the document
   page to 584px — and since body is overflow-x:clip, "Test to me" was clipped
   away with no way to scroll to it. Marking such a form .flex-wrap opts it into
   shrinking so it wraps its own controls onto extra lines instead. */
.d-flex.gap-2 > form.flex-wrap { flex: 0 1 auto; min-width: 0; }

@media (max-width: 575.98px) {
  main.container-fluid { padding-left: .75rem !important; padding-right: .75rem !important; }
  .card-body { padding: .85rem; }
  h1.h3 { font-size: 1.3rem; }
  .stat-card .stat-value { font-size: 1.35rem; }
  /* Toolbars / button rows wrap by default on phones. */
  .toolbar, .page-actions, .d-flex.gap-2:not(.flex-nowrap) { flex-wrap: wrap; }
  .btn-group.flex-wrap-sm { flex-wrap: wrap; }
}
/* ---- Dashboard "Edit layout" mode ----
   View mode is clean (no widget chrome). Edit mode reveals the Add/Reset controls and a × on
   each tile, outlines tiles as draggable, and makes their inner links/buttons inert so a drag
   or a stray tap never navigates. SortableJS keeps tiles compacted as one is moved. */
.dash-edit-only { display: none; }
.dash-editing .dash-edit-only { display: inline-flex; }
.dash-edit-hint { display: none; }
.dash-editing .dash-edit-hint { display: block; }
.dash-del {
  display: none; border: 0; background: transparent; color: var(--bs-danger);
  font-size: 1.15rem; line-height: 1; padding: 0 .3rem; cursor: pointer;
}
.dash-editing .dash-del { display: inline-flex; }
/* THE GRAB AREA, and `touch-action: none` on it is LOAD-BEARING — not styling.
   A touch that begins on a touch-action:auto surface is claimed by the compositor as soon as the
   finger slides, and from then on the browser DISCARDS programmatic scrolling: measured on an
   iPhone mid-drag, we asked for 11216px of edge auto-scroll and the page moved 4px. Starting the
   drag on a touch-action:none element keeps the gesture on the main thread — the same test then
   moves 1241px of 1750 (620px/s vs 257px/s).
   It is INSET from the tile's edges because touch-action:none also disables NATIVE scrolling
   wherever it applies: the band of tile left around the pad is what you swipe to scroll the page,
   with the browser's own inertia. Percentage insets so a short stat card and a tall table card
   both keep a usable band. */
.dash-editing #dash-grid [data-mod] { cursor: grab; }
.dash-editing #dash-grid [data-mod].sortable-chosen { cursor: grabbing; }
/* Holding a widget must not also start a text selection on its figures (and on iOS raise the
   copy/look-up callout) underneath the drag. Suppressed only while editing — the numbers stay
   selectable in view mode. -webkit-touch-callout is the iOS-only half; Blink ignores it. */
.dash-editing #dash-grid [data-mod] {
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
.dash-editing #dash-grid [data-mod] .card,
.dash-editing #dash-grid [data-mod] .stat-card {
  outline: 1px dashed var(--brand-primary); outline-offset: -1px;
}
/* Inert content while editing — a drag or a stray tap must never navigate. */
.dash-editing #dash-grid [data-mod] a,
.dash-editing #dash-grid [data-mod] button:not(.dash-del),
.dash-editing #dash-grid [data-mod] input,
.dash-editing #dash-grid [data-mod] form { pointer-events: none; }

.dash-drag-pad { display: none; }
.dash-editing #dash-grid [data-mod] { position: relative; }
.dash-editing #dash-grid [data-mod] .dash-drag-pad {
  display: flex; align-items: center; justify-content: center;
  position: absolute; inset: 15% 10%; z-index: 3; border-radius: .9rem;
  touch-action: none; cursor: grab;
  -webkit-user-select: none; user-select: none;
  /* Frosted glass rather than a coloured box: a neutral veil plus a slight blur reads as a
     panel laid OVER the widget, and keeps the figures legible underneath. */
  background: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
  -webkit-backdrop-filter: blur(1.5px); backdrop-filter: blur(1.5px);
  /* Fade the panel out towards its own edges so it blends into the tile instead of ending in a
     hard rectangle — and so the band around it reads as "not part of the handle". */
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 40%, transparent 100%);
  mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 40%, transparent 100%);
  transition: background .12s ease;
}
/* The colour change means UNDOCKED, not "touched". :active fires the instant a finger lands, so
   tinting on it told you the widget was grabbed while the press-and-hold delay was still running —
   a lie about the state, and the only thing distinguishing a press that will become a drag from a
   swipe that will scroll the page. .sortable-chosen is added at exactly the instant the delay is
   satisfied, so the tint now lands with the lift. The CURSOR still changes on press: that is a
   desktop affordance, not a claim about what happened. */
.dash-editing #dash-grid [data-mod] .dash-drag-pad:active { cursor: grabbing; }
.dash-editing #dash-grid [data-mod].sortable-chosen .dash-drag-pad {
  cursor: grabbing;
  background: color-mix(in srgb, var(--brand-primary) 24%, transparent);
}
.dash-drag-pad-label {
  font-size: .68rem; font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
  color: var(--bs-body-color); text-align: center; white-space: nowrap;
  padding: .16rem .5rem; border-radius: .5rem;
  /* Its own chip, nearly opaque: the label has to stay readable over a table row or a bar chart,
     and it sits at the centre of the mask where the panel is at full strength. */
  background: color-mix(in srgb, var(--bs-body-bg) 84%, transparent);
  box-shadow: 0 1px 6px rgba(0, 0, 0, .22);
}
/* A short tile keeps the words, just smaller — they must be legible on every widget. */
.dash-drag-pad.is-tight .dash-drag-pad-label { font-size: .56rem; padding: .1rem .38rem; }
/* On a short tile the panel is only ~55px tall, so a fade starting at 40% swallows the label.
   Hold full strength across the middle and fade only at the very edge. */
.dash-drag-pad.is-tight {
  -webkit-mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 72%, transparent 100%);
  mask-image: radial-gradient(ellipse closest-side at 50% 50%, #000 72%, transparent 100%);
}

.dash-ghost { opacity: .4; }
/* "You've got it" feedback the moment the press-and-hold lands. It has to be VISUAL: iOS plays
   its haptic only for a real finger on a real control, so a programmatic tap can't buzz an
   iPhone at all (probed on iOS 26.5.2 — see plan.xml). Sortable adds .sortable-chosen at exactly
   the instant the delay is satisfied, before the tile has moved, so the lift lands on the beat
   the finger is waiting for. .sortable-drag is the floating clone that follows it. */
.dash-editing #dash-grid [data-mod].sortable-chosen .card,
.dash-editing #dash-grid [data-mod].sortable-chosen .stat-card,
.sortable-drag .card, .sortable-drag .stat-card {
  transform: scale(1.04);
  box-shadow: 0 .6rem 1.2rem rgba(0, 0, 0, .45);
  transition: transform .1s ease-out, box-shadow .1s ease-out;
}

/* Admin section tiles — a calm, tappable grid instead of a wall of buttons. */
.admin-tile { color: inherit; transition: border-color .12s, transform .06s; }
.admin-tile:hover { border-color: var(--brand-primary); }
.admin-tile:active { transform: translateY(1px); }

.stat-card { border:1px solid var(--bs-border-color); border-radius:.6rem; background:var(--bs-body-bg); }
.stat-card .stat-value { font-size:1.6rem; font-weight:600; line-height:1.1; }
.stat-card .stat-label { font-size:.8rem; text-transform:uppercase; letter-spacing:.04em; color:var(--bs-secondary-color); }
/* Dashboard module controls (↑ ↓ ✕) sit beside the title in a flex row. A long
   title used to push them past the card's right border on a phone ("Outstanding
   A/R" overflowed by 3px): the title's longest word sets a min-content width the
   row can't shrink below. Let the title break mid-word and give way; keep the
   button group at its natural size. */
.stat-card .stat-label { min-width: 0; overflow-wrap: anywhere; }
.stat-card .btn-group { flex-shrink: 0; }
.table-sm td, .table-sm th { vertical-align:middle; }
.login-wrap { max-width:380px; margin:8vh auto; }
.login-logo { height:64px; }
.badge-status { text-transform:capitalize; }
.money { font-variant-numeric: tabular-nums; }

/* Audit-log actor styling: platform admins (super-admins + global service
   accounts) in Vorperian gold with a crown; agent actions get a muted gear. */
.actor-admin { color: #ff9900; font-weight: 600; }
.actor-agent { color: var(--bs-secondary-color); font-size: .85em; }

/* Line-item cells that are empty render as "XXX" on the document — grey them
   out in the builder and show an XXX cue so it's clear they're intentionally blank. */
input.li-empty { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); }
input.li-empty::placeholder { color: var(--bs-secondary-color); opacity: 1; }
input.li-total { background: var(--bs-secondary-bg); }

/* No number-input spinner "adjustment arrows" on line-item cells (they show on
   hover/focus otherwise). Fields are still editable by typing. */
input.li-xxx::-webkit-outer-spin-button,
input.li-xxx::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input.li-xxx[type="number"] { -moz-appearance: textfield; appearance: textfield; }

/* Locked XXX cells on catalog line items (field not set for that item). */
input.li-locked { cursor: not-allowed; background: var(--bs-secondary-bg); }

/* ---- Dark-mode readability ----
   Several Bootstrap helpers are hard-coded to the LIGHT palette (light bg +
   black text). In dark mode they render as glaring light bars, and any text we
   theme separately can disappear on them. Re-map them to theme variables so
   they stay legible in both modes. */
[data-bs-theme="dark"] .table-light {
  --bs-table-color: var(--bs-body-color);
  --bs-table-bg: var(--bs-tertiary-bg);
  --bs-table-border-color: var(--bs-border-color);
  --bs-table-striped-color: var(--bs-body-color);
  --bs-table-striped-bg: var(--bs-secondary-bg);
  --bs-table-hover-color: var(--bs-body-color);
  --bs-table-hover-bg: var(--bs-secondary-bg);
  color: var(--bs-body-color);
  border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .text-bg-light {
  color: var(--bs-body-color) !important;
  background-color: var(--bs-tertiary-bg) !important;
}

/* Highlighted (confidential) note: yellow background needs dark text in BOTH
   modes, or the theme's light body text vanishes on the yellow. */
.note-highlight { background: #fff3a0; color: #212529; }

/* Dashboard magnitude bars: use the theme's emphasis color so the fill
   contrasts with the card in light AND dark mode (was a fixed near-black). */
.bar-fill { background: var(--bs-emphasis-color); }

/* Outline buttons are hard-coded to the light palette: btn-outline-dark is
   black (invisible on a dark page) and btn-outline-secondary is a low-contrast
   grey. Re-map them to theme vars so they stay visible in dark mode — used by
   the Documents Quotes/Invoices toggle, the dashboard Reset, Edit buttons, etc. */
[data-bs-theme="dark"] .btn-outline-dark {
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-hover-color: var(--bs-body-bg);
  --bs-btn-hover-bg: var(--bs-body-color);
  --bs-btn-hover-border-color: var(--bs-body-color);
  --bs-btn-active-color: var(--bs-body-bg);
  --bs-btn-active-bg: var(--bs-body-color);
  --bs-btn-active-border-color: var(--bs-body-color);
}
[data-bs-theme="dark"] .btn-outline-secondary {
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-hover-color: var(--bs-body-bg);
  --bs-btn-hover-bg: var(--bs-secondary-color);
  --bs-btn-hover-border-color: var(--bs-secondary-color);
  --bs-btn-active-color: var(--bs-body-bg);
  --bs-btn-active-bg: var(--bs-secondary-color);
}
/* Solid dark button (Save, Record payment/expenditure, etc.): a near-black
   button on a dark page has no visible edge. Invert it to a light button in
   dark mode so it reads as the primary action. */
[data-bs-theme="dark"] .btn-dark {
  --bs-btn-bg: var(--bs-emphasis-color);
  --bs-btn-border-color: var(--bs-emphasis-color);
  --bs-btn-color: var(--bs-body-bg);
  --bs-btn-hover-bg: var(--bs-secondary-color);
  --bs-btn-hover-border-color: var(--bs-secondary-color);
  --bs-btn-hover-color: var(--bs-body-bg);
  --bs-btn-active-bg: var(--bs-secondary-color);
  --bs-btn-active-border-color: var(--bs-secondary-color);
  --bs-btn-active-color: var(--bs-body-bg);
}

/* Navbar: always keep the logo (brand) and the hamburger toggler on ONE row on
   mobile. The trimmed logo is wide, so with the brand text + tenant badge the
   toggler used to wrap to a second line. The reason min-width:0 alone wasn't
   enough: flexbox decides whether to WRAP an item using its *hypothetical* width
   (the content width), which min-width doesn't reduce — so the wide brand pushed
   the toggler off before any shrinking happened. Setting flex-basis:0 makes the
   brand's hypothetical width zero, so it can never bump the toggler off the row;
   it then grows to fill the space (content left-aligned + ellipsized) while the
   toggler sits at the far right. Scoped to <992px; the desktop layout and the
   drop-down collapse menu are untouched. */
.navbar-toggler { flex-shrink: 0; }
@media (max-width: 991.98px) {
  /* Slim the bar so a bigger logo doesn't make it tall. */
  .navbar { padding-top: .25rem; padding-bottom: .25rem; }
  .navbar > .container-fluid > .navbar-brand {
    flex: 1 1 0;            /* hypothetical width 0 -> never wraps the toggler */
    min-width: 0;
    overflow: hidden;
    flex-wrap: nowrap;      /* keep logo + name + badge on ONE line (don't stack) */
    white-space: nowrap;    /* also covers the case it's laid out as inline content */
  }
  .navbar-brand img { height: 38px; flex-shrink: 0; }   /* scale down, keep aspect */
  .navbar-brand > span { flex: 0 1 auto; }              /* the text/badge give way, not the logo */
  .navbar-brand .fw-semibold,
  .navbar-brand .badge {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }
}

/* ---------------------------------------------------------------------------------------------
   THE DOCK STACK — bottom-right of the viewport: notifications (permanent, nearest the thumb)
   and the setup checklist (temporary, stacked above it).

   Notifications used to be a bell in the navbar, where a 340px dropdown hanging off a 44px icon
   is fiddly on a phone and invisible on a laptop. Docked, both are one tap from anywhere, and
   both are native <details> so they collapse with NO JavaScript — a panel covering the screen
   with no way to shut it is an obstacle, which is exactly what the first attempt shipped.

   SIZE IS THE WHOLE PROBLEM ON A PHONE: a fixed narrow column, a hard height cap on each panel,
   and only one open at a time (docks.js). The stack hides entirely in dashboard edit mode — it
   sits exactly where a widget gets dragged and would eat both the drop and the edge auto-scroll.
   --------------------------------------------------------------------------------------------- */
.dockstack {
  /* CLEAR OF THE CORNER. A phone screen's corners are ROUNDED, and a round button parked in one
     with a 12px margin is half-hidden behind the curve — which is exactly what shipped. The inset
     is now large enough that a 4rem button sits inside the straight edges, and the safe-area
     insets are added on top for the day the app runs edge-to-edge (they resolve to 0 today, so
     the fallback is what does the work). */
  position: fixed;
  right: calc(1.25rem + env(safe-area-inset-right, 0px));
  bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
  z-index: 1035;                                            /* under modals (1055), over content */
  display: flex; flex-direction: column; align-items: stretch; gap: .5rem;
  width: min(21rem, calc(100vw - 1.5rem));
  pointer-events: none;                       /* the gaps between docks stay click-through */
}
.dockstack > * { pointer-events: auto; }
.dash-editing .dockstack { display: none; }

/* Panel above pill, so the pill stays anchored at the corner and the list grows upward — the
   thumb never has to move to collapse it again. */
.dock { display: flex; flex-direction: column-reverse; align-items: stretch; gap: .4rem; }

.dock__pill {
  display: flex; align-items: center; gap: .45rem; cursor: pointer; list-style: none;
  margin-left: auto; width: fit-content; max-width: 100%;
  border: 1px solid var(--bs-border-color); border-radius: 999px;
  background: var(--bs-body-bg); color: var(--bs-body-color);
  padding: .45rem .85rem; font-size: .85rem; font-weight: 600;
  box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .18);
}
/* An OVAL, not a circle. A round icon button is a small target however big you draw it, and it
   says nothing; the word pads it out horizontally and makes the whole pill the thing you press.
   3.25rem tall clears the 44px iOS and 48dp Android minimums on the short axis, and the label
   gives it ~11rem on the long one. It is the one control on every page of the app. */
.dock__pill--wide {
  min-height: 3.25rem; padding: 0 1.15rem; gap: .55rem; font-size: .95rem;
  background: var(--brand-primary); border-color: transparent; color: #0b0b0d;
  box-shadow: 0 .4rem 1.2rem rgba(0, 0, 0, .35);
}
.dock__pill--wide:hover { filter: brightness(1.08); }
.dock__pill--wide .dock__spark { font-size: 1.35rem; }
.dock__pill--wide .dock__count {
  min-width: 1.5rem; font-size: .8rem; line-height: 1.5rem; padding: 0 .35rem; text-align: center;
}
.dock__pill::-webkit-details-marker { display: none; }
.dock__pill::marker { content: ""; }
.dock__pill:hover { border-color: var(--brand-primary); }
.dock__spark { line-height: 1; }
.setup-dock__pill .dock__spark { color: var(--bs-warning); }
.dock__count {
  background: color-mix(in srgb, var(--bs-warning) 22%, transparent);
  border-radius: 999px; padding: .05rem .45rem; font-variant-numeric: tabular-nums;
}
/* Unread notifications are a call to action, not a progress figure. */
#notif-dock .dock__count { background: var(--bs-danger); color: #fff; font-weight: 700; }
#notif-dock .dock__count.d-none { display: none; }
.dock__chev { transition: transform .15s ease; font-size: .7rem; }
.dock[open] .dock__chev { transform: rotate(180deg); }

.dock__panel {
  box-shadow: 0 .75rem 2rem rgba(0, 0, 0, .28);
  max-height: min(48vh, 24rem); overflow: auto;
}
.dock__scroll { max-height: min(42vh, 21rem); overflow-y: auto; }
/* Installed to the home screen, the home indicator owns the bottom strip — lift clear of it. */
html.pwa-standalone .dockstack { bottom: calc(2.4rem + env(safe-area-inset-bottom, 0px)); }
/* The setup checklist, PINNED at the top of the feed. Tinted and sticky so it reads as "this
   stays until you deal with it" rather than as the oldest notification in the list. */
.notif-pinned {
  position: sticky; top: 0; z-index: 2;
  background: color-mix(in srgb, var(--bs-warning) 12%, var(--bs-body-bg));
  border-bottom: 1px solid var(--bs-border-color);
}
.notif-pinned .progress { height: 5px; }
.notif-pinned__steps { list-style: none; margin: 0; padding: 0; }
.notif-pinned__steps li { display: flex; align-items: center; gap: .5rem; padding: .3rem 0; }
.notif-pinned__done summary { cursor: pointer; }

@media (max-width: 575.98px) {
  .dockstack {
    right: calc(1.1rem + env(safe-area-inset-right, 0px));
    bottom: calc(1.6rem + env(safe-area-inset-bottom, 0px));
    width: min(19rem, calc(100vw - 2.2rem));
  }
  .dock__panel { max-height: 42vh; }
  .dock__scroll { max-height: 36vh; }
  /* The label stays on a phone: it is what gives the button its width, and width is what makes
     it hittable. (It used to be hidden here, which is how the button ended up as a small circle
     tucked into the screen's rounded corner.) */
  .dock__pill--wide { min-height: 3.25rem; padding: 0 1.1rem; font-size: .92rem; }
}

/* The × on a widget must stay clickable in EDIT MODE, where you actually need it. The drag pad is
   inset only 15%/10% and sits at z-index 3, so on a short stat card it lands directly over the
   button — the one control edit mode exists for was the one the handle swallowed. Raised above
   the pad, and given a real touch target: it is a 1.15rem glyph, which is not a tap area. */
.dash-editing #dash-grid [data-mod] .dash-del {
  position: relative; z-index: 4; pointer-events: auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.9rem; min-height: 1.9rem; border-radius: 50%;
  background: color-mix(in srgb, var(--bs-body-bg) 86%, transparent);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .2);
}
.dash-editing #dash-grid [data-mod] .dash-del:hover {
  background: color-mix(in srgb, var(--bs-danger) 16%, var(--bs-body-bg));
}


/* BACK / UP control — one step toward the dashboard from wherever you are (services/navigation).
   A BREADCRUMB, not a button: the first version was a bordered pill on its own row, which took a
   full line of vertical space on every page and competed with the page heading and its action
   buttons for attention. It is a wayfinding hint, so it reads like one — small, quiet, tucked
   above the title — while keeping a 44px tap target through padding rather than bulk. */
.app-back {
  display: inline-flex; align-items: center; gap: .15rem;
  /* Small to look at, generous to hit: the padding carries the touch target (~40px) and the
     negative margins take that space back out of the layout, so it still reads as a quiet line
     above the title rather than a button. Making it visually smaller shrank the target to 29px,
     which the check caught. */
  margin: -.7rem 0 -.1rem -.45rem; padding: .85rem .6rem .85rem .45rem;
  color: var(--bs-secondary-color); text-decoration: none;
  font-size: .82rem; font-weight: 600; line-height: 1;
}
.app-back:hover { color: var(--brand-primary); }
.app-back__chev { font-size: 1.05rem; line-height: .8; margin-top: -.08rem; }


/* PAGE HEADER ROWS ON A PHONE. Nearly every page opens with a
   `d-flex justify-content-between` holding the title and its action buttons. With no wrap the
   two fight over ~390px: the title breaks mid-word and the buttons stack into a narrow column
   beside it. Letting the row wrap puts the buttons on their own line, where they are both
   readable and hittable. Scoped to direct children of <main> so it only ever affects page
   headers, not the many flex rows inside cards and tables. */
@media (max-width: 575.98px) {
  main > .d-flex.justify-content-between { flex-wrap: wrap; row-gap: .5rem; }
  main > .d-flex.justify-content-between > .d-flex { flex-wrap: wrap; }
}
