/* Custom Engineering Concepts — dashboard
   The palette, the type pairing and the surface language come from the CEC
   site (ceconceptz.com): Space Grotesk over Inter, cyan on near-black, panels
   that fade from --surface-1 down to --surface-grad. Chart colours are drawn
   from the same ramp as the chrome, so charts sit on the page as one system. */

:root {
  color-scheme: light;

  /* Cyan is the brand hue, but it only works bright on a dark ground: on white
     #22d3ee manages 1.7:1. Light mode therefore runs the same hue several
     steps down the ramp, so the identity survives and the text stays legible. */
  --accent:      #0a7086;
  --accent-2:    #0b7c93;
  --accent-ink:  #ffffff;
  --grad: linear-gradient(120deg, var(--accent), var(--accent-2));

  --plane:          #f6f8f9;
  --surface-1:      #ffffff;
  --surface-grad:   #f1f4f7;   /* foot of the panel gradient */
  --surface-2:      #eef1f4;
  --surface-sunk:   #eaeef1;

  --text-primary:   #0b1116;
  --text-secondary: #45505c;
  /* Muted still has to clear 4.5:1 — on the sunk sidebar as well as the card
     surface. #616d7a manages only 4.4:1 there, which reads as haze. */
  --text-muted:     #5a6673;
  --link:           #0b6f85;

  --grid:           #e3e8ec;
  --axis:           #bfc8d1;
  --border:         rgba(11, 17, 22, 0.11);
  --border-strong:  rgba(11, 17, 22, 0.20);
  /* A form field sits on the same surface as the card around it, so its border
     is the only thing identifying it as a control. WCAG wants 3:1 for that,
     which is a good deal stronger than a decorative panel rule. */
  --field-border:   #84919f;
  /* Scrollbar: a shade off the surface, not a contrast element. */
  --scroll-thumb:       rgba(11, 17, 22, 0.22);
  --scroll-thumb-hover: rgba(11, 17, 22, 0.38);

  --series-1:       #0a7086;
  --series-1-soft:  rgba(10, 112, 134, 0.11);
  /* "Paid" needs to read as its own thing next to a cyan accent, so it takes
     the site's violet rather than a blue that would blur into the brand. */
  --paid:           #5b4bd6;

  /* ordinal ramp — receivables ageing. One hue, light to dark, every step
     >= 3:1 on the card surface (the light steps of the cyan ramp are not). */
  --ord-1: #0e93ad;
  --ord-2: #0b7c93;
  --ord-3: #0a6577;
  --ord-4: #094e5c;
  --ord-5: #073a44;

  --good:     #12a05e;
  --warning:  #fab219;
  --serious:  #ec835a;
  --critical: #c53030;
  --good-text: #0a6b3d;

  --radius:      14px;
  --radius-sm:   9px;
  --radius-pill: 99px;
  --sidebar-w:   240px;

  /* Space Grotesk throughout — the face the stat figures are set in, asked for
     as the app's one font. Inter stays in the stack behind it as the fallback
     while the webfont loads or if it fails to. */
  --font: "Space Grotesk", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Space Grotesk", "Inter", system-ui, sans-serif;

  --dur:  160ms;
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);

  /* Type scale — five steps, each ~1.25x, so hierarchy comes from real jumps
     rather than one-pixel differences. Nothing sits below 12px: under that,
     labels stop being read and start being squinted at. */
  --t-micro: 12px;   /* table headers, badges, meta */
  --t-small: 13px;   /* secondary body, captions */
  --t-body:  14.5px; /* default */
  --t-lead:  17px;   /* card and section titles */
  --t-title: 21px;   /* page titles */
  --t-hero:  30px;   /* stat values */

  /* Small text needs more leading, not less. */
  --lh-tight: 1.3;
  --lh-body:  1.55;
  --lh-loose: 1.65;

  /* Uppercase needs generous tracking or the letters jam together. */
  --track-caps: 0.075em;
}

/* Dark is the default and the one that matches the site exactly: these are the
   site's own hex values, not an approximation of them. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --accent:      #22d3ee;
  --accent-2:    #3fd0ff;
  --accent-ink:  #04141a;

  --plane:          #0a0b0d;
  --surface-1:      #14171c;
  --surface-grad:   #101216;
  --surface-2:      #191d24;
  --surface-sunk:   #0c0e12;

  --text-primary:   #f2f4f7;
  --text-secondary: #c3cbd6;
  --text-muted:     #9aa3af;
  --link:           #3fd0ff;

  --grid:           #1c2027;
  --axis:           #2d333d;
  --border:         #23272f;
  --border-strong:  #2d333d;
  --field-border:   #5a6673;
  --scroll-thumb:       #2b313a;
  --scroll-thumb-hover: #3c4450;

  --series-1:       #22d3ee;
  --series-1-soft:  rgba(34, 211, 238, 0.13);
  --paid:           #9d90ff;

  /* Ageing ramp on the dark surface: the light steps of the cyan ramp are the
     readable ones here, so the ramp is anchored bright and runs down to the
     mid steps. Every step clears 3:1 on --surface-1 and each bar is directly
     labelled, so nothing rests on colour alone. */
  --ord-1: #c9f7fd;
  --ord-2: #7fecfa;
  --ord-3: #22d3ee;
  --ord-4: #0ba7c4;
  --ord-5: #0d7f96;

  --good:     #34d399;
  --serious:  #f0946e;
  --critical: #f2555a;
  --good-text: #34d399;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--plane);
  color: var(--text-primary);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Safari inflates the text of any block it decides is a main column when the
     phone is turned to landscape. On a dashboard that is not a courtesy — the
     stat figures and the table rows change size mid-rotation. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); }

