/* ==========================================================================
   Invoice Studio - minimalist design system
   Palette is built on one brand blue, #2457d6 - a brighter, more saturated
   step from the brand mark's #004ca2, which read as a corporate navy on screen.
   ========================================================================== */

/* --- Type ------------------------------------------------------------------
   Inter, self-hosted. The interface used to take whatever the system offered -
   Segoe on Windows, Roboto on Linux, SF on a Mac - so it looked like a
   different product on each and like nobody's on all three. One variable file
   per script covers every weight; latin-ext carries the č, ć, đ, š and ž that
   Bosnian needs, and loads only on a page that uses one. Served from our own
   static files: no third-party request, nothing for the CSP to widen. OFL -
   see core/fonts/OFL-Inter.txt. The PDFs keep their own faces; this is the
   screen only. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("../fonts/inter-latin-wght-normal.260c81a4759b.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("../fonts/inter-latin-ext-wght-normal.1ad231aac0a8.woff2") format("woff2-variations");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- Tokens ----------------------------------------------------------------

   Two layers, and the split is what makes a second theme possible at all.

   The *roots* are the handful of colours a theme actually chooses: the brand,
   the ink, the two grounds, and the four state hues. Everything else is
   *derived* from them with `color-mix`, so a wash is written once as "the brand
   at 5% of the surface" rather than as a hex that has to be recomputed by hand
   for every ground it might sit on. The dark theme further down redefines the
   roots and nothing else.
   --------------------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* Roots. The brand is the brighter #2457d6 (6.2:1 under white text, 5.8:1
     as a link on the canvas); every wash, ring and fill below is derived from
     it, and each was re-measured against AA - see the select note further
     down, which is the tightest at 4.8:1. */
  --brand: #2457d6;
  --brand-dark: #1b44ab;

  --ink: #16202c;
  /* Darkened from #5b6b7c. This is the colour of every secondary cell in a
     table - client names, dates, the total beside the amount due - and those
     cells now sit on a tinted row. At the old value, muted text on a hovered
     untouched-overdue row measured 4.0:1, under AA; it is 4.8:1 here. The
     difference is barely visible on white and is what makes the tints safe. */
  --ink-muted: #51606e;
  /* Darkened from #8a99a8, which measured 2.9:1 on the card. Everything wearing
     it is small text - the stat labels, the chart's legend glosses, a void row's
     figures - so it needed the same 4.5:1 floor as any other prose, not the 3:1
     a decorative rule gets. It sits closer to --ink-muted than it used to; the
     hierarchy between them is carried by size and weight, which survives. */
  --ink-faint: #5c6b75;

  --surface: #ffffff;
  --canvas: #f6f8fa;

  /* The four state hues. Each is dark enough to be read as *text* on its own
     palest wash, which is what the row tints and the stat tiles are made of -
     the whole scheme falls apart the moment a status has to be printed in a
     colour that its own background swallows. Verified against the strongest
     tint each one is used at; see "Row state" for those strengths.

     --ok was darkened from #1a7f52 and --warn from #a76a00: "Paid" and
     "Partial" measured 4.4:1 and 4.0:1 on their own chips, under AA before any
     row tint was beneath them. */
  --ok: #167048;
  --warn: #8a5600;
  --danger: #b3261e;
  /* Money owed back to a client - an overpayment, or a paid invoice since
     credited. Deliberately its own hue rather than the danger red it used to
     borrow: a refund is not an error, and on a list where overdue rows are
     already red, "we owe them" printed in the same colour is the one reading
     somebody would act on backwards. Violet also survives the two common
     colour deficiencies alongside red and green, which amber does not. */
  --alt: #5d3fa8;
  /* An overpayment, and a credit note's own badge. Amber, but not --warn: that
     one means "part done" and this means "somebody sent too much", and they
     appear in the same column. */
  --credit: #8a4b12;

  /* How strongly a chip, a wash and a hairline take their hue. One place, so
     the twelve state surfaces below cannot drift apart from each other - and so
     the dark theme can lift all of them at once, which it has to: the same
     percentage of a hue reads far weaker against a dark ground. */
  --chip-mix: 12%;
  --hairline-mix: 24%;
  --tint-mix: 5%;
  --light-mix: 11%;

  /* How far a small control moves when the pointer is over it, and how far
     again when it is the chosen one. Their own mixes rather than --tint-mix and
     --light-mix because these two carry an answer - which page size, which
     theme, which language - and so have to be told apart from each other and
     from the track behind them, where a chip only has to be seen at all.

     The dark theme lifts them further than it lifts the others. A picked tile
     used to be --surface on a --canvas track: a step you can see on white and
     cannot see at all when the two are #171d26 and #10151c, which left the
     switches with no visible answer in them. */
  --hover-mix: 9%;
  --select-mix: 16%;
  --select-line-mix: 38%;
  /* The grey of the theme and language pills, behind their marks - see
     `.toggle-track`. The ink is what is mixed, so it is a grey on either
     theme; the dark one lifts it furthest, as it does the two above. */
  --pill-mix: 10%;

  /* The row-tint ladder. Four stops, and they are the whole of how strongly a
     table row is coloured - see "Row state". Written here rather than at each
     rule because the dark theme has to lift every one of them together: the
     same percentage of a hue reads far weaker against a dark ground.

     Three graded stops for the states that describe a part-settled balance, and
     one quiet stop for the states that are simply information. The top of the
     ladder is set by legibility: at 18% of the danger hue, muted text - the
     client names and dates filling most of the row - measures 4.5:1, which is
     exactly AA. */
  --tint-1: 4%;   --tint-1-hover: 8%;
  --tint-2: 9%;   --tint-2-hover: 13%;
  --tint-3: 15%;  --tint-3-hover: 18%;
  --tint-quiet: 5%;  --tint-quiet-hover: 9%;

  /* Text on a brand-filled control. White here; on the dark theme the brand is
     a pale blue and the text has to go the other way. */
  --on-brand: #ffffff;

  /* Derived: the brand scale, one hue. The two opaque stops are backgrounds;
     the translucent washes further down have to let whatever is under them
     show through. */
  --brand-tint: color-mix(in srgb, var(--brand) var(--tint-mix), var(--surface));
  --brand-light: color-mix(in srgb, var(--brand) var(--light-mix), var(--surface));
  /* The three faces of a small toggle. The ring is what keeps a picked tile
     apart from a hovered neighbour: two fills a step apart from each other read
     as "warmer and cooler" until one of them is also outlined. */
  --hover-fill: color-mix(in srgb, var(--brand) var(--hover-mix), var(--surface));
  --select-fill: color-mix(in srgb, var(--brand) var(--select-mix), var(--surface));
  --select-line: color-mix(in srgb, var(--brand) var(--select-line-mix), var(--surface));
  /* The label on a picked tile. The brand reads on its own pale wash here; the
     dark theme has to take the other one - see there. */
  --select-ink: var(--brand);

  /* Derived: rules. */
  --line: color-mix(in srgb, var(--ink) 12%, var(--surface));
  --line-strong: color-mix(in srgb, var(--ink) 24%, var(--surface));
  /* A rule inside a banded table. The full `--line` between every pair of rows
     draws a grid, which is one visual system too many once the rows are
     striped. */
  --line-soft: color-mix(in srgb, var(--ink) 6%, var(--surface));

  /* Derived: one chip, one hairline and one wash per state, all from the hue.
     `-bg` is the badge chip: a step darker than a row wash, so a badge sitting
     on a row of its own colour still reads as a chip on a ground. `-line` is
     its edge, which is what keeps the chip a chip once the row beneath it is
     tinted the same colour. */
  --ok-bg: color-mix(in srgb, var(--ok) var(--chip-mix), var(--surface));
  --ok-line: color-mix(in srgb, var(--ok) var(--hairline-mix), var(--surface));
  --warn-bg: color-mix(in srgb, var(--warn) var(--chip-mix), var(--surface));
  --warn-line: color-mix(in srgb, var(--warn) var(--hairline-mix), var(--surface));
  --danger-bg: color-mix(in srgb, var(--danger) var(--chip-mix), var(--surface));
  --danger-line: color-mix(in srgb, var(--danger) var(--hairline-mix), var(--surface));
  --alt-bg: color-mix(in srgb, var(--alt) var(--chip-mix), var(--surface));
  --alt-line: color-mix(in srgb, var(--alt) var(--hairline-mix), var(--surface));
  --credit-bg: color-mix(in srgb, var(--credit) var(--chip-mix), var(--surface));
  --credit-line: color-mix(in srgb, var(--credit) var(--hairline-mix), var(--surface));
  --info-bg: color-mix(in srgb, var(--brand) 9%, var(--surface));

  /* The stateless chip - a draft, a cancelled document, a void one. Grey rather
     than a hue, because it is the absence of a state rather than one of them. */
  --neutral-bg: color-mix(in srgb, var(--ink) 8%, var(--surface));
  --neutral-line: color-mix(in srgb, var(--ink) 16%, var(--surface));

  /* Paper.

     A template thumbnail, the hero document and the layout editor's page are all
     pictures of a printed A4 invoice, and a printed invoice is white in either
     theme - which is why a PDF viewer keeps its sheet white on a dark chrome and
     why these four are deliberately *not* redefined by the dark theme below.
     Inverting them would not be a dark mode, it would be a claim about the
     paper. The surfaces around them are themed; the sheet is not. */
  --paper: #ffffff;
  --paper-block: #e4e8ec;
  --paper-rule: #eceff3;
  /* The desk a page is laid on, in the layout editor. This one does theme: it is
     furniture, not paper. */
  --desk: #8a99a8;

  /* Rounder, and flatter. A card is drawn by its hairline and sits on the
     page; only what floats over the page - a menu, a modal, a toast - casts a
     real shadow. `--shadow` is kept as a single soft pixel so the dozens of
     rules that use it settle flat rather than lose their edge. */
  --radius: 12px;
  --radius-sm: 8px;
  --shadow: 0 1px 2px rgb(22 32 44 / 0.04);
  --shadow-lg: 0 4px 12px rgb(22 32 44 / 0.08), 0 16px 40px rgb(22 32 44 / 0.10);

  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --gap: 20px;
  --sidebar-width: 232px;

  /* The height of the band across the top of the application: the brand box
     in the sidebar and the topbar beside it are two halves of one strip, and
     a reader sees a single rule running the width of the window.

     One token because they were two independent heights - the brand's grown
     from the logo's own aspect ratio plus its padding, the topbar's a fixed
     58px - and they did not match. The seam was 17px out, which reads as the
     sidebar being slightly taller than the page rather than as a deliberate
     step. Anything that changes one has to change the other, so there is only
     one number to change.

     At 720px and below the sidebar stacks above the content and the two are
     no longer side by side, so both drop this and size themselves. Above it
     the sidebar is a column beside the page and the seam is visible, which is
     every width from 721px up. */
  --header-h: 75px;
  /* Where a sticky table head stops: under the top bar, which is sticky too.
     Zero on a phone, where the bar scrolls away - see the narrow-screen block. */
  --sticky-top: var(--header-h);
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--canvas);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0 0 0.5em; line-height: 1.25; font-weight: 650; letter-spacing: -0.018em; }
h1 { font-size: 1.85rem; letter-spacing: -0.025em; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }
h4 { font-size: 0.95rem; }

p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

hr { border: none; border-top: 1px solid var(--line); margin: var(--gap) 0; }

code, kbd { font-family: var(--mono); font-size: 0.88em; }

/* --- Layout ---------------------------------------------------------------- */

.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--sidebar-width);
  flex: 0 0 var(--sidebar-width);
  background: var(--surface);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
}

.sidebar-brand {
  position: relative;
  overflow: hidden;
  /* Fixed to the shared header height and centring its contents, rather than
     the old asymmetric padding that let the logo decide how tall the box was.
     That is what kept it out of step with the topbar, and what would have put
     it back out of step the next time the mark changed. */
  height: var(--header-h);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 20px;
  border-bottom: 1px solid var(--line);
  /* Flat. It carried a brand wash and a corner of ruled grain, which tied it
     to the stat tiles that carried the same; both are gone - the logo is the
     only thing this box has to show. */
  background: var(--surface);
}

/* Stacked lockup, centred in the column: wordmark on top, product name
   beneath. The mark is 5:1, so anything that constrains its height crops or
   distorts it — it gets its width and keeps its own ratio. */
.sidebar-brand a { display: block; color: var(--ink); text-align: center; }
.sidebar-brand a:hover { text-decoration: none; }

/* The lockup is one image and carries the product name as artwork, so it is
   sized by height and left to work out its own width - the three logo files
   have different aspect ratios and a fixed width would squash whichever one is
   used next. */
.brand-logo { display: block; height: auto; width: 100%; max-width: 178px; }

.sidebar-brand .wordmark {
  display: block;
  width: 100%;
  max-width: 148px;
  height: auto;
  margin: 0 auto;
}

.sidebar-brand .product {
  display: block;
  margin-top: 6px;
  font-size: 0.78rem;
  font-weight: 650;
  /* Trailing letter-space is trimmed so the text optically centres rather than
     sitting a couple of pixels left. */
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.sidebar-nav { padding: 10px 12px 16px; flex: 1; overflow-y: auto; }

/* A rule above each group but the first, so the two halves of the column read
   as two groups rather than as one list with a word dropped into it. `+ .nav-link`
   is not used: the rule belongs to the heading, and the heading is what moves if
   a group is ever added. */
.nav-section {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  padding: 10px 8px 8px;
  font-weight: 650;
}
.nav-section:not(:first-child) {
  margin-top: 12px;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}

/* One row per page. Plain glyph and label at rest, a neutral grey step under
   the pointer, and the page you are on as a pale brand fill with the label in
   the brand colour - that fill is the only colour in the column.

   The glyphs used to sit in chips of their own, filled solid blue on the
   active row, with an accent bar on the column's edge beside it: three marks
   for one fact. The fill alone says it. */
.nav-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  font: inherit;
  font-size: 0.92rem;
  font-weight: 500;
  text-align: left;
  margin-bottom: 2px;
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease;
}

.nav-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 20px;
  height: 20px;
}
.nav-link .icon { flex: none; width: 16px; height: 16px; opacity: 1; }

.nav-link:hover {
  background: color-mix(in srgb, var(--ink) 5%, var(--surface));
  color: var(--ink);
  text-decoration: none;
}

.nav-link.is-active { background: var(--brand-light); color: var(--brand); font-weight: 600; }

/* A locked entry is still legible - it is an advertisement for the tier that
   unlocks it - but it must not read as somewhere you can go, and it must not
   read as *louder* than the entries that are open. Faint text and a faint
   glyph. */
.nav-link.is-locked { color: var(--ink-faint); }
.nav-link.is-locked .nav-ico { opacity: 0.75; }

/* --- Folding the sidebar ------------------------------------------------------
   The toggle sits at the foot of the column, under its own rule. The two
   labels and the two chevrons are both in the markup; `.is-collapsed` on the
   shell decides which of each shows, so the script that folds the column
   without a reload only has to move one class. */
.sidebar-toggle { margin: 0; padding: 8px 12px 12px; border-top: 1px solid var(--line-soft); }
.sidebar-toggle .nav-link { margin: 0; }
.sidebar-toggle .when-folded,
.shell.is-collapsed .sidebar-toggle .when-open { display: none; }
.shell.is-collapsed .sidebar-toggle .when-folded { display: inline; }
.shell.is-collapsed .sidebar-toggle .icon.when-folded { display: block; }

/* The square mark stands in for the lockup when the column is too narrow for
   it. Only one of the two is ever displayed. */
.brand-mark { display: none; width: 34px; height: 34px; margin: 0 auto; }

.sidebar { transition: width 0.18s ease, flex-basis 0.18s ease; }

/* Folded: 68px of glyphs, each naming itself in its `title`. Above the phone
   breakpoint only - at 720px and below the sidebar is already a row of icons
   across the top, and the toggle is hidden there. */
@media (min-width: 721px) {
  .shell.is-collapsed { --sidebar-width: 68px; }
  .shell.is-collapsed .sidebar-brand { padding: 0 10px; }
  .shell.is-collapsed .brand-logo { display: none; }
  .shell.is-collapsed .brand-mark { display: block; }
  .shell.is-collapsed .sidebar-nav { padding: 10px 12px 16px; }
  /* A heading becomes the rule above its group, with the words gone. */
  .shell.is-collapsed .nav-section { font-size: 0; padding: 0; margin: 10px 2px; }
  .shell.is-collapsed .nav-section:first-child { display: none; }
  .shell.is-collapsed .nav-link { justify-content: center; padding: 8px 0; }
  .shell.is-collapsed .nav-text {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
  /* The padlock rides the glyph's corner rather than trailing a label that is
     no longer there. */
  .shell.is-collapsed .nav-lock { position: absolute; top: 3px; right: 5px; margin: 0; }
  .shell.is-collapsed .nav-lock .icon { width: 10px; height: 10px; }
  .shell.is-collapsed .nav-flag { display: none; }
}

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  padding: 0 26px;
  /* Matched to the brand box beside it - see `--header-h`. */
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  position: sticky;
  top: 0;
  z-index: 20;
}

.topbar-title { font-weight: 640; font-size: 1.02rem; letter-spacing: -0.01em; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }

/* --- The account menu -------------------------------------------------------
   Who, which tier, the plan, billing and the way out - folded into one control
   in the bar. They used to be at the foot of the sidebar, which is a column of
   the *page* rather than of the window: on a long page - an issued invoice is
   the longest - its foot sat at the bottom of the document, so the two things
   somebody needs at any moment were a scroll to the end away. The bar is
   sticky, so from here they are always on screen.

   A <details>, not a scripted menu: it opens, closes and is reachable by
   keyboard with no JavaScript at all. `shell.js` only adds the one thing the
   element cannot do for itself - closing when the click lands somewhere else. */
.account { position: relative; }

.account > summary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 9px 5px 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  cursor: pointer;
  list-style: none;
  transition: border-color 0.12s ease, background 0.12s ease;
}