/* Scrollbars.
 *
 * `color-scheme` already asks the browser for a dark scrollbar, but its idea
 * of dark is a mid grey that sits noticeably lighter than these surfaces and
 * reads as a bright stripe down the side of every panel. These pull the track
 * to the surface colour and leave only the thumb visible, so the bar registers
 * as part of the panel until you need it.
 *
 * Standard properties first — Firefox, and Chrome 121+ honour them. The
 * -webkit- rules below cover older Chrome and Safari. Deliberately not a
 * re-drawn scrollbar: same widths, same behaviour, only the colours change. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scroll-thumb) transparent;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border-radius: 99px;
  /* Inset by a transparent border rather than a smaller width, so the thumb
     keeps a full-size hit target while looking slim. */
  border: 3px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

:where(h1, h2, h3) { text-wrap: balance; }

/* Every transition and the two looping indicators answer to this. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ------------------------------------------------------------ layout --- */

/* 100vh on iOS is the height of the window with the URL bar *collapsed*, so a
   full-height element is taller than what you can actually see and its foot
   sits under the browser chrome. dvh is the live value. The vh line stays as
   the fallback for anything that does not know dvh. */
.shell { display: flex; min-height: 100vh; min-height: 100dvh; }

.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background: var(--surface-sunk);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
}

.brand {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px 16px 18px;
  border-bottom: 1px solid var(--border);
}

/* The site opens with a cyan wash behind its mark. One instance of it here, at
   the head of the sidebar, is enough to make the two surfaces read as the same
   brand; any more and it becomes decoration competing with the data. It paints
   under the logo rather than around it, so the mark's own blend is untouched. */
.sidebar { position: relative; }
.sidebar::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 260px;
  background: radial-gradient(220px 150px at 50% 12%,
    color-mix(in srgb, var(--accent) 13%, transparent), transparent 70%);
  pointer-events: none;
}
.brand, nav, .sidebar-foot { position: relative; }

/* The logo is a grayscale JPG on a white ground. Rather than ship two files,
   blend the ground away: multiply drops white on the light surface, and in
   dark mode the image is inverted first so screen drops the resulting black.
   Marks stay crisp in both themes. */
.brand-logo {
  width: 100%;
  max-width: 176px;
  height: auto;
  display: block;
  mix-blend-mode: multiply;
}

/* invert(1) alone, deliberately: the site can afford the extra brightness it
   applies because its mark sits on a plain hero. Lifting the ground here turns
   the inverted black to grey, and `screen` then leaves a visible box. */
:root[data-theme="dark"] .brand-logo {
  filter: invert(1);
  mix-blend-mode: screen;
}

nav { flex: 1; overflow-y: auto; padding: 14px 10px; }
.nav-group { margin-bottom: 18px; }
.nav-group-label {
  font-family: var(--font-display);
  font-size: var(--t-micro); font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted);
  padding: 0 8px 8px;
}
.nav-item {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border-radius: var(--radius-sm);
  color: var(--text-secondary); cursor: pointer;
  font-size: var(--t-body); line-height: var(--lh-tight);
  user-select: none;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
/* Hover states are guarded on every control a finger lands on. Safari keeps
   the last tapped element in :hover until you tap something else, so an
   unguarded rule leaves a row looking selected long after you moved on. */
@media (hover: hover) {
  .nav-item:hover { background: var(--surface-2); color: var(--text-primary); }
}
.nav-item:active { background: var(--surface-2); }
.nav-item.active { background: var(--series-1-soft); color: var(--series-1); font-weight: 600; }
.nav-count {
  font-size: var(--t-micro); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.nav-item.active .nav-count { color: var(--series-1); }

/* Unread mail is a call to action, not a total, so it is filled rather than
   set in muted ink like the record counts beside it. */
.nav-unread {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  text-align: center;
  line-height: 1.5;
}
.nav-item.active .nav-unread { color: var(--accent-ink); }

.sidebar-foot { padding: 12px; border-top: 1px solid var(--border); }
.sync-box { margin-bottom: 8px; }
.sync-row {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--t-small); color: var(--text-secondary);
  margin-bottom: 9px; line-height: var(--lh-tight);
}
.sync-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-muted); flex: 0 0 7px;
}
.sync-dot.ok { background: var(--good); }
.sync-dot.busy { background: var(--warning); animation: pulse 1.1s ease-in-out infinite; }
.sync-dot.err { background: var(--critical); }
@keyframes pulse { 50% { opacity: 0.3; } }

.theme-toggle {
  width: 100%; padding: 7px; font-size: var(--t-micro);
  background: none; border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text-muted);
  cursor: pointer; font-family: inherit;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
.sign-out { margin-top: 6px; }
.sign-out:hover { color: var(--critical); border-color: var(--critical); }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* The site's header goes translucent and blurs what scrolls under it. Here the
   bar is sticky over a scrolling table, so the same treatment is doing work:
   it keeps the boundary legible instead of a hard opaque band. */
.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 0 24px; height: 60px;
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--plane) 82%, transparent);
  backdrop-filter: blur(14px);
  position: sticky; top: 0; z-index: 20;
}
@supports not (backdrop-filter: blur(1px)) {
  .topbar { background: var(--plane); }
}
.crumb {
  font-family: var(--font-display);
  font-size: var(--t-lead); font-weight: 600; letter-spacing: -0.01em;
}
.topbar-actions { display: flex; align-items: center; gap: 10px; }

.search {
  width: 280px; padding: 8px 12px;
  background: var(--surface-1); color: var(--text-primary);
  border: 1px solid var(--field-border); border-radius: var(--radius-pill);
  font-family: inherit; font-size: var(--t-small);
  transition: border-color var(--dur) var(--ease);
}
.search:hover { border-color: var(--text-muted); }
.search:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }

.page { padding: 24px; flex: 1; }

/* ----------------------------------------------------------- surfaces --- */

/* Panels fade top-to-bottom exactly as the site's cards do. */
.card {
  background: linear-gradient(180deg, var(--surface-1), var(--surface-grad));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
}
.card-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-bottom: 14px;
}
.card-title {
  font-family: var(--font-display);
  font-size: 15px; font-weight: 600; letter-spacing: -0.005em;
}
.card-sub { font-size: var(--t-small); color: var(--text-muted); line-height: var(--lh-body); }

/* min() on the track floor, not a bare pixel value: `minmax(190px, 1fr)` keeps
   asking for 190px even when the grid itself is 170px wide, and the overflow
   goes to the page rather than the card. */
.grid { display: grid; gap: 16px; }
.grid-kpi { grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); }
/* 1.6fr/1fr starved the right column: a horizontal bar chart needs room for a
   label gutter, the bar, and a value. 1.35fr keeps the split without crushing
   it, and both collapse to one column well before that becomes cramped. */
.grid-2 { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
.grid-2-even { grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); }
@media (max-width: 1180px) {
  .grid-2 { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------- stat tiles --- */

.stat {
  background: linear-gradient(180deg, var(--surface-1), var(--surface-grad));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px;
}
.stat-label {
  font-family: var(--font-display);
  font-size: var(--t-micro); font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--text-muted);
  margin-bottom: 10px;
}
.stat-value {
  font-family: var(--font-display);
  font-size: var(--t-hero); font-weight: 700;
  letter-spacing: -0.02em; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stat-meta {
  font-size: var(--t-small); color: var(--text-secondary);
  margin-top: 8px; line-height: var(--lh-body);
}
.stat.alert .stat-value { color: var(--critical); }

/* -------------------------------------------------------------- table --- */

/* overscroll-behavior stops a swipe that runs off the end of a wide table from
   continuing into the page — or, on iOS, into the browser's back gesture. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
thead th {
  text-align: left; font-family: var(--font-display);
  font-size: var(--t-micro); font-weight: 600;
  letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 14px 10px; white-space: nowrap;
  border-bottom: 1px solid var(--border);
  cursor: pointer; user-select: none;
}
thead th.no-sort { cursor: default; }
@media (hover: hover) {
  thead th:hover:not(.no-sort) { color: var(--text-primary); }
}
tbody td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--grid);
  color: var(--text-secondary);
  line-height: var(--lh-tight);
  white-space: nowrap;
}
tbody td.primary { color: var(--text-primary); font-weight: 550; }
@media (hover: hover) {
  tbody tr:hover td { background: var(--surface-2); }
}
tbody tr.clickable { cursor: pointer; }
tbody tr.clickable:active td { background: var(--surface-2); }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.wrap-cell { white-space: normal; min-width: 200px; }

/* ------------------------------------------------------------- badges --- */

.badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: var(--radius-pill);
  font-size: var(--t-micro); font-weight: 600; line-height: 1.5;
  white-space: nowrap;
  border: 1px solid var(--border-strong); color: var(--text-secondary);
}
.badge::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: 0 0 6px;
}
.badge.good     { color: var(--good-text);  border-color: color-mix(in srgb, var(--good) 40%, transparent); }
.badge.warning  { color: var(--text-primary); border-color: color-mix(in srgb, var(--warning) 55%, transparent); }
.badge.warning::before  { background: var(--warning); }
.badge.serious  { color: var(--text-primary); border-color: color-mix(in srgb, var(--serious) 55%, transparent); }
.badge.serious::before  { background: var(--serious); }
.badge.critical { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 45%, transparent); }
.badge.neutral  { color: var(--text-muted); }
.badge.info     { color: var(--series-1); border-color: color-mix(in srgb, var(--series-1) 40%, transparent); }

/* Paid takes the site's violet at full-strength outline, where .info softens
   its cyan border to 40% — two different hues, so the two most common pills on
   an invoice list never get mistaken for each other. The violet is stepped per
   theme because one value cannot clear 4.5:1 on both surfaces. */
.badge.paid {
  color: var(--paid);
  border-color: var(--paid);
}

.source-tag {
  font-size: 11px; padding: 2px 7px; border-radius: 6px;
  background: var(--surface-2); color: var(--text-muted);
  border: 1px solid var(--border);
}

/* ------------------------------------------------------------ buttons --- */

/* The primary action carries the site's accent gradient; ghost and danger are
   the same shape and the same padding, so the vocabulary stays one system. */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; border-radius: var(--radius-sm);
  font-family: inherit; font-size: var(--t-small); font-weight: 600;
  line-height: 1.4;
  border: 1px solid transparent; cursor: pointer;
  background: var(--grad); color: var(--accent-ink);
  transition: filter var(--dur) var(--ease), border-color var(--dur) var(--ease),
              background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.btn:hover { filter: brightness(1.08); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; filter: none; }