/* Safari and Chrome each draw their own marker on a <summary>; both are the
   wrong triangle in the wrong place next to a caret of our own. */
.account > summary::-webkit-details-marker { display: none; }
.account > summary::marker { content: ""; }
.account > summary:hover { background: var(--brand-tint); border-color: var(--brand-light); }
.account[open] > summary { background: var(--brand-tint); border-color: rgba(0, 76, 162, 0.35); }
.account > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The initial, which is identity enough at this size - an address in a bar is
   read once and then only ever takes up room. It is in the panel below. */
.account-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--brand);
  color: var(--on-brand);
  font-size: 0.78rem;
  font-weight: 700;
}

.account-plan { font-size: 0.82rem; font-weight: 600; color: var(--ink); }
.account-caret { width: 11px; height: 11px; color: var(--ink-faint); }
.account[open] .account-caret { transform: rotate(180deg); }

.account-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 40;
  min-width: 248px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}

.account-who {
  display: grid;
  gap: 1px;
  padding: 9px 10px 11px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 6px;
}
.account-who strong { font-size: 0.9rem; }
.account-who .tiny { color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; }

.account-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
  text-align: left;
  cursor: pointer;
}
.account-item:hover { background: var(--brand-tint); text-decoration: none; }

/* Where you are. The sidebar used to say it; these pages left it when they
   moved into this menu, and a menu that opens on the page you are already
   looking at should admit as much. */
.account-item.is-active { background: var(--brand-light); color: var(--brand); font-weight: 600; }
.account-item.is-active .icon { color: var(--brand); }
.account-item .icon { color: var(--ink-faint); flex: none; }
.account-item:hover .icon { color: var(--brand); }

/* The tier sits at the end of its own row, so the row reads as a fact with a
   link on it rather than as a menu entry that happens to have a word after. */
.account-value { margin-left: auto; color: var(--brand); font-weight: 650; }

.account-signout {
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  margin-top: 6px;
  padding-top: 11px;
}
.account-signout:hover { background: var(--danger-bg); color: var(--danger); }
.account-signout:hover .icon { color: var(--danger); }

/* The menu's own form must not inherit the block layout a bare <form> has, or
   the sign-out row sits below the panel's padding rather than in it. */
.account-panel form { margin: 0; }

/* --- Dropdown menus ----------------------------------------------------------
   The same <details> the account menu is built from, for anywhere a set of
   actions is worth one control instead of six: the "New invoice" split button,
   and the overflow menu at the end of a table row.

   Two triggers are provided. `.menu-toggle` is the caret welded to the right of
   a real button - a split button, where the button itself still does the
   obvious thing and the caret opens the variants. `.btn-icon` is the bare
   ellipsis used in a row. Both open the same panel. */
.menu { position: relative; display: inline-flex; }
.menu > summary { list-style: none; cursor: pointer; }
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::marker { content: ""; }
.menu > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* A split button: one control, two halves, one border between them. The pair
   sits in a flex row so the two keep the same height whatever the label does. */
.split { display: inline-flex; align-items: stretch; }
.split > .btn:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.split > .menu > summary.btn {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  /* The two halves share an edge rather than drawing one each, which at 1px
     apiece reads as a hairline gap down the middle of the button. */
  margin-left: -1px;
  padding-left: 9px;
  padding-right: 9px;
}
.split > .menu > summary.btn .icon { width: 12px; height: 12px; }
.menu[open] > summary.btn .icon { transform: rotate(180deg); }
/* The caret half of a primary split button keeps the primary's own colours, and
   a divider light enough to read on them. */
.split > .btn-primary + .menu > summary.btn {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  /* The divider between the two halves, as a wash of whatever the text is - so
     it stays readable on the pale blue the dark theme fills this button with. */
  border-left-color: color-mix(in srgb, var(--on-brand) 35%, transparent);
}
.split > .btn-primary + .menu > summary.btn:hover {
  background: var(--brand-dark);
  border-color: var(--brand-dark);
}

.menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 40;
  min-width: 210px;
  /* A ceiling as well as a floor. Without it a single long label - the "this
     invoice is locked" sentence was one - stretches the panel to 665px, which
     is half the table. */
  max-width: 280px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  /* Left to itself the panel inherits the row's `white-space: nowrap` from the
     table cell it opens in, and a two-word label runs off its own edge. */
  white-space: normal;
}
.menu-panel form { margin: 0; }

/* Set by shell.js when there is no room below - see `place()` there. The last
   row of a table is the case this exists for: `.table-wrap` scrolls sideways,
   and a box with `overflow-x: auto` clips on both axes, so a panel hanging off
   the bottom row was cut in half by its own table. */
.menu[data-drop="up"] > .menu-panel { top: auto; bottom: calc(100% + 6px); }

.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
  font-weight: 500;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
.menu-item:hover { background: var(--brand-tint); text-decoration: none; }
.menu-item .icon { color: var(--ink-faint); flex: none; }
.menu-item:hover .icon { color: var(--brand); }
.menu-item.is-danger:hover { background: var(--danger-bg); color: var(--danger); }
.menu-item.is-danger:hover .icon { color: var(--danger); }
/* A locked action stays in the menu - hiding it means nobody discovers the
   feature they would upgrade for - with the padlock at the end of its row. */
.menu-item.is-locked { opacity: 1; color: var(--ink-faint); }
.menu-item.is-locked .icon:last-child { margin-left: auto; }
.menu-item.is-disabled { color: var(--ink-faint); cursor: default; }
.menu-item.is-disabled:hover { background: none; }
.menu-sep { height: 1px; margin: 5px 4px; background: var(--line); }

.content { padding: 26px; flex: 1; }
/* Capped and left-aligned, against the sidebar. The pages that were leaving a
   void on the right are on `content-wide` now and fill it; what stays narrow
   is the short stuff - a delete confirmation, a password change - where a
   sentence stretched across 1200px would be worse than the space beside it. */
.content-narrow { max-width: 780px; }
.content-wide { max-width: 1200px; }

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: var(--gap);
  flex-wrap: wrap;
}

.page-head h1 { margin-bottom: 2px; }

/* A leading icon needs a gap of its own. `.icon` is an inline SVG sized in
   `em`, so with nothing between it and the next character it sits flush
   against the letterform - which reads as a rendering fault rather than as a
   tight lockup.

   Listed rather than applied to every `.icon`, because the places that lead
   with one and already look right - `.stat-label`, `.feature h3`, the late-fee
   card head - are flex containers with a `gap`, and a margin here would be
   added to that gap rather than replacing it.

   `:first-child` so this only ever affects an icon that *leads*; one used
   inline mid-sentence brings its own spacing from the text around it. */
.page-head h1 > .icon:first-child,
.legal-updated > .icon:first-child,
table.data th > .icon:first-child,
.plan-period label > .icon:first-child {
  margin-right: 0.45em;
}
.page-head .sub { color: var(--ink-muted); font-size: 0.92rem; }
/* The header action row. Nine buttons on an issued invoice, and the labels are
   longer in Bosnian, so it is tightened here rather than at .btn - the same
   buttons elsewhere have room to breathe.

   `flex-wrap` stays as a safety net: an overflowing nowrap row would clip or
   push the page sideways, which is worse than a second line. At 1440px it fits
   on one line in both languages with all nine buttons - measured, not asserted;
   the test suite cannot see layout. Re-measure if a tenth action is added. */
.page-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  /* Grow to fill the line. Without this the row sizes to its content, so
     `flex-end` has no free space to push anything into once it has wrapped onto
     a line of its own - which is exactly when the alignment matters. */
  flex: 1 1 auto;
  justify-content: flex-end;
}

/* The document-language picker is not an action - it changes what the PDF is
   rendered in. Held hard left so the slack in the row sits between it and the
   first button, rather than trailing off the right-hand edge past the last one. */
.page-actions #doc-language { margin-right: auto; }

.page-actions .btn {
  padding: 7px 11px;
  gap: 6px;
}

/* An icon must never be the thing that gets squeezed - at one point the row
   fitted with a single pixel to spare and the Duplicate glyph was clipped. */
.page-actions .btn .icon { flex: none; }

/* --- Cards ----------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.card + .card { margin-top: var(--gap); }

/* ...but not between grid siblings. The rule is for cards stacked down a column;
   inside a grid it landed on the second card in the row, whose stretched box then
   sat 20px lower than the first and 20px shorter. That is what pulled the
   "Due in the next 14 days" card out of line with "Recent invoices", and the
   charts below them out of line with each other. */
.grid > .card + .card { margin-top: 0; }
/* min-height so a head with an action button and a head with only a title come
   out the same height - 30px of padding, a 1px border and a 32px btn-sm. Two
   cards side by side otherwise start their tables at different heights. */
/* Flat, on the card's own surface: the hairline under it is what separates
   the head from the body. It used to take a brand wash as well, and a page of
   cards each opening on a blue gradient made colour the loudest thing on
   screen when it should mark only what needs acting on. */
.card-head {
  min-height: 63px;
  padding: 15px 18px;
  border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.card-head h2, .card-head h3 { margin: 0; font-size: 0.98rem; }
.card-body { padding: 18px; }
/* `tight` exists for a table, which brings its own cell padding and wants to
   run to the card's edges. Anything else inside one has to supply its own -
   see `.doc-row`, which for a long time did not and sat flush against the
   border. */
.card-body.tight { padding: 0; }

/* A row in a list of things: a team member, an open order, an attempt to send
   a document. Three templates share it and none of them had any styling at
   all, so the label, the detail and the button stacked in a column with no
   padding.

   The same shape as `.contact-row`, `.api-key-row` and `.audit-row`, and
   deliberately so: they are all the same component - something on the left,
   what you can do about it on the right - and four near-identical rules that
   drift apart is worse than one that is used four times. The horizontal
   padding matches `.card-body`, so a `tight` card and a normal one line up.

   Not to be confused with `.hero-doc .doc-row` in landing.css, which is a
   decorative line in the fake invoice on the landing page. That one is scoped
   under `.hero-doc` and lives in a stylesheet this file never meets. */
.doc-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--line-soft);
  flex-wrap: wrap;
}
.doc-row:last-child { border-bottom: 0; }
/* `min-width: 0` so a long address or an error message wraps inside the row
   instead of pushing the button off the card. */
.doc-info { min-width: 0; flex: 1 1 auto; }
.doc-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.card-foot {
  padding: 13px 18px;
  border-top: 1px solid var(--line);
  /* A neutral step off the card, not a brand wash - the action row is set apart
     by where it is, and the colour is left to the button in it. */
  background: color-mix(in srgb, var(--ink) 2.5%, var(--surface));
  border-radius: 0 0 var(--radius) var(--radius);
}

.grid { display: grid; gap: var(--gap); }
/* One column, and the `minmax(0, 1fr)` is the whole point of the class: a grid
   left to its own devices places the item in an `auto` column, which refuses to
   go below the content's min-width. A card holding a wide table then grows past
   the page instead of letting `.table-wrap` scroll - the same failure the
   narrow-screen rule at the bottom of this file exists to prevent. */
.grid-1 { grid-template-columns: minmax(0, 1fr); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* The subject of a page and the panel beside it: line items and the summary,
   the message and what it will send, the schedule and its source.

   A class rather than the inline style these three pages used to carry. An
   inline `grid-template-columns` cannot be overridden by a media query without
   `!important`, so the two columns stayed side by side at every width - the
   invoice detail page came out 524px wide on a 390px screen, with the summary
   squeezed to a column about eight characters across. */
.grid-main-aside { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }

/* --- Stats ----------------------------------------------------------------- */

/* A tile carries one number, so everything else in it is kept quiet - quieter
   than it was. The tile used to open on a wash of its state colour and carry a
   ruled texture in the corner; on a row of five that was five tints and five
   textures competing with the figures. It is a plain card now, and the state
   is said by the icon chip and the figure, which are small enough to take a
   full-strength colour. */
.stat {
  position: relative;
  overflow: hidden;
  /* Same three-value scheme the table rows use, for the same reason: a tile and
     a row that mean the same thing must not be two different colours. The hue
     drives the border and the icon chip together, so a tile changes state as
     one object rather than as tinted parts. */
  --tile-color: var(--brand);
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--tile-color) 10%, var(--line));
  border-radius: var(--radius);
  padding: 16px 18px;
  box-shadow: var(--shadow);
  /* A column, so the note can be pushed to the foot - see `.stat-note`. The
     grid already stretches every tile in a row to the same height; this is
     what puts something useful at the bottom of that height instead of
     leaving the note wherever the figures above it happened to end. */
  display: flex;
  flex-direction: column;
}

/* The state variants. Only the hue moves, and it reaches the border and the
   icon chip - the figure takes its own colour from `.stat-value`. */
.stat.is-attention { --tile-color: var(--danger); }
.stat.is-settled { --tile-color: var(--ok); }
.stat.is-owing { --tile-color: var(--alt); }

.stat.is-attention .stat-icon { background: var(--danger-bg); color: var(--danger); }
.stat.is-settled .stat-icon { background: var(--ok-bg); color: var(--ok); }
.stat.is-owing .stat-icon { background: var(--alt-bg); color: var(--alt); }


.stat-label {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  font-weight: 600;
  margin-bottom: 6px;
}

/* The label's glyph, in a tinted chip. It names what the figure counts, and a
   bare icon at that size disappears against a tinted ground. */
.stat-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  background: var(--brand-light);
  color: var(--brand);
}
.stat-icon .icon { width: 14px; height: 14px; }

.stat-value { font-size: 1.5rem; font-weight: 680; letter-spacing: -0.02em; }
.stat-value.is-danger { color: var(--danger); }
.stat-value.is-ok { color: var(--ok); }
.stat-value.is-owing { color: var(--alt); }
/* A zero on a tile that reports a problem. Nothing to see, and it should not be
   printed in the same weight of colour as a figure that needs acting on - the
   tile's own green ground is already saying "none". */
.stat-value.is-none { color: var(--ink-muted); }
/* The line under the figure that says what it counts.
   
   These used to sit immediately under the last figure, so their position was
   decided by how many currencies a tile happened to show: on a row where one
   tile listed three currencies and the next listed one, the two explanations
   were forty pixels apart and the row read as broken.
   
   Two rules fix it together, and both are needed:
   
   * `margin-top: auto` pushes the note to the foot of a tile whose height the
     grid has already equalised, so every note in a row ends on the same line.
   * `min-height` holds open two lines' worth of band whether the note fills
     it or not, so a one-line note *starts* where a two-line note starts
     rather than sitting a line lower. Without it the bottoms line up and the
     tops do not, which is the more noticeable of the two.
   
   Two lines rather than one: every note in English, Bosnian and German runs to
   one or two at every width the grid produces, German being the long one. A
   translation that needed three would push that tile's note up a line and
   leave the rest where they are - degraded, not broken - and the fix would be
   to widen this to three, which costs a line of empty space on every tile.
   That is the trade, and two is where it currently sits. */
.stat-note {
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--ink-muted);
  margin-top: auto;
  padding-top: 8px;
  /* `em` resolves against this element's own font size, so the band tracks
     the note's type rather than the root's. The 8px is the padding above,
     which `border-box` counts inside the height. */
  min-height: calc(2 * 1.45em + 8px);
}

/* The quota meter. A flat fill in a pale channel of the same hue: the channel
   is what makes the empty part read as room left rather than as nothing. The
   rounded cap means a tiny value still draws as a mark, so `min-width` keeps
   it from being a dot. */
.meter {
  height: 8px;
  background: var(--brand-light);
  border-radius: 999px;
  margin-top: 9px;
  overflow: hidden;
}
.meter span {
  display: block;
  height: 100%;
  min-width: 8px;
  border-radius: 999px;
  background: var(--brand);
  transition: width 0.35s ease;
}
.meter.is-full span { background: var(--danger); }

/* The overdue ageing bar. One rule, three bands, and the bands are one hue at
   three strengths rather than three hues - they are degrees of the same thing,
   and a sequential ramp is what says so. A categorical set here would claim they
   are different kinds of overdue.

   The ramp runs on --danger, so it moves with the theme like everything else,
   and the palest band still clears 3:1 against the card. */
.ageing-head { margin-bottom: 12px; }
.ageing-head .stat-label { margin-bottom: 0; }
.stat-icon.is-attention { background: var(--danger-bg); color: var(--danger); }

.ageing-row + .ageing-row { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }

/* The currency and what it comes to, above its bands. The old layout had
   neither: three bars sat stacked with the currency discoverable only by
   reading the amounts underneath, so the first question a reader had - which
   of these is which - was the one thing the block did not answer. */
.ageing-row-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.ageing-total {
  font-size: 1.15rem;
  font-weight: 680;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

/* The verdict: what share sits in the worst band, and which band that is.
   Graded, because "all of it is a week late" and "all of it is past ninety
   days" are different news and should not be the same colour. */
.ageing-verdict {
  font-size: 0.76rem;
  font-weight: 650;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--neutral-bg);
  color: var(--ink-muted);
  white-space: nowrap;
}
.ageing-verdict.is-bad { background: var(--warn-bg); color: var(--warn); }
.ageing-verdict.is-grave { background: var(--danger-bg); color: var(--danger); }

/* Three fixed cells, always present and always in the same order, so a band
   is identified by where it is rather than by what colour it is. On a narrow
   screen they stack rather than squeezing: three money figures side by side
   in 320px is three figures nobody can read. */