.btn-ghost {
  background: var(--surface-1); color: var(--text-secondary);
  border-color: var(--border-strong);
}
.btn-ghost:hover {
  color: var(--accent); filter: none;
  background: var(--surface-2); border-color: var(--accent);
}
.btn-danger { background: var(--critical); color: #fff; }
.btn-sm { padding: 6px 11px; font-size: var(--t-micro); }

.toolbar {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap; margin-bottom: 16px;
}
.toolbar .spacer { flex: 1; }

/* Fields are sunk below the panel they sit on and outlined at 3:1, so a control
   is identifiable as a control on either theme. */
select, input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="date"], textarea {
  padding: 8px 11px; line-height: var(--lh-tight);
  background: var(--plane); color: var(--text-primary);
  border: 1px solid var(--field-border); border-radius: var(--radius-sm);
  font-family: inherit; font-size: 13px;
  transition: border-color var(--dur) var(--ease);
}
select:hover, input:hover, textarea:hover { border-color: var(--text-muted); }
select:focus, input:focus, textarea:focus {
  outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent;
}
::placeholder { color: var(--text-muted); opacity: 1; }

/* Checkboxes, radios and range thumbs stay native — the standard control is
   the one people already know how to use. `accent-color` is the supported way
   to brand them without rebuilding them out of divs. */
input[type="checkbox"], input[type="radio"], input[type="range"], progress {
  accent-color: var(--accent);
}
input[type="checkbox"], input[type="radio"] { width: 15px; height: 15px; }
textarea { resize: vertical; min-height: 90px; width: 100%; }
label.field { display: block; margin-bottom: 12px; }
label.field > span {
  display: block; font-size: var(--t-small); font-weight: 600;
  color: var(--text-secondary); margin-bottom: 6px;
}
label.field > input, label.field > select, label.field > textarea { width: 100%; }
.field-hint {
  font-size: var(--t-small); color: var(--text-muted);
  margin-top: 6px; line-height: var(--lh-body);
}

.check { display: flex; align-items: center; gap: 8px; font-size: 13px; margin-bottom: 8px; }

/* ------------------------------------------------------------- charts --- */

.chart-figure { margin: 0; }
.chart-svg { display: block; width: 100%; overflow: visible; }
.chart-note { font-size: var(--t-small); color: var(--text-muted); margin-top: 12px; }

.chart-tooltip {
  position: fixed; z-index: 60; pointer-events: none;
  background: var(--surface-1); color: var(--text-primary);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 7px 10px; font-size: 12px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  white-space: nowrap;
}
.chart-tooltip .tt-label { color: var(--text-muted); font-size: 11px; }
.chart-tooltip .tt-value { font-weight: 650; font-variant-numeric: tabular-nums; }

.legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; }
.legend-item {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--t-small); color: var(--text-secondary);
}
.legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 10px; }

.table-toggle {
  background: none; border: none; color: var(--text-muted);
  font-family: inherit; font-size: var(--t-small); cursor: pointer;
  padding: 0; text-decoration: underline;
}
.table-toggle:hover { color: var(--text-primary); }

/* -------------------------------------------------------------- misc --- */

.empty {
  text-align: center; padding: 56px 20px; color: var(--text-muted);
}
.empty h3 {
  color: var(--text-primary); font-family: var(--font-display);
  font-size: var(--t-lead);
  margin: 0 0 8px; font-weight: 600;
}
.empty p {
  margin: 0 auto 18px; max-width: 52ch;
  font-size: var(--t-small); line-height: var(--lh-loose);
}

.detail-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; margin-bottom: 18px; flex-wrap: wrap;
}
.detail-title {
  font-family: var(--font-display);
  font-size: var(--t-title); font-weight: 700;
  letter-spacing: -0.02em; line-height: var(--lh-tight);
  margin: 0 0 6px; text-wrap: balance;
}
.detail-sub { color: var(--text-secondary); font-size: var(--t-small); }

.kv {
  display: grid; grid-template-columns: 150px 1fr;
  gap: 11px 18px; font-size: var(--t-small);
  line-height: var(--lh-body);
}
.kv dt { color: var(--text-muted); font-size: var(--t-small); }
.kv dd { margin: 0; color: var(--text-primary); }

.toast {
  position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
  background: var(--surface-1); color: var(--text-primary);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 12px 20px; font-size: var(--t-small); z-index: 100;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25);
  max-width: 560px;
}
.toast.err { border-color: var(--critical); }

.modal {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(4, 6, 9, 0.62);
  backdrop-filter: blur(3px);
  display: grid; place-items: center; padding: 24px;
}
/* A class rule with an explicit `display` beats the UA's `[hidden]` rule, so
   the overlay must opt out of display by hand or it covers the whole app. */
.modal[hidden] { display: none; }
.modal-card {
  background: linear-gradient(180deg, var(--surface-1), var(--surface-grad));
  border: 1px solid var(--border-strong);
  border-radius: var(--radius); padding: 22px;
  width: min(680px, 100%); max-height: 86vh; max-height: 86dvh; overflow-y: auto;
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  box-shadow: 0 32px 70px -30px rgba(0, 0, 0, 0.7);
}
.modal-title {
  font-family: var(--font-display);
  font-size: var(--t-lead); font-weight: 600;
  margin: 0 0 18px; letter-spacing: -0.01em;
}
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; }

.callout {
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 13px 15px; border-radius: var(--radius-sm);
  font-size: var(--t-small); line-height: var(--lh-loose);
  color: var(--text-secondary);
  margin-bottom: 16px;
}
.callout strong { color: var(--text-primary); }
/* Severity reads from the whole frame plus a background tint, not a stripe
   down one edge — the old border-left-color left a 1px mark nobody saw. */
.callout.warn {
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
  background: color-mix(in srgb, var(--warning) 9%, var(--surface-2));
}
.callout.crit {
  border-color: color-mix(in srgb, var(--critical) 45%, transparent);
  background: color-mix(in srgb, var(--critical) 9%, var(--surface-2));
}

.steps { counter-reset: step; padding-left: 0; list-style: none; margin: 0; }
.steps li {
  counter-increment: step; position: relative;
  padding-left: 32px; margin-bottom: 14px;
  font-size: var(--t-small); line-height: var(--lh-loose);
  color: var(--text-secondary);
}
.steps li::before {
  content: counter(step);
  position: absolute; left: 0; top: 1px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--series-1-soft); color: var(--series-1);
  font-size: 11px; font-weight: 700;
  display: grid; place-items: center;
}

.sent-note { color: var(--good-text); }

.deposit-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.deposit-row input:disabled { opacity: 0.45; cursor: not-allowed; }
@media (max-width: 520px) { .deposit-row { grid-template-columns: 1fr; } }

/* ----------------------------------------------------------- notepad --- */

.notepad textarea {
  min-height: 170px;
  line-height: var(--lh-loose);
  font-size: var(--t-body);
  /* Prose, not data: give it a comfortable measure and let the card carry the
     rest of the width rather than running lines to 200 characters. */
  max-width: 78ch;
}
.memo-state {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition: color var(--dur) var(--ease);
}
.memo-state.is-error { color: var(--critical); white-space: normal; }

.notepad-actions {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 10px;
}

/* Each note is a block with its own date, not a row in a table: they are
   paragraphs of varying length and a table would fight that. */
.memo-list { margin-top: 18px; }
.memo-item {
  border-top: 1px solid var(--grid);
  padding: 12px 0;
}
.memo-item:first-child { border-top: 1px solid var(--border); }
.memo-item-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 5px;
}
.memo-when {
  font-size: var(--t-micro); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* The delete control stays out of the way until the note is hovered — it is
   next to prose that gets read far more often than it gets removed. */
.memo-del { opacity: 0; transition: opacity var(--dur) var(--ease); }
.memo-item:hover .memo-del, .memo-del:focus-visible { opacity: 1; }
@media (hover: none) { .memo-del { opacity: 1; } }
.memo-text {
  font-size: var(--t-small); line-height: var(--lh-loose);
  color: var(--text-secondary);
  white-space: pre-wrap;
  max-width: 78ch;
  /* Four lines is enough to recognise a note; the rest opens on demand. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.memo-text.is-open {
  -webkit-line-clamp: unset;
  overflow: visible;
}
.memo-more { margin-top: 4px; }

/* mail */
.mail-layout { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: 16px; }
@media (max-width: 940px) { .mail-layout { grid-template-columns: 1fr; } }
/* The list column is narrow — 300px — so these three controls do not fit on
   one line once a count appears. They wrap to a second row instead of being
   squeezed, and nothing inside them breaks mid-phrase. */
.mail-select-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; border-bottom: 1px solid var(--border);
}
.mail-select-bar .spacer { display: none; }
.mail-select-all {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--t-small); color: var(--text-secondary);
  cursor: pointer; user-select: none;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex: 0 0 auto;
}
/* The two actions travel together, so they wrap as a pair to the second row
   rather than the button landing on its own under the count. */
.mail-bulk-actions {
  display: flex; align-items: center; gap: 8px;
  margin-left: auto;
  flex: 0 1 auto;
  min-width: 0;
}
.mail-bulk-actions select {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 140px;
  font-size: var(--t-micro);
  padding: 6px 8px;
}
.mail-bulk-actions .btn { flex: 0 0 auto; white-space: nowrap; }

.mail-list {
  max-height: 74vh; max-height: 74dvh;
  overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain;
}
.mail-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px; border-bottom: 1px solid var(--grid);
  cursor: pointer;
}
.mail-row-main { flex: 1; min-width: 0; }
/* Nudged onto the first line of text rather than the top of the padding box. */
.mail-pick { margin-top: 3px; flex: 0 0 auto; cursor: pointer; }
@media (hover: hover) {
  .mail-row:hover { background: var(--surface-2); }
}
.mail-row.active { background: var(--series-1-soft); }
.mail-row.picked { background: color-mix(in srgb, var(--accent) 9%, transparent); }
.mail-row.picked.active { background: var(--series-1-soft); }
.mail-row.unread .mail-from { font-weight: 700; color: var(--text-primary); }
.mail-from { font-size: var(--t-small); color: var(--text-secondary); }
.mail-subject {
  font-size: var(--t-small); color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mail-date { font-size: var(--t-micro); color: var(--text-muted); }
.attachment-list { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
.attachment-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong); font-size: var(--t-small);
  color: var(--text-primary);
}
.attachment-chip:hover { background: var(--surface-2); text-decoration: none; }
.attachment-chip .card-sub { font-size: 11px; }
.attachment-dl {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--border-strong); color: var(--text-muted);
  margin-left: -4px;
}
.attachment-dl:hover { color: var(--text-primary); background: var(--surface-2); text-decoration: none; }