.ageing-bands {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (max-width: 560px) {
  .ageing-bands { grid-template-columns: 1fr; gap: 10px; }
}

.ageing-band-cell { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.ageing-band-name {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--ink-faint);
}

/* The track is always drawn, so an empty band reads as an empty band. That is
   the whole point of the rebuild: a band with nothing in it should be visibly
   nothing, not a figure of zero with the same weight as the one holding the
   money. */
.ageing-band-track {
  height: 10px;
  border-radius: 999px;
  background: var(--neutral-bg);
  overflow: hidden;
  box-shadow: inset 0 1px 2px rgb(22 32 44 / 0.10);
}
.ageing-band-fill { display: block; height: 100%; border-radius: 999px; }

/* One hue at three strengths, not three hues: these are degrees of the same
   thing and a sequential ramp is what says so. The colour is now a second
   channel behind position, rather than the only one. */
.ageing-d30 { background: color-mix(in srgb, var(--danger) 45%, var(--surface)); }
.ageing-d60 { background: color-mix(in srgb, var(--danger) 72%, var(--surface)); }
.ageing-d60plus { background: var(--danger); }

.ageing-band-figure {
  font-size: 0.88rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.ageing-band-figure .is-none { font-weight: 500; color: var(--ink-faint); }
/* The band holding the most is the one worth looking at first. */
.ageing-band-cell.is-worst .ageing-band-name { color: var(--ink-muted); }
.ageing-band-cell:not(.has-money) .ageing-band-name { opacity: 0.65; }

/* --- Tables ---------------------------------------------------------------- */

.table-wrap { overflow-x: auto; }

/* The column heads stay in view while a long table scrolls under them - but
   only on a table that fits its card. `.table-wrap` scrolls sideways on a
   narrow screen, and a box that scrolls on one axis is the box a sticky header
   sticks inside, which is to say nowhere. `shell.js` measures each table and
   sets `.fits` where nothing needs scrolling sideways; there the wrapper gives
   up its overflow and the heads can stick to the window, under the top bar.
   Without the script every table simply keeps its heads where they are. */
.table-wrap.fits { overflow: visible; }
.table-wrap.fits table.data thead th {
  position: sticky;
  top: var(--sticky-top);
  z-index: 5;
  /* A collapsed border scrolls away with the body; the rule under a stuck
     head has to be drawn by the cell itself. */
  box-shadow: inset 0 -1px 0 var(--line);
}

table.data { width: 100%; border-collapse: collapse; font-size: 0.9rem; }

/* The header labels the columns and should not compete with them: muted grey
   words on a faint neutral band, closed by the hairline. It used to be a blue
   gradient with blue type, which made the one row nobody reads the most
   coloured row in the table - colour belongs to status and money. */
table.data th {
  text-align: left;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  font-weight: 600;
  padding: 11px 14px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  background: color-mix(in srgb, var(--ink) 3%, var(--surface));
}

table.data th .icon { opacity: 0.55; margin-right: 6px; vertical-align: -0.15em; }

table.data td { padding: 11px 14px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
table.data tbody tr:last-child td { border-bottom: none; }

/* --- Row state -------------------------------------------------------------

   A row is tinted by what it says. The status badge names the state in words;
   the wash is what lets somebody see, without reading a single cell, that four
   of twenty-five invoices need chasing.

   Every row carries three inherited values - a hue, a resting strength and a
   hover strength - and composes them into a translucent wash. The state classes
   below set the three; everything else is one rule. Three things fall out of
   doing it that way rather than writing a background per state:

   * An untinted row is not a special case. It is the brand hue at 0%, so hover
     is a single declaration that works on a plain row and a red one alike, and
     a hovered overdue row deepens into its own colour instead of flattening to
     blue and losing the state while the pointer is on it.
   * The defaults sit on the table, not the row, so a `tr.row-*` class beats
     them by inheritance rather than by specificity, and the levels below need
     only one class more than the state they grade.

   Strengths run 4-18%. Under 4% there is nothing to see. The ceiling is set by
   legibility rather than by taste: at 18% of the danger hue, muted text - the
   client names and dates filling most of the row - measures 4.5:1 against the
   ground, which is exactly AA, and every step past that trades a distinction
   somebody might notice for text they have to work at. */
table.data {
  --row-color: var(--brand);
  --row-tint: 0%;
  --row-hover: 5%;
  --row-accent: transparent;
}

/* No stripe. Every second row used to carry a faint brand band, so a page of
   invoices was striped blue on top of its state tints; the hairline between
   rows and the hover below carry the eye along a row without it, and the
   only colour left in a table is the state of each document. */
table.data tbody tr {
  background-color: color-mix(in srgb, var(--row-color) var(--row-tint), transparent);
  transition: background-color 0.12s ease;
}

/* A row lights up under the pointer, which is also what says the row is a thing
   rather than nine separate cells. */
table.data tbody tr:hover {
  background-color: color-mix(in srgb, var(--row-color) var(--row-hover), transparent);
}

/* The state's edge. A wash alone is ambiguous at the top of a long list - there
   is nothing above the first row to compare it against - and a 3px rule at full
   strength is not. Drawn as an inset shadow because the table is
   `border-collapse: collapse`, where a border on the cell would move the column
   and be shared with its neighbour. */
table.data tbody td:first-child { box-shadow: inset 3px 0 0 var(--row-accent); }

/* Past due with a balance. The one state that is asking for something to be
   done, so it takes the graded ladder and sits at its middle stop by default. */
tr.row-attention { --row-color: var(--danger); --row-tint: var(--tint-2); --row-hover: var(--tint-2-hover); --row-accent: var(--danger); }

/* Part settled and not yet due. Amber: under way, nothing wrong. Graded on the
   same ladder as attention rather than on one two points weaker - the hue is
   what tells them apart, and two ladders a percentage point apart was a
   distinction nobody could see and everybody had to maintain. */
tr.row-progress { --row-color: var(--warn); --row-tint: var(--tint-2); --row-hover: var(--tint-2-hover); --row-accent: var(--warn); }

/* Nothing further owed. The quiet stop, on purpose - a settled invoice is the
   one row nobody needs to look at, and green shouting for attention is exactly
   backwards. */
tr.row-settled { --row-color: var(--ok); --row-tint: var(--tint-quiet); --row-hover: var(--tint-quiet-hover); --row-accent: var(--ok); }

/* Money to go back to the client. Something to do, so it is louder than the
   quiet stop and flatter than an overdue row, which is more urgent. */
tr.row-owing { --row-color: var(--alt); --row-tint: var(--tint-1); --row-hover: var(--tint-1-hover); --row-accent: var(--alt); }

/* A draft: not yet a document anybody owes anything on. Grey rather than a hue,
   because it is the absence of a state rather than one of them. */
tr.row-pending { --row-color: var(--ink-faint); --row-tint: var(--tint-quiet); --row-hover: var(--tint-quiet-hover); --row-accent: var(--line-strong); }

/* Void or cancelled: on the record, out of every figure. The same grey as a
   draft - they are both "not in play" - and the text goes faint with it, which
   is what tells the two apart at a glance. The figures in this row are real
   numbers that count towards nothing, and that is worth saying twice. Links keep
   the brand colour: the row is out of the reckoning, not out of reach. */
tr.row-closed { --row-color: var(--ink-faint); --row-tint: var(--tint-quiet); --row-hover: var(--tint-quiet-hover); --row-accent: var(--line-strong); }
tr.row-closed td { color: var(--ink-faint); }

/* How much is still outstanding, in three steps, on the two states that
   describe a part-settled balance. This is the amount paid showing through the
   colour: an overdue invoice with nine tenths of the money in is a pale wash, an
   untouched one is a definite red - and the difference between the two is
   visible across a page in a way that two identical badges are not.

   Steps rather than a percentage ramp. A continuous scale down twenty-five rows
   reads as noise; three levels read as a scale. `.lvl-2` is the state's own
   default and needs no rule. */
tr.row-attention.lvl-1,
tr.row-progress.lvl-1 { --row-tint: var(--tint-1); --row-hover: var(--tint-1-hover); }
tr.row-attention.lvl-3,
tr.row-progress.lvl-3 { --row-tint: var(--tint-3); --row-hover: var(--tint-3-hover); }

/* The total row under a values table. Ruled off with a heavier line than the
   rows carry, because a sum is a different kind of number from the twelve above
   it, and the rule is the whole of what says so. The label cell is a `th` and so
   picks up the head's own tint, which is what ties the two ends of the table
   together. */
table.data tfoot td { border-top: 2px solid var(--line-strong); font-weight: 650; }
/* The line that says why a column has a dash where a total should be. Inside the
   table's own container, so it travels with the thing it explains. */
.chart-note { margin: 8px 14px 0; }
table.data tfoot th { border-top: 2px solid var(--line-strong); border-bottom: none; }

table.data .num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
table.data .shrink { width: 1%; white-space: nowrap; }

/* An empty state is the first thing a new account sees, so its icon sits in a
   tinted chip rather than being a grey glyph on white. On the card's own
   surface - the radial wash it used to stand in went with the rest. */
.empty {
  padding: 44px 20px;
  text-align: center;
  color: var(--ink-muted);
}
.empty .empty-icon {
  box-sizing: content-box;
  width: 28px;
  height: 28px;
  padding: 13px;
  margin-bottom: 12px;
  border-radius: 50%;
  background: var(--brand-light);
  color: var(--brand);
  opacity: 1;
}
.empty h3 { color: var(--ink); }

/* --- Icons ----------------------------------------------------------------- */

/* Bootstrap Icons, inlined as an SVG sprite. Sized in em so an icon always
   matches the type around it, and coloured by currentColor. */
/* overflow is not decoration. Forty of the fifty-five glyphs in the sprite are
   drawn flush to the edge of their 0 0 16 16 viewBox - every circle, every
   square, check-circle among them - and an <svg> clips at its viewport by
   default. Sizing in em puts that viewport on a fractional pixel (1em of the
   0.86rem note on the import page is 13.76px), so the outermost sliver of the
   stroke falls outside and is cut rather than antialiased. Firefox snaps the
   viewport hardest and loses the whole bottom of a ring; Chromium shaves a
   device pixel. Letting the glyph paint past its viewport costs nothing in
   layout - the box is still 1em - and closes the circle in both. */
.icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  flex: none;
  vertical-align: -0.125em;
  overflow: visible;
}

.btn .icon { font-size: 1.05em; }
.icon-lg { width: 1.35em; height: 1.35em; }

/* --- Buttons --------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 8px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 0.89rem;
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s ease, border-color 0.12s ease;
}

/* The border moves with the fill. At --hover-mix the wash alone is a small
   step on a card that is already near-white, or already near-black; the edge is
   what makes the button look picked up rather than repainted. */
.btn:hover { background: var(--hover-fill); border-color: var(--select-line); text-decoration: none; }
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }

.btn-primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.btn-primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); }

.btn-danger { color: var(--danger); border-color: var(--danger-line); }
/* Its own edge on hover, at the strength .btn:hover would otherwise hand it in
   the brand hue - a red button that turns blue at the border on the way to
   being pressed is the one place the shared rule above is wrong. */
.btn-danger:hover {
  background: var(--danger-bg);
  border-color: color-mix(in srgb, var(--danger) var(--select-line-mix), var(--surface));
}

.btn-ghost { border-color: transparent; background: transparent; }
.btn-ghost:hover { background: var(--hover-fill); }

.btn-sm { padding: 5px 10px; font-size: 0.82rem; }
.btn-lg { padding: 11px 22px; font-size: 0.98rem; }
.btn-block { width: 100%; }

.btn-group { display: inline-flex; gap: 6px; flex-wrap: wrap; }

/* Square icon-only button, for inline table row actions. */
.btn-icon {
  padding: 6px;
  width: 30px;
  height: 30px;
  color: var(--ink-muted);
  border-color: transparent;
  background: transparent;
}

.btn-icon:hover { background: var(--select-fill); color: var(--select-ink); border-color: transparent; }
.btn-icon .icon { font-size: 1rem; }

/* Destructive row action: neutral until hovered, so it does not shout from
   every row, but unmistakable once the pointer is on it. */
.btn-icon-danger:hover { background: var(--danger-bg); color: var(--danger); }

.row-actions { display: inline-flex; gap: 2px; align-items: center; }

/* --- Forms ----------------------------------------------------------------- */

/* Positioned so anything a browser or password manager injects into a field -
   a reveal button, a fill icon - anchors to the field it belongs to. Without
   it those land against whatever ancestor happens to be positioned, which is
   how the password reveal button ended up under the inputs rather than in
   them. */
.field { position: relative; margin-bottom: 15px; }
.field:last-child { margin-bottom: 0; }

.field label,
.form-label {
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-muted);
  margin-bottom: 5px;
}

.field .req { color: var(--danger); }

input[type="text"], input[type="email"], input[type="password"], input[type="number"],
input[type="date"], input[type="url"], input[type="tel"], input[type="search"],
select, textarea {
  width: 100%;
  padding: 8px 11px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.92rem;
  color: var(--ink);
  background: var(--surface);
}

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(0, 76, 162, 0.12);
}

input[type="color"] { padding: 2px; height: 38px; cursor: pointer; }

/* At identical padding and font a select computes 38px against an input's 41px,
   so any row mixing the two - the KIF filter, the invoice filters, the form
   grids - stepped out of line by 3px. Matched to the input rather than the
   other way round, because inputs are the majority everywhere. */
select { height: 41px; }
textarea { resize: vertical; min-height: 72px; }

.field-help { font-size: 0.79rem; color: var(--ink-faint); margin-top: 4px; }

/* Chrome, Edge and Safari draw a reveal control inside a password box. Without
   the reserved space it sits on top of the last characters typed. */
input[type="password"] { padding-right: 2.4rem; }
.field-error { font-size: 0.81rem; color: var(--danger); margin-top: 4px; }
.field.has-error input, .field.has-error select, .field.has-error textarea { border-color: var(--danger); }

/* **One grid for every row of fields: four columns, always.**

   Rows used to declare their own column count - two here, three there, four
   somewhere else - so a box holding a postcode was a different width on every
   card, and nothing lined up down the page. Now a row is always four columns
   and a field is one of them, so a line with two fields leaves two columns
   empty rather than stretching its two boxes to half the page each. The boxes
   are the same width wherever they are.

   Width is declared by the *field* instead, through `span` on
   `core/field.html`: two columns for something that needs room to be read,
   four for a textarea or a value that runs to a sentence. That is a property
   of the field, not of the row it happens to share.

   Four is the ceiling because of what it holds: a postcode, a rate, a number
   of days. Below 1080px it folds to two - four boxes on a laptop is four
   boxes nobody can read - and to one at 720px, where everything stacks. A
   span collapses with it, or a field asking for two columns of two would take
   the whole row and leave the next one beside nothing. */
.field-row { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.field-span-2 { grid-column: span 2; }
.field-span-3 { grid-column: span 3; }
.field-span-4 { grid-column: 1 / -1; }

@media (max-width: 1080px) {
  .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .field-span-3, .field-span-4 { grid-column: 1 / -1; }
}

/* Four columns is four to the width of the page, not four per card. A card
   sharing a `.grid-2` with another - the invoice form's details beside its
   presentation - has half that width, and four columns in it were boxes too
   narrow to show a date. So it gets two at every width: one card across the
   page holds four fields to a line, two cards hold two each.

   `:where()` so this weighs no more than `.field-row` itself. Plain
   `.grid-2 .field-row` would outrank the single-column rule at 720px further
   down, and the two cards would keep their pairs on a phone. */
:where(.grid-2) .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
:where(.grid-2) .field-span-3,
:where(.grid-2) .field-span-4 { grid-column: 1 / -1; }

/* The same for the sign-up card, which is 520px at its widest: four columns
   left first and last name, and the two passwords, in half the row each at a
   quarter of the card. Two to a line is what its rows hold. */
:where(.auth-column) .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
:where(.auth-column) .field-span-3,
:where(.auth-column) .field-span-4 { grid-column: 1 / -1; }

.checkbox { display: flex; align-items: center; gap: 8px; }
.checkbox input { width: auto; }
.checkbox label { margin: 0; font-weight: 500; color: var(--ink); }

fieldset { border: none; padding: 0; margin: 0 0 22px; }
legend {
  padding: 0;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  font-weight: 650;
  margin-bottom: 10px;
}

/* --- Badges ---------------------------------------------------------------- */

.badge {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 20px;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  background: var(--line);
  color: var(--ink-muted);
  white-space: nowrap;
}

/* Every status badge carries a hairline in its own hue.

   On white a chip needs no edge - it is a pale shape on a white ground and reads
   as one. On a row tinted with the same hue it is a pale shape on a slightly
   less pale ground: "Paid" on a settled row measured 1.06:1 against it, which is
   to say the pill was invisible and only the word was left. The border restores
   the shape without making either colour louder.

   It also makes the set consistent. Void, Overpaid and Refund already carried
   borders, so the badges in one column were two different heights. */
.badge { border: 1px solid transparent; }
.badge-draft { background: var(--neutral-bg); color: var(--ink-muted); border-color: var(--neutral-line); }
.badge-sent { background: var(--info-bg); color: var(--brand); border-color: color-mix(in srgb, var(--brand) var(--hairline-mix), var(--surface)); }
.badge-partial { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.badge-paid { background: var(--ok-bg); color: var(--ok); border-color: var(--ok-line); }
.badge-overdue { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-line); }
.badge-cancelled { background: var(--neutral-bg); color: var(--ink-faint); border-color: var(--neutral-line); text-decoration: line-through; }
.badge-brand { background: var(--brand-light); color: var(--brand); border-color: color-mix(in srgb, var(--brand) var(--hairline-mix), var(--surface)); }

/* A status badge leads with a dot in its own colour - the shape a status takes
   everywhere else now, and the thing the eye finds first scanning down a
   column. Status badges only: a plan, a type code or a "Built-in" label is a
   name, not a state, and gets no dot. */
.badge-draft::before,
.badge-sent::before,
.badge-partial::before,
.badge-paid::before,
.badge-overdue::before,
.badge-cancelled::before,
.badge-credited::before,
.badge-void::before,
.badge-overpaid::before,
.badge-refund::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 5px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: 0.1em;
}

/* --- Messages -------------------------------------------------------------- */

.messages { margin-bottom: var(--gap); display: grid; gap: 8px; }

.alert {
  padding: 11px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-size: 0.9rem;
}

/* The leading icon ran straight into the text ("⚠Columns follow..."). Spaced
   inline rather than with flex on the alert: several alerts are a sentence with
   a link inside it, and flex would break that link onto a column of its own. */
.alert > svg:first-child { margin-right: 7px; }

.alert-success { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok); }
.alert-error, .alert-danger { background: var(--danger-bg); border-color: var(--danger-line); color: var(--danger); }
.alert-warning { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.alert-info { background: var(--info-bg); border-color: color-mix(in srgb, var(--brand) var(--hairline-mix), var(--surface)); color: var(--brand-dark); }

/* --- Pagination ------------------------------------------------------------
   Under every list, and the same one under each: the size control on the left,
   what is on screen in the middle, the way through the pages on the right. It
   sits inside the card's own padding rather than under it, so the table and its
   controls read as one object. */

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 16px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
  color: var(--ink-muted);
}

.pager-size { display: flex; align-items: center; gap: 10px; }
.pager-size-label {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 600;
}

/* A segmented control: one track, four numbers, the current one raised out of
   it. The set is small and fixed, so showing all of it costs a few pixels and
   saves opening anything - and the raised tile is the state, which a select
   only shows once it is closed again. */
.segmented {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 999px;
}

.segmented-option {
  min-width: 34px;
  padding: 4px 10px;
  border-radius: 999px;
  color: var(--ink-muted);
  font-size: 0.82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.segmented-option:hover { background: var(--hover-fill); color: var(--select-ink); text-decoration: none; }
/* The ring, not the drop shadow, is what says "this one". A shadow on a dark
   ground has nothing darker to fall on, so the raised tile it lifts on white
   does not exist there; the inset rule is the same mark in both. */
.segmented-option.is-active {
  background: var(--select-fill);
  color: var(--select-ink);
  box-shadow: inset 0 0 0 1px var(--select-line), 0 1px 2px rgb(22 32 44 / 0.12);
}
.segmented-option:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.pager-range { font-variant-numeric: tabular-nums; }
.pager-links { display: flex; align-items: center; gap: 4px; }

/* Round, because they are one glyph each and a rounded rectangle round a single
   chevron reads as a button with something missing from it. */
.pager-links .btn-icon { width: 30px; height: 30px; padding: 0; border-radius: 50%; }

.pager-page {
  padding: 0 10px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The ends of the list keep their buttons in place rather than removing them,
   so the row does not shuffle sideways as somebody pages through it. */
.pager-links .is-disabled { opacity: 0.35; pointer-events: none; }

/* --- Filters --------------------------------------------------------------- */

.filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.filters .field { margin: 0; min-width: 150px; flex: 1 1 150px; }
.filters .field-actions { display: flex; gap: 8px; }

/* --- Utilities ------------------------------------------------------------- */

.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }
.small { font-size: 0.85rem; }
.tiny { font-size: 0.78rem; }
.strong { font-weight: 650; }
.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }
.right { text-align: right; }
.center { text-align: center; }
.num { font-variant-numeric: tabular-nums; }

/* The public link on the invoice page: the URL and its copy button. */
.share-row { display: flex; gap: 8px; align-items: center; }
.share-row .share-url { flex: 1; min-width: 0; font-family: var(--mono); font-size: 0.82rem; }

/* The "confirm your address" strip above the page content. */
.verify-banner { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.verify-banner form { margin: 0; }

/* A list whose bullets are icons: the close-account page's "what goes" list.
   The same shape as .legal-list in landing.css, which app pages do not load. */
.icon-list { list-style: none; margin: 12px 0 14px; padding: 0; display: grid; gap: 8px; }
.icon-list li { display: flex; align-items: flex-start; gap: 10px; line-height: 1.55; }
.icon-list .icon { flex: none; margin-top: 0.25em; color: var(--danger); }

.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }
.mt-3 { margin-top: 24px; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }
.mb-3 { margin-bottom: 24px; }

.flex { display: flex; }
.flex-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.flex-gap { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.stack { display: grid; gap: 14px; }

.divider { border-top: 1px solid var(--line); margin: 18px 0; }

/* --- Taking a card ----------------------------------------------------------
   The scheme marks, the currency statement and the policy links. They appear on
   the public pricing section, the signed-in plan page, the billing page and in
   the footer, so they live here rather than in landing.css - the billing page
   does not load that one. */

.card-marks { display: flex; flex-direction: column; gap: 8px; }

.card-mark-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Drawn at their own 3:2, and never stretched: a scheme mark with the wrong
   proportions is a scheme mark nobody is licensed to use. */
.card-mark-list img { display: block; width: 42px; height: 28px; }

.card-marks-note { margin: 0; font-size: 0.78rem; color: var(--ink-faint); }

/* A statement the page is making rather than a paragraph in it: the currency
   debited, the refund route. Given a ground and a rule so it is found by
   somebody scanning for it, which is how these are read. */
.legal-callout {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0 0 14px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--radius-sm);
  background: var(--brand-tint);
  font-size: 0.9rem;
}
.legal-callout .icon { flex: none; margin-top: 0.15em; color: var(--brand); }

/* The marks on one side, the policy links on the other, stacking when there is
   no room for both. */
.pay-terms-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px 24px;
  margin-top: 14px;
}

.pay-terms-links {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.86rem;
}
.pay-terms-links li { display: inline-flex; align-items: center; gap: 7px; }
.pay-terms-links .icon { color: var(--ink-faint); flex: none; }

/* --- Label-and-value list ---------------------------------------------------
   A column of facts about one thing: the invoice summary, and what the billing
   page says about the live subscription. Ruled between rows rather than merely
   spaced, so the eye can carry a label across to its value. */
.detail-meta { display: grid; }
.detail-meta .row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 0.9rem;
}
.detail-meta .row:last-child { border-bottom: none; }
.detail-meta .row > span:first-child {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-muted);
  white-space: nowrap;
}
.detail-meta .row > span:first-child .icon { opacity: 0.5; }
.detail-meta .row > span:last-child { font-weight: 550; text-align: right; }

/* --- Responsive ------------------------------------------------------------ */

@media (max-width: 900px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The aside goes under the thing it describes. It is a third of the width, so
     it runs out of room well before an even split would. */
  .grid-main-aside { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 720px) {
  :root { --sticky-top: 0px; }
  .shell { flex-direction: column; }
  /* Kept for the cascade's sake even though `display: contents` below drops
     the box: a browser that does not support it still gets a full-width,
     static sidebar rather than a 232px column jammed against the content. */
  .sidebar {
    width: 100%;
    flex: none;
    height: auto;
    position: static;
    border-right: none;
    border-bottom: 1px solid var(--line);
  }
  /* Stacked, so there is no seam to line up with: the box goes back to being
     as tall as its contents need. */
  .sidebar-brand { height: auto; padding: 12px 16px 10px; }
  .sidebar-brand .brand-logo { max-width: 150px; margin: 0 auto; }

  /* Icons only. With their labels the eight entries took three wrapped rows -
     a third of a phone screen spent on navigation before the page starts, on
     every page. The glyphs fit on one row, the `title` names each for a long
     press, and the label stays in the markup for a screen reader.

     Centred as a row rather than stretched: eight equal cells would put the
     first glyph hard against the left edge and the last against the right,
     which reads as a toolbar spanning the window rather than as a group. */
  .sidebar-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
    padding: 8px 10px;
  }
  .nav-section { display: none; }
  .nav-link {
    margin: 0;
    width: 42px;
    height: 42px;
    padding: 0;
    justify-content: center;
    position: relative;
  }
  /* The glyph becomes the button. With the label gone there is nothing for a
     pill to wrap, so the hover and active fills go on a 34px disc around the
     glyph and the link is just the 42px target around that. */
  .nav-link .nav-ico { width: 34px; height: 34px; border-radius: var(--radius-sm); }
  .nav-link:hover,
  .nav-link.is-active { background: transparent; }
  .nav-link:hover .nav-ico { background: color-mix(in srgb, var(--ink) 5%, var(--surface)); }
  .nav-link.is-active .nav-ico { background: var(--brand-light); color: var(--brand); }
  /* Already a row of icons; there is nothing left to fold. */
  .sidebar-toggle { display: none; }
  .nav-link .nav-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  .nav-link .icon { flex: none; width: 1.15em; height: 1.15em; }

  /* The padlock rides the corner of the glyph rather than sitting after a label
     that is no longer there - left in flow it doubled the button's width. */
  .nav-link .nav-lock {
    position: absolute;
    right: 3px;
    bottom: 3px;
    margin: 0;
  }
  .nav-link .nav-lock .icon { width: 10px; height: 10px; }
  .nav-link .nav-flag { display: none; }

  /* The bar is not sticky on a phone. Stacked under the navigation it is no
     longer a strip at the top of the window but a band a third of the way down
     it, and pinning that costs a sixth of a short screen on every page. The
     account menu is a scroll away rather than always on screen, which is the
     trade the width forces. */
  .topbar {
    position: static;
    height: auto;
    padding: 10px 16px;
    gap: 10px;
    flex-wrap: wrap;
  }
  /* The page heading already says where you are - the bar's copy of it is what
     gets dropped when the row has to choose. Below 600px it is dropped
     outright; see the block after this one. */
  .topbar-title { flex: 1 1 auto; min-width: 0; }
  .topbar-actions { flex: none; margin-left: auto; }

  /* Stacked, the bar across the top has to come *first*: the theme, the
     language and the account are what somebody reaches for on a phone, and
     burying them under a brand block and a row of navigation icons puts three
     controls behind a scroll on every page.

     `display: contents` on `.main` is what makes that possible without moving
     anything in the markup. The topbar and the page content are its children,
     so ordering them around the sidebar means they have to be siblings of it -
     dropping `.main`'s own box promotes them into `.shell`. It carries no role
     and no ARIA, so nothing is lost with it; doing this to an element that did
     would take its semantics out of the tree.

     Order, not `row-reverse` or a second markup path: the source order is what
     a screen reader and keyboard tabbing follow, and those want the navigation
     before the content it leads to, which is what it already is. */
  .main { display: contents; }
  .topbar { order: 1; }
  /* The sidebar moves as one piece - brand above its own navigation, the way
     it is at every other width - so the `<aside>` keeps its box and with it
     its landmark. An earlier pass split the two around the topbar with a
     second `display: contents`; that put the logo at the very top but cost
     the landmark, and it looked worse. */
  .sidebar { order: 2; }
  .content { order: 3; }

  /* minmax(0, 1fr), not 1fr. A bare `1fr` is `minmax(auto, 1fr)`, and that auto
     minimum is the track's min-content width - so a card holding a table with
     fixed column widths could not shrink, `overflow-x: auto` on .table-wrap never
     engaged, and the whole page scrolled sideways instead of the table. At 380px
     the dashboard overflowed by 223px. */
  .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0, 1fr); }
  .field-row { grid-template-columns: minmax(0, 1fr); }
  .field-span-2, .field-span-3, .field-span-4 { grid-column: 1 / -1; }
  .content { padding: 16px; }

  /* A header whose actions have wrapped onto their own line should use that
     line rather than huddling at its right-hand end - and the buttons share it
     evenly instead of each taking the width of its own label. Two buttons of
     different widths sitting left on a phone read as one important control and
     one afterthought; at equal widths they read as the pair they are.

     `flex-basis: 0` is what makes them equal rather than merely stretched: with
     the default `auto` basis the free space is shared *on top of* each label's
     own width, so a long label still wins. `min-width` keeps a three-button row
     from squeezing labels to nothing before the icon-only rule below takes over. */
  .page-actions {
    justify-content: flex-start;
    align-items: stretch;
  }
  .page-actions > .btn,
  .page-actions > .split { flex: 1 1 0; min-width: 140px; }
  /* The split button divides its own row: the face takes the slack, the caret
     stays the width of a caret. */
  .page-actions > .split > .btn:first-child { flex: 1 1 auto; }
}

/* Below 600px the bar cannot carry the page's name and its controls on one
   line, and it is the name that goes. The page's own `<h1>` is directly
   underneath and says the same thing in larger type, so the bar's copy was
   only ever a convenience for a wide window - at this width it pushes the
   theme, language and account controls onto a second row to repeat something
   already on screen. */
@media (max-width: 600px) {
  .topbar-title { display: none; }
  /* Nothing left to sit beside, so the controls take the row and the
     `margin-left: auto` that pushed them off the title has nothing to do. */
  .topbar { justify-content: flex-end; }
  /* Allowed to shrink, and to wrap once it has. The row needs 430px - search,
     two pills, the account - and a phone gives it 288 to 382; held at its own
     width with `flex: none` it ran off the *left* edge, because the bar is
     packed from the right, and overflow on that side cannot be scrolled to.
     The search button was gone entirely and the theme pill cut in half.
     The two rules below make it fit one row on most phones; this is what
     keeps a control on screen when they do not. */
  .topbar-actions {
    margin-left: 0;
    flex: 0 1 auto;
    min-width: 0;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px 6px;
  }
}

/* On a phone the account button is the initial and the caret. The plan's
   name is the widest thing in the bar, and the panel it opens names it again;
   it stays in the markup, so the button still says it to a screen reader. */
@media (max-width: 560px) {
  .account-plan {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
}

/* --- On a phone --------------------------------------------------------------
   The rows of controls that have more in them than fits, and the targets that
   are sized for a pointer rather than a thumb. */
@media (max-width: 560px) {
  /* A crowded action row drops its words and keeps its icons - but only the
     secondary ones. The primary action is what the page is for, and an
     unlabelled plus on the one button somebody came to press is a guess; the
     supporting actions beside it are recognisable from their glyph and their
     tooltip. So the row reads as "one named thing, and some tools".

     Only crowded rows: a header holding a single "New client" has room for its
     label. Three or more controls is what stops fitting at this width, which is
     why this hangs off :has() rather than being blanket. The label stays in the
     markup either way, so the button is still named for a screen reader, and
     `title` names it for a long press. */
  .page-actions:has(> :nth-child(3)) .btn:not(.btn-primary) .btn-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  /* Sized to their glyph rather than sharing the row equally: an icon in a
     140px button is a target floating in a field of nothing. The space they
     give up goes to the primary, which is the one with something to say. */
  .page-actions:has(> :nth-child(3)) > .btn:not(.btn-primary) {
    flex: 0 0 auto;
    min-width: 0;
    padding-left: 12px;
    padding-right: 12px;
  }

  /* A tap target is a finger, not a mouse pointer. The row actions came out at
     30px squares and the small buttons at 30px tall - both fine under a cursor
     and a guess under a thumb. */
  .btn-icon { min-width: 38px; min-height: 38px; }
  .btn-sm { min-height: 38px; }
  /* Except inside the pager, where they are round and sit in a row of four; at
     38px that row no longer fits beside the range. */
  .pager-links .btn-icon { min-width: 32px; min-height: 32px; width: 32px; height: 32px; }

  /* The pager's three groups stack rather than squeezing onto one line, and
     each takes the full width so they read as a column rather than as three
     things that failed to fit. */
  .pager { justify-content: center; text-align: center; }
  .pager-size { width: 100%; justify-content: center; }
  .pager-range { width: 100%; }
  .pager-links { width: 100%; justify-content: center; }

  /* A form's buttons go full width. Two half-width buttons side by side on a
     390px screen are two small targets for what is usually one decision and its
     way out. */
  .form-actions,
  .card-foot.flex-between { flex-wrap: wrap; }
  .form-actions .btn { flex: 1 1 100%; }

  .filters .field { flex: 1 1 100%; }
  .filters .field-actions { width: 100%; }
  .filters .field-actions .btn { flex: 1; }

  /* --- Columns a phone does not get -----------------------------------------
     An eight-column table on a 390px screen showed two of them and hid the rest
     behind a sideways scroll with nothing to say it was there - so the list
     appeared to have no amounts, no status and no actions at all.

     Dropping columns is better than scrolling to them. Which ones is a decision
     per table rather than per column *type*, which is why this keys on a class
     the template applies deliberately: the due date is noise on the invoice
     list and the entire point of the dashboard's "To collect", and a rule that
     hid every `.col-date` got the second one wrong.

     The rule of thumb for marking a column optional: it can be recovered by
     opening the document, and it is not the question the list is being asked.
     The full table is a rotation or a wider screen away, and both are one
     gesture. */
  table.data th.col-optional,
  table.data td.col-optional { display: none; }

  /* The client moves under the number instead of losing its own column outright
     - the pair is one answer to "which document", and separating them across
     the width was only ever a desktop convenience.

     Qualified with `table.data` deliberately: the base rule that hides this is
     written further down the file, next to the column it belongs with, so a
     bare class here would lose the tie on source order. */
  table.data .row-client { display: block; }

  /* The status badge says the status. The qualifier beside it - "not yet
     issued" - is a second sentence in a column that has room for a word, and it
     was pushing the amount off the screen. */
  table.data .status-cell .tiny { display: none; }

  table.data th, table.data td { padding-left: 8px; padding-right: 8px; }

  /* A hair smaller, and only here: sixteen monospaced characters is the widest
     thing in the row, and it is the column everything else is squeezed by. */
  table.data .doc-number { font-size: 0.8em; }
  table.data .badge { font-size: 0.68rem; padding-left: 7px; padding-right: 7px; }

  /* Headers wrap here. `table.data th` sets nowrap, which turns each heading
     into a floor for its column - "AMOUNT DUE" alone reserved 105px for a
     column whose values are 55px wide. Two lines in the header row costs that
     height once; nowrap costs the width on every row. */
  table.data th { white-space: normal; }
}

/* Locked invoices show a lock in place of the edit/delete actions. */
.btn-icon.is-locked-hint { color: var(--ink-faint); cursor: default; }
.btn-icon.is-locked-hint:hover { background: transparent; color: var(--ink-faint); }

/* --- Modal ----------------------------------------------------------------- */

/* Native <dialog>: the browser supplies focus trapping, Esc and the backdrop. */
.modal {
  border: none;
  border-radius: var(--radius);
  padding: 0;
  max-width: 440px;
  width: calc(100% - 32px);
  box-shadow: var(--shadow-lg);
  color: var(--ink);
  background: var(--surface);
}

.modal::backdrop { background: rgba(22, 32, 44, 0.45); }

.modal-card { padding: 0; }

.modal-head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 18px 20px 0;
}