.mail-body {
  border-top: 1px solid var(--border); margin-top: 14px; padding-top: 14px;
  font-size: 13.5px; line-height: 1.6; overflow-x: auto;
}
.mail-body img { max-width: 100%; height: auto; }
.mail-frame { width: 100%; min-height: 480px; border: 0; background: #fff; border-radius: var(--radius-sm); }

.folder-tree { max-height: 60vh; max-height: 60dvh; overflow-y: auto; }
.folder-row {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 4px; border-radius: var(--radius-sm);
}
@media (hover: hover) {
  .folder-row:hover { background: var(--surface-2); }
  .folder-row:hover .folder-actions { opacity: 1; }
}
.folder-toggle {
  width: 18px; height: 18px; flex: 0 0 18px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; color: var(--text-muted);
  font-size: 11px; cursor: pointer; border-radius: 4px; padding: 0;
}
.folder-toggle:hover { color: var(--text-primary); background: var(--surface-sunk); }
.folder-toggle-spacer { cursor: default; }
.folder-name { flex: 1; font-size: var(--t-body); }
.folder-actions {
  display: flex; gap: 4px; opacity: 0; transition: opacity .12s;
  /* Always visible on touch — there is no hover to reveal them there. */
}
@media (hover: none) { .folder-actions { opacity: 1; } }

.skeleton {
  background: var(--surface-2); border-radius: var(--radius-sm);
  height: 14px; animation: shimmer 1.2s ease-in-out infinite;
}
@keyframes shimmer { 50% { opacity: 0.45; } }

/* ==========================================================================
   Phone and tablet
   ==========================================================================

   Three separate axes, deliberately not conflated:

     `pointer: coarse`   — a finger is driving. Controls grow, and every form
                            field goes to 16px, because Safari zooms the whole
                            page when you focus a field set smaller than that
                            and never zooms back out.
     `max-width: 900px`  — the sidebar no longer fits beside the content, so it
                            becomes a drawer. iPad portrait (820) lands here;
                            iPad landscape (1180) keeps the full layout.
     `max-width: 640px`  — one column of anything. Tables stop being tables and
                            become a card per record; the modal becomes a sheet.

   A desktop browser narrowed to phone width gets the layout but not the
   touch sizing, which is the correct outcome for both.
   ------------------------------------------------------------------------ */

/* --------------------------------------------------------- form layout --- */

/* These were inline `style="display:grid;grid-template-columns:…"` on the
   forms. An inline style cannot be overridden by a media query, so paired
   fields could never be unpaired on a narrow screen — hence the classes. */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-grid.three { grid-template-columns: 1fr 1fr 1fr; }
.form-grid.host-port { grid-template-columns: minmax(0, 1fr) 90px; }

.line-head, .line-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 74px 104px 92px 30px;
  gap: 8px; align-items: center;
}
.line-head { margin-bottom: 6px; }
.line-row { margin-bottom: 8px; }
/* The per-field label is redundant while the header row above the grid is
   showing; it only earns its place once that row is gone. */
.line-field { display: contents; }
.line-field > span { display: none; }

.contact-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr) minmax(0, 1fr) 26px 30px;
  gap: 8px; align-items: center; margin-bottom: 8px;
}

/* The filter box on a list page: fixed at 240px where there is room, but
   allowed to give that up rather than push Export and New off the toolbar. */
.list-search { flex: 0 1 240px; min-width: 120px; }

/* Sorting a list by tapping a column header needs column headers, and the
   stacked card view has none. This is the same control in dropdown form. */
.list-sort { display: none; }

/* ------------------------------------------------------------- drawer --- */

/* Both are inert above 900px: the button is not shown and the scrim has no
   rule that displays it, so `nav-open` on the body means nothing there. */
.nav-toggle { display: none; }
.scrim { display: none; }