.modal-head h3 { margin: 0; font-size: 1.05rem; }

.modal-icon {
  width: 34px;
  height: 34px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--brand-light);
  color: var(--brand);
}

.modal.is-danger .modal-icon { background: var(--danger-bg); color: var(--danger); }

.modal-body {
  padding: 12px 20px 20px;
  color: var(--ink-muted);
  font-size: 0.93rem;
  line-height: 1.5;
}

.modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 20px;
  border-top: 1px solid var(--line);
  background: var(--brand-tint);
  border-radius: 0 0 var(--radius) var(--radius);
}

@media (max-width: 480px) {
  .modal-foot { flex-direction: column-reverse; }
  .modal-foot .btn { width: 100%; }
}

/* --- Toasts ---------------------------------------------------------------- */

.toasts {
  position: fixed;
  top: 72px;
  right: 20px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 9px;
  width: 360px;
  max-width: calc(100vw - 32px);
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 13px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  font-size: 0.9rem;
  pointer-events: auto;
  animation: toast-in 0.18s ease-out;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

.toast.is-leaving { opacity: 0; transform: translateY(-6px); transition: opacity 0.18s, transform 0.18s; }

.toast-icon { flex: none; display: flex; align-items: center; }
.toast-text { flex: 1; min-width: 0; }

.toast-close {
  flex: none;
  border: none;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 2px;
  line-height: 1;
  border-radius: var(--radius-sm);
}
.toast-close:hover { color: var(--ink); background: var(--canvas); }

.toast-success { border-left: 3px solid var(--ok); }
.toast-success .toast-icon { color: var(--ok); }
.toast-error, .toast-danger { border-left: 3px solid var(--danger); }
.toast-error .toast-icon, .toast-danger .toast-icon { color: var(--danger); }
.toast-warning { border-left: 3px solid var(--warn); }
.toast-warning .toast-icon { color: var(--warn); }
.toast-info, .toast-debug { border-left: 3px solid var(--brand); }
.toast-info .toast-icon, .toast-debug .toast-icon { color: var(--brand); }

@media (max-width: 720px) {
  .toasts { top: 12px; right: 12px; left: 12px; width: auto; }
}

/* Marks a document that is not an ordinary invoice, and one that has been
   credited. Both change what the figures mean, so they sit beside the number
   rather than being left to the reader to work out from the totals. */
.badge-type {
  background: var(--brand-tint);
  color: var(--brand-dark);
  border: 1px solid color-mix(in srgb, var(--brand) var(--hairline-mix), var(--surface));
}

.badge-credited {
  background: var(--credit-bg);
  color: var(--credit);
  border: 1px solid var(--credit-line);
}

/* Searchable select (see country-select.js). Styled to be indistinguishable
   from an ordinary field, because as far as the user is concerned it is one. */
.combo { position: relative; }

.combo-input { width: 100%; }

.combo-list {
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 260px;
  overflow-y: auto;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}

.combo-item {
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  cursor: pointer;
}

.combo-item:hover,
.combo-item.is-active { background: var(--brand-tint); }

.combo-item.is-selected { font-weight: 620; }

.combo-list::-webkit-scrollbar { width: 10px; }
.combo-list::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 6px;
  border: 3px solid var(--surface);
}

/* A feature the current plan does not include. Still clickable — the click is
   what takes the user to the plan page — but visibly not yet theirs. */
.is-locked { opacity: 0.62; }
.is-locked:hover { opacity: 0.85; }
.is-locked .icon:last-child { opacity: 0.75; }

.nav-lock {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
}

.nav-lock .icon { width: 13px; height: 13px; }

/* The flag on a link that only applies in one country - Books, the Bosnian
   VAT books. At the end of the row like the padlock, which follows it when
   the link is locked too. Gone wherever the label goes: an icon with a flag
   and a padlock on it is three signs on one square. */
.nav-link .nav-flag { margin-left: auto; width: 16px; height: 11px; flex-basis: 16px; }
.nav-link .nav-flag + .nav-lock { margin-left: 8px; }

/* A group of form fields the plan does not include. The inputs are genuinely
   disabled by the form; this only makes that legible. */
.is-locked-fields .field label,
.is-locked-fields .field-help { opacity: 0.6; }

.is-locked-fields input:disabled,
.is-locked-fields select:disabled {
  background: var(--canvas);
  color: var(--ink-faint);
  cursor: not-allowed;
}

.lock-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 10px;
  padding: 2px 9px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-faint);
}

.lock-badge:hover { color: var(--brand); border-color: var(--brand); text-decoration: none; }
.lock-badge .icon { width: 12px; height: 12px; }

/* --- Cookie disclosure -------------------------------------------------- */
/* A bar, not a modal. Nothing non-essential is set before a choice is made, so
   there is nothing to block the page for — and a full-screen wall over strictly
   necessary cookies is the pattern regulators object to. */
/* Sticky, not fixed. Fixed, the bar lay over the last 200px of every page
   on a phone until it was answered, and at the end of a page there was
   nowhere to scroll to: a client's accept button on a proforma sat under
   it. Sticky, it follows the bottom of the screen the same way while there
   is page left below, and at the end takes its own room after the content.
   It is the last thing in <body>, so its place in the flow is the end. */
.cookie-notice {
  position: sticky;
  bottom: 0;
  z-index: 60;
  background: var(--surface);
  border-top: 1px solid var(--line-strong);
  box-shadow: 0 -6px 28px rgba(16, 24, 34, 0.09);
}

.cookie-inner {
  max-width: 1120px;
  margin: 0 auto;
  padding: 14px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 22px;
}

.cookie-text strong { display: block; font-size: 0.92rem; margin-bottom: 2px; }
.cookie-text p { margin: 0; font-size: 0.84rem; color: var(--ink-muted); max-width: 68ch; }

.cookie-actions { display: flex; align-items: center; gap: 10px; flex: none; }
.cookie-actions form { display: flex; gap: 8px; }

/* --- Privacy hover card ------------------------------------------------- */
.privacy-hover { position: relative; }

.privacy-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font: inherit;
  font-size: 0.84rem;
  color: var(--ink-muted);
  cursor: pointer;
}

.privacy-trigger:hover { color: var(--brand); border-color: var(--brand); }
.privacy-trigger .icon { width: 13px; height: 13px; }

/* Hung from the trigger's right edge, so it opens leftwards: above the
   phone breakpoint the trigger sits at the right of the bar, and hung from
   its left edge the card ran 13px past the screen on a desktop - cut off when
   open, and, once the bar stopped being `fixed`, a sideways scroll on every
   page. Below the breakpoint the card spans the bar instead; see there. */
.privacy-card {
  position: absolute;
  bottom: calc(100% + 10px);
  right: 0;
  width: min(420px, calc(100vw - 40px));
  max-height: min(60vh, 520px);
  overflow-y: auto;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s;
}

/* Hover for a mouse, focus-within for a keyboard or a tap. Hover alone would
   leave the card unreachable for both. */
.privacy-hover:hover .privacy-card,
.privacy-hover:focus-within .privacy-card {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.privacy-card h3 { font-size: 0.95rem; margin: 0 0 12px; }
.privacy-card dl { margin: 0; }
.privacy-card dt {
  font-size: 0.78rem;
  font-weight: 680;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  margin-top: 12px;
}
.privacy-card dt:first-of-type { margin-top: 0; }
.privacy-card dd { margin: 3px 0 0; font-size: 0.85rem; line-height: 1.5; }

.privacy-foot {
  margin: 14px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 0.78rem;
  color: var(--ink-faint);
}

@media (max-width: 760px) {
  .cookie-inner { flex-direction: column; align-items: flex-start; gap: 12px; }
  .cookie-actions { width: 100%; flex-wrap: wrap; }

  /* The card spans the bar rather than hanging off its trigger.

     It used to be `right: 0`, which aligns its right edge with the *trigger's*
     right edge - and the trigger is a small button about 170px from the left,
     so a 350px card ran 182px off the left of a 390px screen. Half the privacy
     notice was unreadable, which is a poor thing for a privacy notice to be.

     Capping the width was never going to fix that: the width was already
     correct, it was the anchor that was wrong. So on a narrow screen the card
     stops being positioned against the trigger at all. `.privacy-hover` gives
     up `position: relative`, which hands the containing block to
     `.cookie-notice` - itself positioned (sticky), and full width - so left and
     right gutters here are gutters against the viewport, and `bottom: 100%`
     puts the card above the whole bar however many rows it has wrapped to. */
  .privacy-hover { position: static; }
  .privacy-card {
    left: 12px;
    right: 12px;
    width: auto;
    bottom: calc(100% + 10px);
    /* Shorter here, because the bar underneath is taller once it stacks. The
       card scrolls inside itself, so this trims what is visible rather than
       what is readable. */
    max-height: 50vh;
  }
}

/* The labels under an invoice number - its type, and whether it is void.
   Their own row rather than trailing the number: one badge fitted beside it,
   two widened the column enough to put a horizontal scrollbar under the table.
   They wrap between badges but never inside one. */
.number-badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  margin-top: 4px;
}

.number-badges .badge { flex: none; }

/* A POST action sitting among the icon links in a table row. A bare <form> is
   a block element and would push the icons after it onto a new line. */
.row-action-form { display: inline-flex; margin: 0; }

/* A document declared void: kept on the record, out of every figure. Struck
   through rather than merely greyed, because "this does not count" has to be
   unmistakable next to figures that do. */
.badge-void {
  background: var(--neutral-bg);
  color: var(--ink-muted);
  border: 1px solid var(--neutral-line);
  text-decoration: line-through;
}

/* A two-letter type code beside an invoice number. Monospaced digits and
   tighter padding than a word badge, so it reads as a marker rather than a
   label and never widens the column. */
.badge-code {
  padding: 2px 6px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: help;
}

.number-cell {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

/* Status badge and its qualifier on one line. `nowrap` on the wrapper rather
   than the parts, so the pair is one unbreakable unit and the column sizes to
   it instead of stacking them. */
.status-cell {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

.status-cell .badge { flex: none; }

/* The client's name folded under the document number. Hidden wherever the
   client has a column of its own, which is everywhere but a phone - see the
   narrow-screen block at the end of this file. */
.row-client {
  display: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 150px;
}

/* Client name in a table: one line, elided rather than wrapped. A wrapped name
   costs a line on every row, and because column widths shift with the language
   it wrapped in one and not the other - the same table two heights. */
.col-client {
  width: 160px;
  max-width: 160px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Pinned columns in the invoice list.
 *
 * `table.data th` sets nowrap, which turns a width into a *minimum* - a long
 * header simply forces the column wider. "DATUM IZDAVANJA" is half again the
 * width of "ISSUE DATE", and the difference came out of the client column,
 * wrapping the name and making the same table taller in one language than the
 * other. Letting these headers wrap costs height once, in the header row. */
/* `table.data th` is specificity 0-1-2 and sets nowrap, so a bare class here
   loses to it. Qualified selectors, or the width stays a minimum. */
table.data th.col-date {
  width: 104px;
  white-space: normal;
}

table.data th.col-status {
  width: 158px;
  white-space: normal;
}

table.data th.col-client {
  width: 160px;
  white-space: normal;
}

/* A stat note carrying something that needs attention rather than describing the
   figure above it. */
.stat-note.is-warning { color: var(--danger); font-weight: 600; }

/* Inline danger colour for a figure that points the wrong way. */
.is-danger { color: var(--danger); }

/* A figure that points the other way - a refund owed, sitting beside an amount
   receivable. Its own colour rather than the danger red, so that it is not
   reporting the same thing as the overdue amount two rows above it. */
.is-owing { color: var(--alt); }

/* More money arrived than was asked for. Warning rather than error colouring:
   nothing is broken, but somebody has to send some back. */
.badge-overpaid {
  background: var(--credit-bg);
  color: var(--credit);
  border: 1px solid var(--credit-line);
}

/* Money that still has to go back to the client. Deliberately not the same
   colour as a credit badge: one describes a charge that was cancelled, the other
   an action still outstanding. Nor the danger red it used to carry - a row owed
   a refund is tinted violet, and a red badge on it read as a second, contrary
   diagnosis of the same invoice. See --alt. */
.badge-refund {
  background: var(--alt-bg);
  color: var(--alt);
  border: 1px solid color-mix(in srgb, var(--alt) 22%, transparent);
}

/* Dashboard stat tiles: a fixed column count per width, never auto-fit, so
   every row is full. Six tiles go 1, 2, 3 across; four - an account without
   the expenses and profit pair, or the page narrowed to one client - go 1, 2,
   4. auto-fit put five of six in a row and stranded the sixth under them.
   The widths are the window's, so they allow for the sidebar beside the
   tiles: 1100px leaves about 820px for three, 1280px about 1000px for four. */
.grid-stats { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 600px) {
  .grid-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1100px) {
  .grid-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid-stats.is-four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1280px) {
  .grid-stats.is-four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Stacked by default, side by side once there is room. Deliberately not auto-fit:
   the divider between compartments has to be a left border in one arrangement and
   a top border in the other, and CSS cannot ask a grid how many columns it ended
   up with. Fixing the fold at one breakpoint means the rule is always the right
   rule - auto-fit gave two columns at 600px with the divider suppressed, which is
   the ambiguity this is here to remove. */
.stat-split {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}

/* A rule between compartments, not just space. Three groups of pills across one
   card read as a single strip of nine: the gap between neighbouring columns was no
   larger than the gap between pills within one, so nothing said where a group
   ended. */
.stat-split > div + div {
  border-top: 1px solid var(--line);
  margin-top: 14px;
  padding-top: 14px;
}

@media (min-width: 860px) {
  .stat-split { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .stat-split > div { padding-right: 26px; }
  .stat-split > div + div {
    border-top: 0;
    margin-top: 0;
    padding-top: 0;
    border-left: 1px solid var(--line);
    padding-left: 26px;
  }
}

/* A figure's month-on-month trend: a small pill beside it, set in rem so the
   1.5rem figure it sits in does not scale it up. Up is the only direction in
   a colour - more money in is the news; less is information, not an alarm. */
.trend {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 1px 7px 1px 5px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0;
  background: var(--neutral-bg);
  color: var(--ink-muted);
}
/* A figure and its trend as one wrapping row. Five tiles across leave about
   190px a tile, and a long figure plus its pill does not always fit: wrapped,
   the pill goes flush under its own figure rather than hanging indented off
   the end of an empty line. */
.stat-value.has-trend { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; }
.trend .icon { width: 11px; height: 11px; }
.trend.is-up { background: var(--ok-bg); color: var(--ok); }
/* A cost's trend: rising spending is not the good news rising income is, so
   it keeps the arrow and loses the colour. */
.trend.is-cost.is-up { background: var(--neutral-bg); color: var(--ink-muted); }

/* A figure with its sparkline beside it. */
.stat-figure-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sparkline { flex: none; width: 84px; height: 24px; overflow: visible; }
.sparkline polyline {
  fill: none;
  stroke: var(--brand);
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.sparkline circle { fill: var(--brand); }

/* The limit is context for the number, not a second number. */
.stat-split .of-limit { font-size: 0.6em; font-weight: 500; color: var(--ink-faint); }
.stat-split .meter { margin-top: 7px; }

/* The per-type tally. The count sits *inside* the badge rather than after it: as
   a bare number alongside a pill it read as a separate figure and left the eye to
   pair the two up. One object, two compartments - code tinted, count on the
   surface. */
/* Sized to their content and held close under their own number. Stretched to fill
   the column they were the width of the compartment, which put the last pill of
   one group hard against the first pill of the next - and a pill mostly empty down
   the middle reads as a progress bar rather than a label. Proximity is what says
   which tally belongs to which heading. */
.tally { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }

.tally-badge {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.5;
  cursor: help;
}

/* Two compartments in one pill: the code tinted, the count on the surface. The
   count keeps a floor width so 1 and 12 do not change the pill's shape, and the
   three pills in a row stay the same size as each other. */
.tally-badge .code,
.tally-badge .count { padding: 1px 7px; text-align: center; }
.tally-badge .count { min-width: 26px; }

.tally-badge .code { background: var(--brand-light); color: var(--brand); }
.tally-badge .count {
  background: var(--surface);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Zeros stay, because the row is read as a comparison across the three types and
   dropping the empty ones makes the same account look structurally different from
   one month to the next. They recede rather than disappear. */
.tally-badge.is-zero { opacity: 0.5; }

/* --- Document numbers ------------------------------------------------------ */
/* A document number is an identifier, not prose. Set in the system monospace
   with tabular figures so INV-2026-08-0001 and CRN-2026-08-0001 occupy the same
   width and a column of them lines up digit under digit — the proportional face
   made the prefixes different widths and the numbers ragged.
   System stack only: a webfont would need a network request, which the artifact
   CSP forbids and which would flash unstyled text anyway. */
.doc-number {
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono",
               "Roboto Mono", Menlo, Consolas, "Liberation Mono", monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.94em;
  letter-spacing: -0.01em;
  /* One line, always. A number is a single token to read, and the hyphens in
     INV-2026-08-0013 are break opportunities as far as the browser is
     concerned - in a squeezed column on a phone it came apart over four lines,
     which turned every row of the dashboard's list into a paragraph. The table
     scrolls sideways instead, which is what .table-wrap is for. */
  white-space: nowrap;
}

/* In a heading the mono face reads large; pull it back to match the sans. */
h1 .doc-number, .doc-number-lg { font-size: 0.88em; letter-spacing: -0.02em; }

/* ---------------------------------------------------------------------------
   Charts

   The categorical hues are fixed in this order and never cycled: slot 1 is
   always the first series, so narrowing the dashboard to one client cannot
   repaint the series that remain. They are checked rather than chosen by eye,
   by simulating each of the three dichromacies and measuring CIEDE2000 between
   every pair. The set now clears dE 15.1 in the worst case under any of them,
   against a threshold of 8, and every hue clears 3:1 against the card - which a
   2px line needs and a filled bar does not.

   Slot 3 was #12958c, a teal, and slot 2 was a lighter orange. Two things were
   wrong with that set and both are fixed here.

   The blue and the teal collapsed under *tritanopia* - dE 4.0, which is to say
   indistinguishable - because they were separated by hue alone and tritanopia is
   the one that takes the blue-yellow axis. The original check only covered
   protanopia, where they are 24.6 apart, so it never showed. Darkening the teal
   separates them by lightness as well, which no colour deficiency can take away.

   Slot 4 is the danger red, the same one an overdue row and the Overdue tile are
   drawn in. A fourth hue had to be found anyway; taking it from the state
   palette rather than inventing one means "overdue" is one colour across the
   product. Red against the orange is the pair to worry about, and it measures
   16.2 at its worst - the deeper orange is what buys that.
--------------------------------------------------------------------------- */
.chart {
  --viz-cat-1: #2a78d6;
  --viz-cat-2: #e0621f;
  --viz-cat-3: #0e5f59;
  --viz-cat-4: var(--danger);
  /* `--ink-muted`, not `--muted`: the latter is not a token this design
     system has ever declared, so this resolved to nothing and the axis
     labels fell through to the hardcoded grey in charts.js. */
  --viz-ink: var(--ink-muted);
  --viz-grid: var(--line-soft);
  --viz-surface: var(--surface);
  position: relative;
}

.chart-plot { position: relative; min-height: 220px; }
/* See the counts chart in core/dashboard.html: three small integers do not need
   the height a four-line money trend does. */
.chart-compact .chart-plot { min-height: 132px; }
.chart-plot svg { display: block; overflow: visible; }
.viz-hit { cursor: default; }

/* The legend carries a gloss under each name, so it is a two-line block per
   series rather than a row of words. `auto-fit` rather than a flex row: four
   glossed items wrap raggedly on a flex line, and the columns keep the swatches
   on one left edge where they can be scanned down. */
.chart-legend {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px 18px;
  margin: 0 0 14px; padding: 0; list-style: none;
  font-size: 0.78rem; color: var(--ink-muted);
}
/* `start`, not `center`: the swatch belongs to the name on the first line, and
   centred it drifts down beside the gloss on a wrapped item. */
.chart-legend li { display: flex; align-items: start; gap: 7px; }
.legend-text { display: flex; flex-direction: column; min-width: 0; }
.legend-label { font-weight: 600; color: var(--ink); }
.legend-hint { color: var(--ink-muted); line-height: 1.35; }
/* Nudged onto the cap height of the label above it. */
.viz-swatch { margin-top: 3px; }

/* Identity is the swatch beside the text, never the text itself. */
.viz-swatch {
  width: 10px; height: 10px; border-radius: 3px;
  flex: 0 0 auto; display: inline-block;
}
.viz-swatch-1 { background: var(--viz-cat-1); }
.viz-swatch-2 { background: var(--viz-cat-2); }
.viz-swatch-3 { background: var(--viz-cat-3); }
.viz-swatch-4 { background: var(--viz-cat-4); }

/* :not([hidden]) rather than a bare .chart-tip: a display declaration outranks
   the `hidden` attribute's own display:none, which left the tooltip parked over
   the legend on load. */
.chart-tip:not([hidden]) {
  display: grid;
}

.chart-tip {
  position: absolute; top: 6px; z-index: 3;
  grid-template-columns: auto auto auto; gap: 2px 8px;
  align-items: center; pointer-events: none;
  padding: 8px 10px; border-radius: 8px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 6px 18px rgba(24, 24, 32, 0.12);
  font-size: 0.76rem; color: var(--text); white-space: nowrap;
}
.chart-tip strong { grid-column: 1 / -1; font-size: 0.72rem; color: var(--ink-muted); }
.chart-tip b { justify-self: end; font-variant-numeric: tabular-nums; }

.chart-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-top: 8px;
}
.chart-table { margin-top: 10px; }
.chart-table table { font-size: 0.76rem; }

/* The client filter sits in one row above everything it scopes, so its reach
   is unambiguous - a filter beside a single tile reads as filtering that tile. */
.dash-filter { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Sized to its content, not to the card: a select stretched across the full
   width reads as the page's subject rather than as a control over it, and it
   pushed the scope note onto a line of its own. */
.dash-filter select { width: auto; min-width: 220px; max-width: 320px; }
.dash-filter .scope-note { font-size: 0.78rem; color: var(--ink-muted); }

/* ---------------------------------------------------------------------------
   Date picker

   Attached to the day-first text date fields by core/js/datepicker.js. One panel
   is moved between fields, positioned in page coordinates, so it is a child of
   <body> rather than of the form - inside a form group it would have been clipped
   by the card's overflow.
--------------------------------------------------------------------------- */
.datepicker {
  position: absolute;
  z-index: 60;
  width: 252px;
  padding: 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  font-size: 0.85rem;
}

.datepicker-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 6px; margin-bottom: 8px;
}
.datepicker-month { font-weight: 640; text-transform: capitalize; }

.datepicker-nav {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  background: none; border: 1px solid var(--line); border-radius: var(--radius-sm);
  color: var(--ink-muted); cursor: pointer;
}
.datepicker-nav:hover { background: var(--brand-tint); color: var(--brand); }

.datepicker-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  text-align: center;
}

.datepicker-weekday {
  font-size: 0.66rem; font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-faint); padding-bottom: 4px;
}

.datepicker-day {
  padding: 5px 0;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--ink); font: inherit; font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.datepicker-day:hover { background: var(--brand-tint); }
.datepicker-day.is-empty { cursor: default; }

/* Today is outlined, the selected day is filled: they can be the same date and
   still have to be told apart. */
.datepicker-day.is-today { border-color: var(--line-strong); font-weight: 650; }
.datepicker-day.is-selected {
  background: var(--brand); border-color: var(--brand); color: var(--on-brand); font-weight: 650;
}

.datepicker-foot {
  display: flex; justify-content: space-between; gap: 6px;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line);
}

/* The calendar affordance. The native input drew one and a text input does not,
   so without it the field gave no sign it had a picker at all.

   A background image rather than markup: every date field stays a bare <input>,
   so no template that renders a form has to know about this. The glyph is not a
   separate control - the whole field opens the calendar on focus - so it is not
   in the tab order and needs no label. */
input[data-datepicker] {
  font-variant-numeric: tabular-nums;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%20fill='%238a99a8'%3E%3Cpath%20d='M11%206.5a.5.5%200%200%201%20.5-.5h1a.5.5%200%200%201%20.5.5v1a.5.5%200%200%201-.5.5h-1a.5.5%200%200%201-.5-.5z'/%3E%3Cpath%20d='M3.5%200a.5.5%200%200%201%20.5.5V1h8V.5a.5.5%200%200%201%201%200V1h1a2%202%200%200%201%202%202v11a2%202%200%200%201-2%202H2a2%202%200%200%201-2-2V3a2%202%200%200%201%202-2h1V.5a.5.5%200%200%201%20.5-.5M1%204v10a1%201%200%200%200%201%201h12a1%201%200%200%200%201-1V4z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px 15px;
  cursor: pointer;
}

/* Bright while the calendar is open, so the field and the panel read as one. */
input[data-datepicker]:focus { cursor: text; }

/* A tile carrying more than one currency: the figures are peers, so same size,
   just tighter than the single-figure case was. */
.stat-value + .stat-value { margin-top: 1px; }

/* The money chart's currency picker, in its card head beside the title. */
.chart-scope { display: flex; align-items: center; gap: 6px; }
.chart-scope select { width: auto; padding: 4px 8px; font-size: 0.82rem; }

/* --- The sign-in and sign-up shell ------------------------------------------
   One centred column on the canvas. `min-height: 100dvh` rather than `100vh`:
   on a phone the browser's own bars are counted into `vh`, so a card centred in
   it sits low enough that the footer link falls under the address bar. */
.auth-shell {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
  background: var(--canvas);
}

.auth-column { width: 100%; }

/* The mark and the preferences, with the switchers under the logo rather than
   beside it - the column is 420px at its widest and the logo does not share a
   line with them comfortably in any language. The two switchers share one row
   with each other, as they do in the top bar. */
.auth-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin-bottom: 22px;
}
.auth-brand .brand-logo { margin: 0 auto; max-width: 200px; }
.auth-prefs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.auth-title { font-size: 1.32rem; margin-bottom: 4px; }
.auth-foot { text-align: center; margin-top: 18px; }

/* The way out of a refused password, at the end of the row it belongs to. */
.login-forgot { margin: -6px 0 0; text-align: right; }

/* The two screens that only report: a link is on its way, and the password has
   been changed. No form on either, so the icon carries the state and the text
   is centred under it rather than set as a paragraph. */
.reset-sent { text-align: center; }
.reset-sent p { margin-bottom: 8px; }
.reset-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: 14px;
  border-radius: 50%;
  background: var(--brand-light);
  color: var(--brand);
  font-size: 1.3rem;
}
.reset-icon.is-done { background: var(--ok-bg); color: var(--ok); }

@media (max-width: 480px) {
  /* Top-aligned rather than centred, and a tighter gutter. Vertically centred
     on a short screen the card starts below the fold, so the first thing on
     screen is empty canvas. */
  .auth-shell { align-items: flex-start; padding: 24px 14px 40px; }
}

/* --- The interface language switcher ----------------------------------------
   Three flag buttons on one row - in the top bar, over the sign-in forms and
   in the landing page's header. Not a
   dropdown: a native option list cannot carry a picture, so the flag had to be
   faked with a scripted overlay, which left the control one shape before the
   script ran and another after. Three fit, so they are all on screen and the
   current one is simply marked.

   The document-language selects further down are a different control choosing a
   different thing, and stay as dropdowns. */
/* The theme switch. The same round toggle as the language switch beside it -
   they sit together in the top bar, and two different-looking controls in one
   corner read as two unrelated features rather than as a pair of preferences.
   Glyph only: three words would double the width of the bar for a control
   nobody uses twice.

   A grey outlined pill holding the palette mark and, beside it, a white
   track for the buttons - see `.toggle-track`. The theme in use on a raised
   round knob, its glyph in the selection colour; the other two flat in the
   track and faded, so they read as off. The knob is shared with the language
   switch - see `.lang-switch .lang-option.is-active` below. */
.theme-switch { display: inline-flex; align-items: center; }

.theme-options {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  background: color-mix(in srgb, var(--ink) var(--pill-mix), var(--surface));
  border: 1px solid var(--line);
  border-radius: 999px;
}

.theme-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-faint);
  font: inherit;
  cursor: pointer;
}
.theme-option .icon { width: 14px; height: 14px; transition: opacity 0.15s ease; }
.theme-option:not(.is-active) .icon { opacity: 0.6; }
.theme-option:not(.is-active):hover { color: var(--ink); }
.theme-option:not(.is-active):hover .icon { opacity: 1; }
.theme-option:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.theme-option.is-active { color: var(--select-ink); }

.lang-switch { display: inline-flex; align-items: center; }

.lang-options {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.lang-option {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 6px;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-muted);
  font: inherit;
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.lang-option:hover { background: var(--hover-fill); color: var(--ink); }
.lang-option:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.lang-option.is-active {
  background: var(--select-fill);
  color: var(--select-ink);
  box-shadow: inset 0 0 0 1px var(--select-line), var(--shadow);
}

/* The flag is 20x14, not square like an .icon glyph, so it gets its own rule
   rather than borrowing that one. The flags are flat colour to the edge and
   several are pale at one corner; without the ring they dissolve into a light
   toolbar. */
.lang-option .flag {
  width: 17px;
  height: 12px;
  display: block;
  flex: none;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(22, 32, 44, 0.15);
}

/* The interface switcher in the bar: a toggle of round flags, no letters.

   A white track inside a grey pill, with the language in use
   sitting on a raised round knob - the one flag in full colour, ringed in the
   surface colour and lifted by a shadow. The others lie flat in the track,
   washed out and desaturated, so they read as off rather than as two more
   choices of equal weight. A hover brings the colour most of the way back,
   which is the preview of what a click will do.

   The flags are cropped to circles from their centre - `slice` on the <svg>
   in core/language_switch.html fills the square and loses the sides, which
   keeps a cross or a set of stripes recognisable at 20px.

   The profile had a second copy of this control, as radios in its form; it
   drifted from this one and is gone - the switcher now saves the account's
   preference itself. */
.lang-switch .lang-options {
  gap: 2px;
  padding: 2px;
  background: color-mix(in srgb, var(--ink) var(--pill-mix), var(--surface));
  border-radius: 999px;
}

/* What each pill switches, as a muted glyph at its leading end: the translate
   mark on the language pill, a palette on the theme pill - see
   core/language_switch.html and core/theme_switch.html. Without them the two
   were the same round track side by side, and nothing said which was which. */
.toggle-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 26px;
  margin-left: 2px;
  color: var(--ink-faint);
}
.toggle-mark .icon { width: 14px; height: 14px; }

/* Below the common phone width the marks go. They say which pill is which,
   and beside the flags and the sun and moon they are the one thing the top
   bar can lose without losing a control - see `.topbar-actions` at 600px. */
@media (max-width: 374px) {
  .toggle-mark { display: none; }
}

/* The buttons' own track, inside the pill. The mark used to share one canvas
   track with them - the same fill, the same size, in the same row - and read
   as a fourth button that did nothing. Now the choices sit on a white track of
   their own and the mark stays out on the pill's grey: a label on the
   control, not a part of it. The grey is mixed from the ink at --pill-mix
   rather than taken from --canvas, a step from --surface that cannot be seen
   on the dark theme. */
.toggle-track {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(22 32 44 / 0.12);
}

/* Both tracks take the stronger rule, so the pill reads as an outlined shape
   in the bar rather than as a faint grey smudge behind the buttons. */
.lang-switch .lang-options,
.theme-options { border-color: var(--line-strong); }

.lang-switch .lang-option {
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border-radius: 50%;
}

.lang-switch .lang-option .flag {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgb(22 32 44 / 0.08);
  transition: filter 0.15s ease, opacity 0.15s ease;
}

/* Every language button is a drawn circle - a thin grey outline round the
   faded flag - so the three read as three round buttons in a track, and the
   selected one is the only circle drawn in colour. */
.lang-switch .lang-option:not(.is-active) { box-shadow: inset 0 0 0 1px var(--line-strong); }

/* The knob, shared by both toggles: a surface-coloured disc, ringed in the
   brand colour so the choice in force is found at a glance, with a shadow
   under it. The inner ring is white at low alpha - invisible on the light
   theme's white, and the edge that keeps the disc from sinking into the
   track on the dark one. */
.lang-switch .lang-option.is-active,
.theme-option.is-active {
  background: var(--surface);
  box-shadow:
    0 1px 3px rgb(22 32 44 / 0.22),
    0 0 0 2px var(--brand),
    inset 0 0 0 1px rgb(255 255 255 / 0.12);
}

.lang-switch .lang-option:not(.is-active) .flag {
  filter: saturate(0.55);
  opacity: 0.42;
}

.lang-switch .lang-option:not(.is-active):hover { background: transparent; }
.lang-switch .lang-option:not(.is-active):hover .flag {
  filter: saturate(0.9);
  opacity: 0.85;
}

/* Off the screen but still in the accessibility tree. The second selector is
   not a duplicate: the shared input rule sets `width: 100%` and outweighs a
   single class, so a clipped radio inside a form came out 390px wide and pushed
   the whole page sideways on a phone. A utility whose one job is to take up no
   room has to win. */
.visually-hidden,
.field .visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- Incoming invoices ------------------------------------------------------
   The running total under the amounts - see purchases/js/purchase-form.js. */
.purchase-total { margin: 4px 0 0; color: var(--ink-muted); font-size: 0.92rem; }
.purchase-total strong { color: var(--ink); font-variant-numeric: tabular-nums; margin-left: 4px; }

/* Its lines. The description takes what the figures leave; on a phone the
   table scrolls inside its wrapper rather than squeezing the description to
   nothing. Everything reads from the left, as it is typed: the headings and
   the sums are indented to where an input's text starts - the table's 14px
   cell padding, the input's 1px border and its 9px padding - so each column
   is one straight left edge from heading to total. `table.data` in the
   selectors because its own cell padding would otherwise win. */
.purchase-lines { min-width: 40rem; }
.purchase-lines .col-amount { width: 9.5rem; }
.purchase-lines .col-rate { width: 6.5rem; }
.purchase-lines input[type="number"] { font-variant-numeric: tabular-nums; }
table.data.purchase-lines th { padding-left: 24px; }
table.data.purchase-lines .sum-row td { padding: 10px 14px 10px 24px; font-variant-numeric: tabular-nums; }
/* The "Add line" row sits between the lines and the sums: a row's own light
   rule above it, not the heavy one the sums take, and its button on the
   inputs' left edge - the cells' own 14px. */
table.data.purchase-lines .add-line-row td {
  padding: 8px 14px;
  border-top: 1px solid var(--line-soft);
  font-weight: inherit;
}
/* Under 560px the table's cells close to 8px (see `table.data td` there), and
   the indents follow: 8 + 1 + 9 for the text, 8 for the button. */
@media (max-width: 560px) {
  table.data.purchase-lines th,
  table.data.purchase-lines .sum-row td { padding-left: 18px; }
  table.data.purchase-lines .add-line-row td { padding-left: 8px; }
}
.purchase-lines-help { margin: 8px 0 16px; }

/* The period's VAT from both books - see purchases/kuf.html. Three lines and
   a rule above the result, laid out like the sum it is. */