.nav-toggle {
  align-items: center; justify-content: center;
  width: 40px; height: 40px; margin-left: -6px;
  flex: 0 0 auto;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text-secondary); cursor: pointer; padding: 0;
  -webkit-tap-highlight-color: transparent;
}
.nav-toggle:active { background: var(--surface-2); }
.nav-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Three rules drawn from one element, so there is nothing to keep in sync. */
.nav-toggle-bars, .nav-toggle-bars::before, .nav-toggle-bars::after {
  display: block; width: 18px; height: 2px; border-radius: 2px;
  background: currentColor;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before, .nav-toggle-bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after  { top: 6px; }

/* ------------------------------------------------- coarse pointer only --- */

@media (pointer: coarse) {
  /* 16px is not a style choice — below it, focusing a field zooms the viewport
     and leaves it zoomed. Every control that can take focus is at or above it. */
  select, textarea,
  input[type="text"], input[type="email"], input[type="password"],
  input[type="number"], input[type="date"], input[type="search"] {
    font-size: 16px;
    padding: 10px 12px;
  }
  /* An empty date input collapses to a few pixels tall on iOS. */
  input[type="date"] { min-height: 42px; }

  .search { font-size: 16px; padding: 10px 14px; }

  /* 44px is Apple's floor for anything you are expected to hit accurately. */
  .btn { min-height: 44px; padding: 10px 16px; }
  .btn-sm { min-height: 38px; padding: 8px 13px; font-size: var(--t-small); }
  .theme-toggle { min-height: 40px; font-size: var(--t-small); }
  .nav-item { padding: 11px 12px; min-height: 44px; }
  /* A text button sitting in prose — it cannot be 44px tall without knocking a
     hole in the paragraph, so it gets padding on the axis that is free. */
  .table-toggle { padding: 6px 0; }
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
  .mail-row { padding: 13px 12px; }
  .folder-row { padding: 9px 4px; }
  thead th { padding-bottom: 12px; }
  tbody td { padding: 13px 14px; }

  /* The grey flash Safari paints over a tapped element, replaced by the
     :active states defined next to each control. */
  a, button, .nav-item, .mail-row, tr.clickable, .folder-row, label {
    -webkit-tap-highlight-color: transparent;
  }
  .btn:active { filter: brightness(0.94); }
}

/* ------------------------------------------ tablet portrait and smaller --- */

@media (max-width: 900px) {
  .nav-toggle { display: inline-flex; }

  /* Out of the flex row and over the content. Fixed rather than sticky so it
     spans the viewport regardless of how far the page has scrolled. */
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0;
    z-index: 60;
    width: min(288px, 84vw); flex: none;
    height: 100vh; height: 100dvh;
    padding-left: env(safe-area-inset-left);
    transform: translateX(-100%);
    transition: transform 220ms var(--ease);
    box-shadow: 0 0 60px rgba(0, 0, 0, 0.5);
  }
  body.nav-open .sidebar { transform: none; }

  body.nav-open .scrim {
    display: block;
    position: fixed; inset: 0; z-index: 55;
    background: rgba(4, 6, 9, 0.55);
    backdrop-filter: blur(2px);
  }
  /* The page behind a drawer must not scroll with it. */
  body.nav-open { overflow: hidden; }

  /* Added to the home screen the app runs with a translucent status bar, which
     means the page starts at y=0 — underneath the clock — rather than below
     it. The safe-area inset is what that clock is worth: ~47px there, and
     exactly 0 in a normal Safari tab, where the browser's own chrome already
     holds that space. So the same rule is correct in both, and the height goes
     from fixed to a minimum so the bar grows by the inset rather than eating
     it. The sticky background extends up behind the status bar, which is the
     point — the clock sits on the topbar's blur instead of on a bare card. */
  .topbar {
    height: auto;
    min-height: 56px;
    padding-top: env(safe-area-inset-top);
    padding-left: max(14px, env(safe-area-inset-left));
    padding-right: max(14px, env(safe-area-inset-right));
    gap: 10px;
  }
  /* The drawer is fixed from top: 0, so it is under the clock too. */
  .sidebar { padding-top: env(safe-area-inset-top); }
  .sidebar-foot { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
  /* A long breadcrumb must give way to the search box, not push it off. */
  .crumb {
    min-width: 0; flex: 0 1 auto;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .topbar-actions { flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
  .search { width: 100%; max-width: 280px; min-width: 0; }

  .page {
    padding: 16px max(16px, env(safe-area-inset-right))
             calc(16px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  }
}

/* Mail is two panes side by side; below this it is one pane above the other,
   and the list has to stop short of filling the screen or the message you just
   opened starts a page-height below the fold. */
@media (max-width: 940px) {
  .mail-list { max-height: 46vh; max-height: 46dvh; }
  .mail-frame { min-height: 320px; }
}

/* --------------------------------------------------------------- phone --- */

@media (max-width: 640px) {
  .page { padding: 12px max(12px, env(safe-area-inset-right))
                  calc(12px + env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); }
  .grid { gap: 12px; }
  .card { padding: 14px; }
  .stat { padding: 15px 16px; }
  /* The hero figure is the one thing you open the dashboard to read; it keeps
     its size while everything around it gives ground. */
  .stat-value { font-size: 26px; }

  /* Title above the actions, rather than a title squeezed beside eight
     buttons trying to share one line. */
  .detail-head { flex-direction: column; align-items: stretch; gap: 12px; }
  .detail-title { font-size: 19px; }

  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv dt { margin-top: 11px; }
  .kv dt:first-of-type { margin-top: 0; }

  /* A title and its subtitle share one line where there is room for both. In
     320-odd pixels there is not, and space-between breaks the title across two
     lines to make the subtitle fit beside it — so they stack instead. */
  .card-head { flex-wrap: wrap; row-gap: 6px; }
  .card-head > * { min-width: 0; }
  .card-title { flex: 1 1 100%; }

  /* The page title is the thing that tells you where you are; it should not be
     the thing that gets clipped so a search box can stay 280px wide. */
  .crumb { flex: 0 0 auto; }
  .search { max-width: 190px; }

  .toolbar { gap: 8px; }
  .list-search { flex: 1 1 100%; }
  .list-sort { display: inline-flex; flex: 1 1 auto; min-width: 0; }
  #list-status { flex: 1 1 auto; min-width: 0; }

  .form-grid, .form-grid.three { grid-template-columns: 1fr; }

  /* --- tables become one card per record ---------------------------------
     A seven-column table on a 390px screen is a horizontal scrollbar with
     data hidden behind it. Stacked, each record is a block: its name as the
     heading, then a labelled line per field. The header row moves to the
     ::before of each cell — Views.stackTables() copies the text across, so
     there is one source for the column names either way. */
  table.stacked { border-collapse: separate; border-spacing: 0; }
  table.stacked thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  table.stacked tbody tr {
    display: block;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface-1);
    padding: 11px 13px; margin-bottom: 9px;
  }
  table.stacked tbody tr:last-child { margin-bottom: 0; }
  table.stacked tbody tr.clickable:active { background: var(--surface-2); }
  table.stacked tbody td {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 14px; padding: 3px 0; border: 0;
    white-space: normal; text-align: left;
  }
  table.stacked tbody td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-family: var(--font-display);
    font-size: var(--t-micro); font-weight: 600;
    letter-spacing: var(--track-caps); text-transform: uppercase;
    color: var(--text-muted);
  }
  /* The first cell is the record's name, so it becomes the heading of the card
     and needs no label. Only the first — `.primary` also marks emphasised
     figures further along the row (a line's Amount, an invoice's Total), and
     those are labelled fields like any other, not a second heading. */
  table.stacked tbody td.primary:first-child {
    display: block;
    font-size: var(--t-body); font-weight: 600;
    padding-bottom: 7px; margin-bottom: 5px;
    border-bottom: 1px solid var(--grid);
  }
  table.stacked tbody td.primary:not(:first-child) { color: var(--text-primary); font-weight: 600; }
  /* No label on the heading, and none for a column that never had one — the
     CEC/Imported tag sits under a blank header. */
  table.stacked tbody td.primary:first-child::before,
  table.stacked tbody td[data-label=""]::before,
  table.stacked tbody td:not([data-label])::before { content: none; }
  /* A blank cell would otherwise be a labelled row with nothing after it. */
  table.stacked tbody td:empty { display: none; }
  table.stacked tfoot tr {
    display: flex; justify-content: space-between; gap: 12px;
    padding: 4px 13px 0;
  }
  table.stacked tfoot td { border: 0; }

  /* --- the modal becomes a sheet ---------------------------------------- */
  .modal { padding: 0; place-items: end stretch; }
  .modal-card {
    width: 100%; max-width: none;
    max-height: 92vh; max-height: 92dvh;
    border-radius: 18px 18px 0 0; border-bottom: 0;
    /* Side insets matter in landscape, where the notch is on one edge and
       viewport-fit=cover has already let the sheet run underneath it. */
    padding: 20px max(16px, env(safe-area-inset-right))
             calc(18px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  }
  .modal-title { margin-bottom: 14px; }
  /* Save stays on screen while you scroll a long form to check it. */
  .modal-actions {
    position: sticky; bottom: calc(-1 * env(safe-area-inset-bottom));
    background: var(--surface-grad);
    padding: 12px 0 calc(2px + env(safe-area-inset-bottom));
    margin-bottom: calc(-1 * env(safe-area-inset-bottom));
    box-shadow: 0 -10px 16px -12px rgba(0, 0, 0, 0.5);
  }
  .modal-actions .btn { flex: 1 1 auto; justify-content: center; }

  .toast { left: 12px; right: 12px; transform: none; max-width: none; bottom: calc(16px + env(safe-area-inset-bottom)); }

  .notepad textarea { min-height: 130px; }
  .empty { padding: 38px 14px; }
}

/* Line items and contacts are grids of tiny fixed columns. Below this they
   wrap to a block per line: the description on its own row, the numbers under
   it, so nothing is 40px wide and nothing overflows the sheet. */
@media (max-width: 620px) {
  .line-head { display: none; }
  .line-row, .contact-row {
    grid-template-columns: repeat(3, minmax(0, 1fr)) auto;
    padding: 10px; margin-bottom: 10px;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface-2);
  }
  .line-row > [data-f="name"] { grid-column: 1 / -1; }
  /* Out of `display: contents` and back to being a real box, so the label sits
     above its input instead of the two becoming separate grid cells. */
  .line-field { display: block; min-width: 0; }
  .line-field > span {
    display: block;
    font-family: var(--font-display);
    font-size: var(--t-micro); font-weight: 600;
    letter-spacing: var(--track-caps); text-transform: uppercase;
    color: var(--text-muted); margin-bottom: 4px;
  }
  .line-field > input { width: 100%; }
  .contact-row { grid-template-columns: minmax(0, 1fr) auto auto; }
  .contact-row > [data-f="name"],
  .contact-row > [data-f="email"],
  .contact-row > [data-f="designation"] { grid-column: 1 / -1; }
}

/* Landscape on a phone is ~380px tall. A drawer and a sheet that each want
   most of the height leave nothing to read, so both give some back. */
@media (max-height: 480px) and (orientation: landscape) {
  .modal-card { max-height: 96dvh; }
  .mail-list { max-height: 60dvh; }
  .brand { padding: 10px 16px 8px; }
  .brand-logo { max-width: 130px; }
}

/* ---------------------------------------------------- collapsible setup --- */
/* Setup grew into a long scroll of unrelated panels — mail, categories, import,
   templates, data. Each is something you visit once and then leave alone, so
   they fold shut and the page becomes a list of what is in it. State is kept
   per panel in localStorage, because "the one I was working in" should survive
   a reload. */
.card.foldable > .card-head {
  cursor: pointer;
  margin-bottom: 0;
  user-select: none;
}
.card.foldable:not(.folded) > .card-head { margin-bottom: 14px; }
.card.foldable > .card-head .card-title::before {
  content: "▾";
  display: inline-block;
  width: 1em;
  margin-right: 2px;
  color: var(--text-muted);
  transition: transform .15s ease;
}
.card.foldable.folded > .card-head .card-title::before { transform: rotate(-90deg); }
.card.foldable.folded > *:not(.card-head) { display: none; }
.card.foldable > .card-head:focus-visible {
  outline: 2px solid var(--series-1);
  outline-offset: 3px;
  border-radius: 6px;
}