.vat-position { margin: 0 0 10px; max-width: 420px; }
.vat-position div { display: flex; justify-content: space-between; gap: 16px; padding: 4px 0; }
.vat-position dt { color: var(--ink-muted); font-weight: 400; }
.vat-position dd { margin: 0; font-variant-numeric: tabular-nums; }
.vat-position .vat-position-balance { border-top: 1px solid var(--line-strong); margin-top: 4px; padding-top: 8px; }
.vat-position .vat-position-balance dt,
.vat-position .vat-position-balance dd { color: var(--ink); font-weight: 650; }

/* --- Command palette ---------------------------------------------------------
   Ctrl K / Cmd K, or the Search pill in the bar - see core/js/palette.js. A
   native <dialog>: showModal() gives it the top layer, a backdrop, Escape to
   close and focus held inside, none of which has to be written by hand. */
.palette-trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 5px 0 11px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--canvas);
  color: var(--ink-faint);
  font: inherit;
  font-size: 0.84rem;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.palette-trigger[hidden] { display: none; }
.palette-trigger:hover { color: var(--ink); border-color: var(--ink-faint); }
.palette-trigger:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.palette-trigger .icon { width: 14px; height: 14px; }

.palette-trigger kbd,
.palette kbd {
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 600;
  padding: 1px 7px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-muted);
}

.palette {
  width: min(620px, calc(100vw - 32px));
  max-height: min(70vh, 560px);
  margin: 12vh auto auto;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.palette[open] { display: flex; flex-direction: column; }
.palette::backdrop { background: rgb(10 15 22 / 0.45); backdrop-filter: blur(2px); }

.palette-field {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
}
.palette-field .icon { flex: none; width: 17px; height: 17px; color: var(--ink-faint); }
/* The shared input rule draws a box and a focus ring; this field is the
   dialog's whole first row and is drawn by the row. */
.palette-input,
.palette-input:focus {
  flex: 1;
  min-width: 0;
  width: auto;
  height: auto;
  padding: 4px 0;
  border: none;
  outline: none;
  box-shadow: none;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 1rem;
}

/* The way out - see core/base_app.html. On a desktop it is the Esc chip, which
   now answers a click as well as the key. A phone has no Esc key, and the
   dialog all but fills its width, so there is little backdrop to tap either:
   there the chip gives way to an X with a target a thumb can find. The
   pointer query is for a tablet, which is wider than a phone and just as
   keyless. */
.palette-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-faint);
  font: inherit;
  cursor: pointer;
}
.palette-close .icon { display: none; color: inherit; }
.palette-close:hover kbd { color: var(--ink); border-color: var(--line-strong); }
.palette-close:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

@media (max-width: 720px), (pointer: coarse) {
  .palette-close {
    width: 36px;
    height: 36px;
    /* Into the row's padding, so the bigger target does not make the row taller. */
    margin: -6px -10px -6px 0;
  }
  .palette-close kbd { display: none; }
  .palette-close .icon { display: block; width: 16px; height: 16px; }
  .palette-close:hover { color: var(--ink); background: var(--hover-fill); }
}

.palette-list { flex: 1; margin: 0; padding: 6px; list-style: none; overflow-y: auto; }

.palette-group {
  padding: 10px 10px 4px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.palette-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  color: var(--ink);
  cursor: pointer;
}
.palette-item .icon { flex: none; width: 15px; height: 15px; color: var(--ink-faint); }
.palette-item.is-active { background: var(--hover-fill); }
.palette-item.is-active .icon { color: var(--brand); }
.palette-label { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.palette-detail {
  margin-left: auto;
  padding-left: 12px;
  font-size: 0.82rem;
  color: var(--ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.palette-empty { padding: 22px 10px; text-align: center; font-size: 0.9rem; color: var(--ink-muted); }

.palette-foot {
  padding: 9px 16px;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--ink) 2.5%, var(--surface));
  font-size: 0.76rem;
  color: var(--ink-faint);
}

/* On a phone the pill is the glyph alone - the bar has no room for a word and
   there is no keyboard to take the shortcut. */
@media (max-width: 720px) {
  .palette-trigger { padding: 0 9px; }
  .palette-trigger span,
  .palette-trigger kbd { display: none; }
  .palette { margin-top: 8vh; }
}

/* --- Language select ------------------------------------------------------- */
/* Replaces the native control so a flag can be an image; see
   core/js/language-select.js for why an emoji cannot do this job. */
.langsel { position: relative; display: inline-block; }
.langsel-native { display: none; }

.langsel-button {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 9px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 0.82rem; line-height: 1.4;
  cursor: pointer;
}
.langsel-button:hover { border-color: var(--brand); }
.langsel-caret { width: 10px; height: 6px; margin-left: 2px; opacity: 0.55; }

.langsel-list {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 40;
  min-width: 100%; margin: 0; padding: 4px; list-style: none;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
}

.langsel-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px; border-radius: var(--radius-sm);
  font-size: 0.85rem; white-space: nowrap; cursor: pointer;
}
.langsel-item:hover { background: var(--brand-tint); }

/* Inside a form field it is one control among others, so it fills the cell and
   matches their height and type - the native <select> it replaces took width:100%
   from the shared input rule, and the button, sizing to its text, came out at 108px
   in a 254px cell. Elsewhere - the topbar, the invoice summary - it stays compact. */
.field .langsel { display: block; width: 100%; }

.field .langsel-button {
  width: 100%;
  justify-content: flex-start;
  padding: 8px 11px;
  font-size: 0.92rem;
  /* Same 41px the inputs and selects settle at, so the control it stands in for
     is not visibly shorter than the one beside it. */
  height: 41px;
}

.field .langsel-caret { margin-left: auto; }
.field .langsel-list { width: 100%; }
.langsel-item.is-selected { background: var(--brand-light); color: var(--brand); font-weight: 600; }

/* 20x14 is the flag sprite's own ratio; a border keeps a white flag off a white
   background. */
.lang-flag {
  width: 20px; height: 14px; flex: 0 0 20px;
  border-radius: 2px; box-shadow: 0 0 0 1px rgba(22, 32, 44, 0.12);
}

/* The fallback for a language with no flag drawn yet. */
.lang-code {
  flex: 0 0 auto; border-radius: 3px;
  background: var(--brand-light); color: var(--brand);
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.04em;
}

/* The language control in the invoice summary: it sits in a label/value row, so
   it has to look like the value it replaced rather than like a form. */
.row-form { display: inline-flex; align-items: center; gap: 6px; margin: 0; }
.row-form .langsel-button { padding: 3px 7px; font-size: 0.78rem; }
.row-form .lang-flag { width: 17px; height: 12px; flex-basis: 17px; }

/* The language question inside the confirm dialog.

   It is a sibling of .modal-body rather than inside it, so it carries the same
   horizontal padding itself - without that the options ran edge to edge and sat
   against the card's border. */
.modal-choice { padding: 4px 20px 20px; }

/* These triggers ask a question and pass no message, and an empty body still
   contributed its own 32px, opening a gap under the title. */
.modal-body:empty { padding: 0; }

.choice-hint {
  font-size: 0.86rem; color: var(--ink-muted); line-height: 1.5;
  margin: 0 0 14px;
}

.choice-list { display: flex; flex-direction: column; gap: 8px; }

.choice {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-size: 0.95rem; cursor: pointer;
}
.choice:hover { background: var(--hover-fill); }
.choice:has(input:checked) { border-color: var(--brand); background: var(--select-fill); font-weight: 600; }
.choice input { margin: 0; accent-color: var(--brand); }

/* Marks a feature that exists for one country's rules - the KIF is Bosnian tax
   law, not a general invoicing feature, and nothing else on the page says so. */
.country-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px 3px 6px; border-radius: 999px;
  border: 1px solid var(--line-strong); background: var(--surface);
  font-size: 0.68rem; font-weight: 650; letter-spacing: 0.02em;
  color: var(--ink-muted); text-transform: none; vertical-align: middle;
  cursor: help;
}
h1 .country-badge { margin-left: 10px; position: relative; top: -2px; }
.btn .lang-flag { width: 16px; height: 11px; flex-basis: 16px; }

/* --- Recording a payment --------------------------------------------------- */
/* Six fields and a button. As a `.filters` flex row each field sized itself and
   the row wrapped wherever it ran out of width, so the two conversion fields
   landed on different lines from each other and out of their source order, with
   the help text under one of them dragging that column further out of line.

   A grid instead: three even columns, two full rows, and the button on its own
   row. The fields no longer decide the layout, so a longer label in Bosnian or
   German moves nothing. */
.payment-form {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px 14px;
  /* Fields align at their tops, so the inputs across a row line up even when
     one cell carries help text underneath. */
  align-items: start;
}

.payment-form .field { margin: 0; min-width: 0; }

/* Its own row, right-aligned under the fields. The alternative - sitting in the
   last cell of row two - puts it beside a field carrying help text, where it
   lines up with neither the input above it nor the text beside it. */
.payment-form .field-actions {
  grid-column: 1 / -1;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

@media (max-width: 900px) {
  .payment-form { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  .payment-form { grid-template-columns: 1fr; }
}


/* --- Password requirements ------------------------------------------------- */

/* Stated once under both password boxes rather than as help text on one of
   them, which made the two columns of the field row different heights. */
.password-rules {
  /* Positive top margin, deliberately. This was -4px to tuck the block under
     the field row, which was fine until a password was rejected: the validator
     messages render inside the field above, and the block was pulled up over
     them. Nothing that sits below a field may use negative margin - the field's
     height is not fixed, it grows by however many errors it has. */
  margin: 10px 0 15px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--brand-tint);
}

.password-rules-title {
  display: block;
  font-size: 0.78rem;
  font-weight: 650;
  color: var(--ink-muted);
  margin-bottom: 7px;
}

/* Two columns where there is room: four stacked lines read as a wall of
   conditions, which is the thing being avoided. */
.password-rules ul {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (max-width: 520px) {
  .password-rules ul { grid-template-columns: 1fr; }
}

.password-rules li {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ink-muted);
}

.password-rules li .icon { color: var(--brand); margin-top: 0.22em; font-size: 0.85em; }

/* --- Dark theme -------------------------------------------------------------

   Three states, and only two palettes. "System" is the absence of a choice: no
   `data-theme` attribute is written, and the media query below decides. Light
   and Dark write the attribute and win over the device either way, which is what
   makes the switch worth having for somebody whose laptop is dark and whose eyes
   are not.

   The two pairs of blocks are identical by necessity and are maintained as such.
   `apps/core/tests/test_theme.py` fails if they ever stop matching: CSS has no
   way to say "these selectors share a body", and a dark theme that is only right
   when the device agrees with the toggle is the kind of bug nobody reports.
   --------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --brand: #6aa5ff;
    /* In the light theme this is a *darker* brand, for emphasis. Emphasis on a
       dark ground means brighter, so the two are mirror images rather than the
       same colour. */
    --brand-dark: #a3c8ff;
    --on-brand: #0d1620;

    --ink: #e4ebf2;
    --ink-muted: #9aabbd;
    --ink-faint: #93a4b6;

    --surface: #171d26;
    --canvas: #10151c;

    /* The state hues, lifted rather than inverted: the same four meanings, at the
       lightness a dark ground needs. Each was checked the same way the light set
       was - as text on the card, as text on its own chip, and as text on the
       strongest row tint it can ever sit on. */
    --ok: #4cb782;
    --warn: #d99a4e;
    --danger: #f28880;
    --alt: #a892e8;
    --credit: #dc9a63;

    /* Every mix goes up. The same percentage of a hue reads far weaker against a
       dark ground than against white - 12% of a pale red over #171d26 is very
       nearly #171d26 - so a dark theme built by swapping only the hues comes out
       flat and grey. These are the numbers that keep a chip looking like a chip. */
    --chip-mix: 16%;
    --hairline-mix: 34%;
    --tint-mix: 7%;
    --light-mix: 15%;

    /* And the toggle mixes go up hardest of all - see the light block. */
    --hover-mix: 15%;
    --select-mix: 26%;
    --select-line-mix: 55%;
    --pill-mix: 18%;
    /* Not the plain brand: on the wash --select-mix now makes, a 0.82rem label
       in it measures 3.9:1, under AA. The brighter blue clears it at 5.6:1. */
    --select-ink: var(--brand-dark);

    /* And so does the row-tint ladder, up to the point where the red printed *on*
       an attention row stops clearing AA - which is what caps the top at 20%. */
    --tint-1: 7%;   --tint-1-hover: 11%;
    --tint-2: 12%;  --tint-2-hover: 16%;
    --tint-3: 17%;  --tint-3-hover: 20%;
    --tint-quiet: 8%;  --tint-quiet-hover: 12%;

    /* The desk the layout editor lays its page on. Furniture, so it themes -
       unlike the page itself, which is paper and does not. */
    --desk: #2b3441;

    /* A shadow on a dark ground cannot be a darker grey - there is nothing darker
       to go to - so it is nearly black, and the card is separated by its own
       lighter fill instead. */
    --shadow: 0 1px 2px rgb(0 0 0 / 0.30);
    --shadow-lg: 0 4px 12px rgb(0 0 0 / 0.45), 0 16px 40px rgb(0 0 0 / 0.40);
  }

  .chart {
    /* The chart palette is re-picked, not reused. Its four hues were chosen to be
       dark enough to read on a white card, which is exactly the wrong constraint
       here. Re-checked the same way: worst confusable pair is dE 10.2 under
       deuteranopia, above the 8 threshold, and all four clear 3:1 on the card.

       Slot 3 goes *lighter* than the blue where the light theme's goes darker. The
       tritanopia problem is that blue and teal differ only in hue; the fix is to
       separate them in lightness, and on a dark ground there is one way to go.

       Slot 4 is the one place a token is not shared with the state palette. The
       chart needs a deeper red to stay clear of the orange under deuteranopia;
       --danger needs a lighter one to be legible as text on a dark row. On white
       one red did both jobs; here it cannot. */
    --viz-cat-1: #5c9ded;
    --viz-cat-2: #f0a057;
    --viz-cat-3: #79d8c8;
    --viz-cat-4: #e8574c;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --brand: #6aa5ff;
  /* In the light theme this is a *darker* brand, for emphasis. Emphasis on a
     dark ground means brighter, so the two are mirror images rather than the
     same colour. */
  --brand-dark: #a3c8ff;
  --on-brand: #0d1620;

  --ink: #e4ebf2;
  --ink-muted: #9aabbd;
  --ink-faint: #93a4b6;

  --surface: #171d26;
  --canvas: #10151c;

  /* The state hues, lifted rather than inverted: the same four meanings, at the
     lightness a dark ground needs. Each was checked the same way the light set
     was - as text on the card, as text on its own chip, and as text on the
     strongest row tint it can ever sit on. */
  --ok: #4cb782;
  --warn: #d99a4e;
  --danger: #f28880;
  --alt: #a892e8;
  --credit: #dc9a63;

  /* Every mix goes up. The same percentage of a hue reads far weaker against a
     dark ground than against white - 12% of a pale red over #171d26 is very
     nearly #171d26 - so a dark theme built by swapping only the hues comes out
     flat and grey. These are the numbers that keep a chip looking like a chip. */
  --chip-mix: 16%;
  --hairline-mix: 34%;
  --tint-mix: 7%;
  --light-mix: 15%;

  /* And the toggle mixes go up hardest of all - see the light block. */
  --hover-mix: 15%;
  --select-mix: 26%;
  --select-line-mix: 55%;
  --pill-mix: 18%;
  /* Not the plain brand: on the wash --select-mix now makes, a 0.82rem label
     in it measures 3.9:1, under AA. The brighter blue clears it at 5.6:1. */
  --select-ink: var(--brand-dark);

  /* And so does the row-tint ladder, up to the point where the red printed *on*
     an attention row stops clearing AA - which is what caps the top at 20%. */
  --tint-1: 7%;   --tint-1-hover: 11%;
  --tint-2: 12%;  --tint-2-hover: 16%;
  --tint-3: 17%;  --tint-3-hover: 20%;
  --tint-quiet: 8%;  --tint-quiet-hover: 12%;

  /* The desk the layout editor lays its page on. Furniture, so it themes -
     unlike the page itself, which is paper and does not. */
  --desk: #2b3441;

  /* A shadow on a dark ground cannot be a darker grey - there is nothing darker
     to go to - so it is nearly black, and the card is separated by its own
     lighter fill instead. */
  --shadow: 0 1px 2px rgb(0 0 0 / 0.30);
  --shadow-lg: 0 4px 12px rgb(0 0 0 / 0.45), 0 16px 40px rgb(0 0 0 / 0.40);
}

:root[data-theme="dark"] .chart {
  /* The chart palette is re-picked, not reused. Its four hues were chosen to be
     dark enough to read on a white card, which is exactly the wrong constraint
     here. Re-checked the same way: worst confusable pair is dE 10.2 under
     deuteranopia, above the 8 threshold, and all four clear 3:1 on the card.

     Slot 3 goes *lighter* than the blue where the light theme's goes darker. The
     tritanopia problem is that blue and teal differ only in hue; the fix is to
     separate them in lightness, and on a dark ground there is one way to go.

     Slot 4 is the one place a token is not shared with the state palette. The
     chart needs a deeper red to stay clear of the orange under deuteranopia;
     --danger needs a lighter one to be legible as text on a dark row. On white
     one red did both jobs; here it cannot. */
  --viz-cat-1: #5c9ded;
  --viz-cat-2: #f0a057;
  --viz-cat-3: #79d8c8;
  --viz-cat-4: #e8574c;
}

/* --- Renewal import ----------------------------------------------------------
   The split button on the invoice form that puts due renewals onto the
   document, and the pick-list behind its caret. Everything structural comes
   from `.split` and `.menu-panel` above; this is only what the panel needs to
   hold a list of checkboxes rather than a list of links. */
.card-head-actions { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Wider than an ordinary menu: each row carries a name, a date and an amount,
   and the default 280px ceiling wraps every one of them onto three lines. */
.renewal-menu { min-width: 280px; max-width: 380px; }

.menu-head {
  padding: 4px 10px 6px;
  color: var(--ink-faint);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* A label, not a button: the whole row toggles its own checkbox, so the target
   is the row rather than the 13px box at the left of it. */
.renewal-option { align-items: flex-start; cursor: pointer; white-space: normal; }
.renewal-option input[type="checkbox"] { margin-top: 3px; flex: none; }
.renewal-option-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.renewal-option-name { font-weight: 500; }
.renewal-option-meta { font-weight: 400; }

/* --- Section tabs -------------------------------------------------------------
   For a page that is several subjects rather than one long list - the staff
   overview is four: people, documents, money, and whether the nightly jobs are
   running. Stacked vertically those read as one undifferentiated scroll of
   tiles, and the question "what am I looking at" has no answer anywhere on it.

   Built the way the plan page's monthly/yearly switch is: radios and CSS,
   with no script behind the switching itself. Every rule hangs off `:checked ~`,
   so the inputs must stay siblings of both the tab strip and the panels.

   `core/js/tabs.js` is an enhancement on top, not a requirement - it only
   redraws the charts inside a panel that has just been revealed, because a
   chart measured while its panel was display:none was measured at zero. Without
   it the switching still works and the charts still render, just at the
   fallback width. */
.tab-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}

.tab-strip {
  display: flex;
  gap: 2px;
  margin-bottom: 18px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--canvas);
  overflow-x: auto;
}

.tab-strip label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  border-radius: var(--radius-sm);
  color: var(--ink-muted);
  font-size: 0.88rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  transition: background 0.15s ease, color 0.15s ease;
}
.tab-strip label:hover { color: var(--ink); background: var(--brand-tint); }
.tab-strip label .icon { width: 15px; height: 15px; opacity: 0.65; }

/* A count riding on the tab, so the strip says how much is behind each one
   without the panel having to be opened. */
.tab-strip .tab-count {
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--line);
  color: var(--ink-faint);
  font-size: 0.75rem;
  font-weight: 700;
}
.tab-strip .tab-count.is-danger { background: var(--danger-bg); color: var(--danger); }

.tab-panel { display: none; }

/* Which panel is shown. One pair per tab, and both halves have to name the same
   id - a mismatch here shows an empty page with no error anywhere. The staff
   overview is the only user of these today; a second tabbed page adds its own
   pair rather than reusing these ids. */
#staff-tab-people:checked ~ #panel-people,
#staff-tab-documents:checked ~ #panel-documents,
#staff-tab-money:checked ~ #panel-money,
#staff-tab-jobs:checked ~ #panel-jobs { display: block; }

#staff-tab-people:checked ~ .tab-strip label[for="staff-tab-people"],
#staff-tab-documents:checked ~ .tab-strip label[for="staff-tab-documents"],
#staff-tab-money:checked ~ .tab-strip label[for="staff-tab-money"],
#staff-tab-jobs:checked ~ .tab-strip label[for="staff-tab-jobs"] {
  background: var(--surface);
  color: var(--brand);
  box-shadow: 0 1px 2px rgba(22, 32, 44, 0.1);
}
#staff-tab-people:checked ~ .tab-strip label[for="staff-tab-people"] .icon,
#staff-tab-documents:checked ~ .tab-strip label[for="staff-tab-documents"] .icon,
#staff-tab-money:checked ~ .tab-strip label[for="staff-tab-money"] .icon,
#staff-tab-jobs:checked ~ .tab-strip label[for="staff-tab-jobs"] .icon { opacity: 1; }

/* The group takes the focus ring, since the radios themselves are clipped. */
.tab-input:focus-visible ~ .tab-strip { outline: 2px solid var(--brand); outline-offset: 2px; }

@media (max-width: 640px) {
  .tab-strip label { padding: 8px 11px; font-size: 0.82rem; }
  .tab-strip label .tab-text { display: none; }
  .tab-strip label .icon { width: 17px; height: 17px; }
}

/* A heading over a band of tiles. The tiles said what each number was and never
   what the group of them was about. */
.band-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0 0 10px;
}
.band-head h2 {
  margin: 0;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.band-head .band-note { color: var(--ink-faint); font-size: 0.8rem; }

/* --- Saved line-item descriptions ---------------------------------------------
   The bookmark pair welded to the right of every description field on the
   invoice form: one half opens the wording the account has kept, the other
   keeps what is typed. Structure comes from `.split` and `.menu-panel` above;
   this is what a description *cell* needs that a page-level split button does
   not - it sits inside a table cell about a third of the row wide. */
.snippet-field { display: flex; align-items: flex-start; gap: 4px; }
.snippet-field > input { flex: 1 1 auto; min-width: 0; }

/* Held to the input's own height so the three controls read as one field
   rather than as a field with two buttons parked beside it. */
.snippet-actions { flex: none; }
.snippet-actions .btn { padding: 0 7px; }
.snippet-actions .icon { width: 13px; height: 13px; }

/* Confirmation without a toast. A save here is a small, frequent action in the
   middle of writing an invoice, and a flash message for each one would bury the
   page in them - the button says so itself and the tooltip carries the words. */
.snippet-actions .btn.is-flashed {
  border-color: var(--brand);
  color: var(--brand);
  background: var(--brand-tint);
}

.snippet-menu { min-width: 300px; max-width: 420px; }

.snippet-filter {
  width: 100%;
  margin: 0 0 6px;
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.85rem;
}

/* Capped and scrolled: sixty saved phrases is a panel taller than the window,
   and `place()` in shell.js can only flip it, not shrink it. */
.snippet-list { max-height: 260px; overflow-y: auto; }

.snippet-row { display: flex; align-items: stretch; gap: 2px; }
.snippet-row .snippet-pick { flex: 1 1 auto; min-width: 0; flex-direction: column; align-items: flex-start; gap: 1px; }
.snippet-row .snippet-remove { flex: none; opacity: 0; }
.snippet-row:hover .snippet-remove,
.snippet-row:focus-within .snippet-remove { opacity: 1; }
.snippet-row .snippet-remove:hover { color: var(--danger); }

/* Wrapped rather than clipped. A description is a sentence, and the point of
   the list is to read them - an ellipsis at 40 characters makes two similar
   lines indistinguishable, which is exactly when you need to tell them apart. */
.snippet-text { white-space: normal; font-weight: 500; }
.snippet-detail { white-space: normal; font-weight: 400; }

.snippet-empty { padding: 10px; text-align: center; }

/* The renewal menu's empty state. Wrapped and padded like a sentence rather
   than a row, because it is one - see `renderEmpty` in renewal-import.js. */
.renewal-empty { padding: 8px 10px 10px; white-space: normal; line-height: 1.45; }

/* A disabled split button still reads as a control worth knowing about, which
   is the point of leaving it on the page when nothing is due. */
.split > .btn:disabled { opacity: 0.55; cursor: default; }
.split > .btn:disabled:hover { background: var(--surface); border-color: var(--line); }

/* --- The billing-period menu on a line's detail field -------------------------
   Shares `.snippet-field` with the description above it, so the two fields and
   their controls line up in one column rather than each finding its own edge. */
.period-actions { flex: none; }
.period-actions .btn { padding: 0 7px; }
.period-actions .icon { width: 13px; height: 13px; }

.period-menu { min-width: 240px; }

/* Value first, what it means second. The three months differ by one word each,
   and reading "September 2026" against "October 2026" to work out which is
   "next" is slower than being told. */
.period-list .period-pick { flex-direction: column; align-items: flex-start; gap: 1px; }
.period-text { font-weight: 500; white-space: nowrap; }
.period-note { white-space: nowrap; }

/* A menu panel lifted out of a table that would clip it - see `place()` in
   shell.js. Fixed to the viewport, so it needs to clear the topbar as well as
   the page; the coordinates themselves are set there, from the trigger. */
.menu-panel.is-floating { position: fixed; z-index: 70; }

/* The honeypot on the registration and password-reset forms - see
   core/_antispam_fields.html. Off the edge of the page rather than
   `display: none`: it has to stay *rendered*, because a form-filling script
   that skips unrendered boxes is the script it is there to catch, and it has
   to stay out of the accessibility tree, which `aria-hidden` on the wrapper
   does. `!important` so no later rule on wrappers can move it back. */
.hp-field {
  position: absolute !important;
  left: -9999px !important;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- The second factor ---------------------------------------------------
   The enrolment page and the recovery codes. Three small things a page of
   digits needs: the QR at a size a phone can actually read, the secret and
   the codes in a monospace face so an O is not read as a 0, and the codes in
   columns so ten of them are one block rather than a scroll. */
.qr-holder {
  display: inline-block;
  margin: 10px 0;
  padding: 10px;
  background: #fff;           /* a QR must be dark-on-light whatever the theme */
  border: 1px solid var(--line);
  border-radius: 8px;
}
.qr-holder svg { display: block; width: 180px; height: 180px; }

.secret-key,
.recovery-codes code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.08em;
}
.secret-key {
  display: inline-block;
  margin-top: 6px;
  padding: 6px 10px;
  background: var(--canvas);
  border-radius: 6px;
  word-break: break-all;
}

.recovery-codes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px 18px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.recovery-codes code { font-size: 1.05em; }

/* The numbered steps on the enrolment page. */
ol.steps { margin: 0; padding-left: 20px; }
ol.steps > li { margin-bottom: 18px; }
ol.steps > li:last-child { margin-bottom: 0; }

@media (max-width: 640px) {
  .recovery-codes { grid-template-columns: minmax(0, 1fr); }
}

/* --- The security rows on the profile page -------------------------------
   Password and the second factor, each as a row of icon, description and
   action. They used to be two words in a card footer, which is where nobody
   found the one that protects the account. The icon carries the state as
   well as the badge does: a struck-through shield reads as "off" before the
   word next to it is read. */
.security-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 14px 0;
}
.security-row + .security-row { border-top: 1px solid var(--line-soft); }
.security-row:first-child { padding-top: 0; }
.security-row:last-child { padding-bottom: 0; }

.security-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: var(--canvas);
  color: var(--ink-muted);
  font-size: 1.15em;
}
.security-icon.is-on { background: var(--ok-bg); color: var(--ok); }
.security-icon.is-off { background: var(--warn-bg); color: var(--warn); }

.security-text strong { display: inline; margin-right: 6px; }
.security-text .tiny { margin-top: 2px; }

@media (max-width: 560px) {
  /* The button drops under the description rather than squeezing it to a
     column of single words. */
  .security-row { grid-template-columns: auto minmax(0, 1fr); }
  .security-row > .btn { grid-column: 2; justify-self: start; }
}

/* The GiroCode on the client's page. Kept on a white tile whatever the theme:
   a QR is read as dark modules on a light field, and a scanner pointed at an
   inverted one usually just fails. */
.public-qr { margin-top: 16px; text-align: center; }
.public-qr-code {
  display: inline-block;
  padding: 8px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.public-qr-code svg { display: block; width: 150px; height: 150px; }

/* Why an invoice's XML is EN 16931 rather than XRechnung. A list of fields to
   fill in, not a warning: the document is valid either way, and the note is
   only there because a portal rejection names none of them. */
.einvoice-note { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 18px; }
.einvoice-missing { margin: 4px 0 0; padding-left: 18px; }
.einvoice-missing li { margin-top: 2px; }

/* The indicative combined total on a stat tile. Quieter than the figures
   above it on purpose: those are the answer, this is an approximation, and
   the type size is the first thing that says so. */
.stat-approx {
  margin-top: 4px;
  font-size: 0.92em;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.stat-approx-date { margin-left: 6px; font-size: 0.85em; opacity: 0.75; }

/* Supporting files: the upload row on the invoice page, and the per-send
   choice on the send page. */
.attach-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.attach-form input[type="file"] { flex: 1 1 200px; min-width: 0; font-size: 0.9em; }

.attach-choices { margin: 0; padding: 0; list-style: none; }
.attach-choices li { margin-top: 4px; }
.attach-choices label { font-weight: 400; }

/* The audit log. A list rather than a table: a row is an event with a
   variable number of field changes under it, and a table would either need a
   cell that grows or one row per changed field - which would split "who
   changed the bank details" into four entries that have to be read together. */
.audit { margin: 0; padding: 0; list-style: none; }
.audit-row {
  display: flex;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.audit-row:last-child { border-bottom: 0; }

/* The glyph carries the kind of event at a glance; the tone carries whether
   it added something or took it away. Both come from the model. */
.audit-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--neutral-bg);
  color: var(--ink-muted);
}
.audit-ok { background: var(--ok-bg); color: var(--ok); }
.audit-warn { background: var(--warn-bg); color: var(--warn); }
.audit-danger { background: var(--danger-bg); color: var(--danger); }

.audit-body { min-width: 0; flex: 1 1 auto; }
.audit-what { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.audit-target { color: var(--ink-muted); font-variant-numeric: tabular-nums; }

.audit-changes { margin-top: 4px; }
.audit-change {
  display: flex;
  gap: 6px;
  align-items: baseline;
  flex-wrap: wrap;
  font-size: 0.92em;
}
/* The old value struck through and the new one plain, so which is which
   survives being read quickly - the arrow alone does not, on a narrow screen
   where the pair has wrapped onto two lines. */
.audit-was { color: var(--ink-muted); text-decoration: line-through; }
.audit-now { color: var(--ink); }
.audit-arrow { width: 0.8em; height: 0.8em; color: var(--ink-muted); }

.audit-when { margin-top: 4px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.audit-dot { opacity: 0.6; }

/* The late-fee panel on an overdue invoice. Bordered in the overdue colour
   rather than filled with it: the figure is indicative until a document is
   raised for it, and a card that shouts as loudly as the balance above would
   invite it to be read as part of that balance. */
.late-fee { border-color: var(--danger-line); }
.late-fee .card-head h2 { display: flex; align-items: center; gap: 8px; }
.late-fee .card-head svg { color: var(--danger); }

/* The client's other contacts. A list of rows rather than a table: each row
   is an address plus two controls, and a table would give three of its four
   columns to whitespace on the common case of an address with no name. */
.contacts { margin: 0 0 12px; padding: 0; list-style: none; }
.contact-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-soft);
}
.contact-row:last-child { border-bottom: 0; }
.contact-who { min-width: 0; flex: 1 1 auto; word-break: break-word; }
.contact-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }

/* A badge that is also the control that changes it. Kept looking like a badge
   because that is what it reports; the pointer and the hover say it acts. */
.badge-button { cursor: pointer; border-style: solid; font: inherit; font-size: 0.78em; }
.badge-button:hover { filter: brightness(0.97); }

.contact-form { margin-top: 4px; }
.contact-form-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

/* API keys. The value itself is monospace and selectable in one go - it is
   copied into a config file, and a key broken across a line by wrapping is a
   key that gets pasted wrong. */
.api-key, .api-example {
  margin: 0;
  padding: 10px 12px;
  background: var(--neutral-bg);
  border: 1px solid var(--line-soft);
  border-radius: 6px;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 0.85em;
  white-space: pre;
  user-select: all;
}
.api-example { user-select: text; }

.api-keys { margin: 0 0 12px; padding: 0; list-style: none; }
.api-key-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
  flex-wrap: wrap;
}
.api-key-row:last-child { border-bottom: 0; }
.api-key-row.is-revoked { opacity: 0.55; }
.api-key-who { min-width: 0; }
.api-key-form { margin-top: 4px; }

/* The last few webhook attempts, inline under the endpoint. Small and dense
   on purpose: it is a health indicator, not a log - the point is that a row
   of red is noticeable without anybody going looking for it. */
.webhook-recent { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; }
.webhook-attempt {
  padding: 1px 6px;
  border-radius: 10px;
  background: var(--neutral-bg);
  font-variant-numeric: tabular-nums;
}
.webhook-sent { background: var(--ok-bg); color: var(--ok); }
.webhook-queued { background: var(--warn-bg); color: var(--warn); }
.webhook-failed { background: var(--danger-bg); color: var(--danger); }

/* A crontab line on the staff jobs page. Wrapped anywhere rather than scrolled:
   the lines are two hundred characters of paths, and the one that matters is
   the line to paste into `crontab -e`, which has to be selectable whole. */
.cron-line {
  display: block;
  margin-top: 6px;
  padding: 6px 10px;
  background: var(--canvas);
  border-radius: 6px;
  font-size: 0.78rem;
  white-space: pre-wrap;
  word-break: break-all;
}
.cron-note { margin-top: 4px; color: var(--warn); }
/* A job's line sits on its own row under the job, and reads as part of it. */
table.data tr.cron-has-line > td { border-bottom: 0; padding-bottom: 4px; }
table.data tr.cron-detail > td { padding-top: 0; }
.cron-detail .cron-line { margin-top: 4px; }

/* --- Payments out without an incoming invoice ------------------------------- */
/* The three ways out of a line, side by side and wrapping on a narrow screen:
   enter the invoice, or pick one already entered and link it. */
.unaccounted-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.unaccounted-actions select { width: auto; min-width: 12rem; max-width: 22rem; height: 32px; padding-top: 0; padding-bottom: 0; }

/* --- Bank statements ---------------------------------------------------------- */
/* Money in, told apart from money out by more than its missing minus sign. */
.bank-in { color: var(--ok); }

/* A segmented track whose labels are phrases rather than numbers. Too wide for
   a phone in one line, it wraps whole options onto a second one instead of
   breaking "All (3)" in two - and gives up the pill ends, which a two-line
   track cannot keep. */
.segmented-wrap { flex-wrap: wrap; }
.segmented-wrap .segmented-option { white-space: nowrap; }
@media (max-width: 560px) {
  .segmented-wrap { border-radius: var(--radius-sm); }
}

/* The payment a typed total matches, offered under the list it would be
   picked from - see purchases/js/purchase-form.js. */
.bank-suggestion { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; font-size: 0.88rem; }
.bank-suggestion[hidden] { display: none; }

/* A line's date, under its amount on a phone, where the date column is
   dropped - the same move as .row-client. */
.row-date { display: none; }
@media (max-width: 560px) {
  table.data .row-date { display: block; font-weight: 400; }
}

