/*
 * Netifi Billing — application stylesheet.
 *
 * Hand-written, one file, CSS custom properties for every design token.
 * The UI borrows the printed invoice's language: warm paper background,
 * hairline rules, amber used sparingly as the one accent, and every number
 * set in Plex Mono with tabular figures. See docs/superpowers/plans/
 * 2026-08-08-billing-vertical-slice.md, "Design system", for the source of
 * every token below.
 */

/* ---------------------------------------------------------------------
   Fonts — vendored as woff2 in public/assets/fonts/ (400/500/600).
   font-display: swap so text is never invisible while the font loads.
   ------------------------------------------------------------------- */

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-600.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600.woff2') format('woff2');
}

/* ---------------------------------------------------------------------
   Tokens
   ------------------------------------------------------------------- */

:root {
  /* OKLCH with a hex fallback on the line above each: an older browser keeps
     the hex, a current one gets the wide-gamut value. Neutrals are tinted
     toward the wordmark's amber rather than left pure grey, and neither pure
     black nor pure white appears anywhere. */
  --ink: #34322D;
  --ink: oklch(0.34 0.006 85);
  --ink-soft: #6B6862;
  --ink-soft: oklch(0.56 0.008 85);
  --paper: #FCFBF8;
  --paper: oklch(0.985 0.004 90);
  --surface: #FFFEFC;
  --surface: oklch(0.995 0.002 90);
  --rule: #E5E3DE;
  --rule: oklch(0.91 0.006 90);

  --amber: #F3AB33;
  --amber: oklch(0.79 0.145 72);
  --amber-ink: #7A5000;
  --amber-ink: oklch(0.45 0.11 70);
  --blue: #508DEE;
  --blue: oklch(0.66 0.155 258);
  --red: #C0392B;
  --red: oklch(0.53 0.17 27);
  --green: #1E8449;
  --green: oklch(0.52 0.12 152);

  --font-ui: 'IBM Plex Sans', ui-sans-serif, 'Segoe UI', sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, Consolas, monospace;

  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 40px;

  --radius: 6px;
  --radius-sm: 4px;

  /* One step darker than the paper the page sits on, so a header row reads
     as the key to the table rather than as its first entry. */
  --table-head: #F1EEE7;
  --table-head: oklch(0.945 0.011 88);

  /* Every control that sits on a toolbar line is this tall, so a select, an
     input and a button line up on their tops and their bottoms rather than
     each taking its height from its own padding. */
  --control-height: 34px;

  --sidebar-width: 200px;
  --topbar-height: 56px;
  --row-height: 32px;

  /* Sidebar. A desaturated blue rather than a saturated one: it sits behind
     the app all day, and the amber accent has to stay the loudest thing on
     screen. */
  --nav-bg: #1E3A5F;
  --nav-bg: oklch(0.33 0.055 252);
  --nav-bg-soft: #2A4A73;
  --nav-bg-soft: oklch(0.40 0.055 252);
  --nav-ink: #E9EEF5;
  --nav-ink: oklch(0.94 0.012 252);
  --nav-ink-soft: #9DB2CC;
  --nav-ink-soft: oklch(0.75 0.035 252);
  /* Stands in for pure white on the navy: still reads as white, but carries
     the same hue as everything around it. */
  --nav-ink-bright: #FBFCFE;
  --nav-ink-bright: oklch(0.99 0.005 252);

  --shadow-card: 0 1px 2px rgba(51, 51, 51, 0.06), 0 1px 1px rgba(51, 51, 51, 0.04);
  --shadow-drawer: -8px 0 32px rgba(30, 58, 95, 0.18);
  --transition-fast: 120ms ease;
}

/* ---------------------------------------------------------------------
   Reset and base
   ------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

/* The browser hides [hidden] with a rule of its own, and any author rule
   setting display beats it — so a .field-hint at display:block or a grid at
   display:grid stays on screen with hidden="hidden" set and every branch of
   a switched view renders at once. !important because that is the only way
   an attribute meaning "not here" outranks a class saying how to lay it out.
   Without this the rule has to be repeated per component, which is how
   .autocomplete-results came to carry its own copy. */
[hidden] { display: none !important; }

html {
  font-size: 13px;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 1rem;
  line-height: 1.45;
  color: var(--ink);
  background: var(--paper);
}

h1, h2, h3, h4, p, ul, ol, dl, dd { margin: 0 0 var(--space-2); }

h1 { font-size: 1.55rem; font-weight: 600; letter-spacing: -0.01em; }
h2 { font-size: 1.25rem; font-weight: 600; }
h3 { font-size: 1.05rem; font-weight: 600; }

a {
  color: var(--blue);
  text-decoration: none;
}
a:hover { text-decoration: underline; }

small, .text-sm { font-size: 0.85rem; }
.text-soft { color: var(--ink-soft); }
.text-red { color: var(--red); }
.text-green { color: var(--green); }

code, kbd, pre, .mono {
  font-family: var(--font-mono);
}

ul.plain { list-style: none; margin: 0; padding: 0; }

/* Money / numerals: tabular figures, right-aligned, Plex Mono — everywhere
   an amount, invoice number or GSTIN appears. */
.money, .num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* ---------------------------------------------------------------------
   Focus / accessibility
   ------------------------------------------------------------------- */

:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
  transition: outline-color var(--transition-fast);
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--surface);
  color: var(--ink);
  padding: var(--space-1) var(--space-2);
  z-index: 1000;
  border: 1px solid var(--rule);
}
.skip-link:focus { left: var(--space-1); top: var(--space-1); }

/* ---------------------------------------------------------------------
   Buttons and form controls
   ------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* inline-flex, not inline-block: half these buttons lead with an icon, and
     inline-block leaves it sitting on the text baseline with no gap after it,
     so the mark and its label read as one run of characters. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-weight: 500;
  padding: 7px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.btn svg { flex: none; }
.btn:hover { border-color: var(--ink-soft); text-decoration: none; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

.btn-primary {
  background: var(--amber);
  border-color: var(--amber);
  color: #2b1c00;
}
.btn-primary:hover { background: #e69c1f; border-color: #e69c1f; }

.btn-danger { color: var(--red); border-color: var(--red); background: var(--surface); }
.btn-danger:hover { background: #fbeceb; }

.btn-link {
  border-color: transparent;
  background: transparent;
  color: var(--blue);
  padding: 7px 4px;
}

.btn-sm { padding: 4px 10px; font-size: 0.85rem; }

label {
  display: block;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--ink-soft);
  margin-bottom: var(--space-2);
}

/* Every text-entry type, not a selected few. A type missing from this list
   keeps the browser's default inline box: it does not go display:block or
   width:100%, so its label sits beside it and the field shrinks to a default
   size. That is what tel and url did on the company page. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="search"],
input[type="tel"],
input[type="url"],
select,
textarea {
  display: block;
  width: 100%;
  margin-top: 4px;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

input:focus, select:focus, textarea:focus {
  border-color: var(--amber);
  box-shadow: 0 0 0 3px rgba(243, 171, 51, 0.25);
  outline: none;
}

input[aria-invalid="true"], select[aria-invalid="true"] {
  border-color: var(--red);
}

.field-error {
  display: block;
  color: var(--red);
  font-size: 0.85rem;
  margin-top: 4px;
  font-weight: 400;
}

/* Explanatory text under a field or fieldset — quieter than a label, and
   never carrying information the user cannot get elsewhere. */
.field-hint {
  display: block;
  color: var(--ink-soft);
  font-size: 0.85rem;
  margin: 4px 0 0;
  font-weight: 400;
  line-height: 1.45;
}

.field-hint label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: 8px;
  font-size: inherit;
  font-weight: inherit;
}

.form-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-3);
}

/* ---------------------------------------------------------------------
   Form grid — a field is as wide as what goes in it

   Every input in this app is display:block at width:100%, which is right
   inside a drawer and wrong on a settings page: it made a PAN box and a
   company-name box both 1158px, and a 5-digit port the same. A field that
   wide reads as "type a lot here" and gives the eye no clue what belongs
   in it.

   Twelve columns, and each field declares how many it wants. The span is
   chosen from the longest value the field can actually hold — GSTIN is 15
   characters, a pincode is 6 — so the box itself says how much is
   expected before anyone reads the label.
   ------------------------------------------------------------------- */

.form-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 0 var(--space-2);
  align-items: start;
}

/* Full width unless a field asks for less, so a new field without a span
   is merely wide rather than overlapping its neighbour. */
.form-grid > * { grid-column: span 12; }

/* The whole scale, not only the spans in use today. A missing one falls back
   to the span-12 default above and silently prints a full-width box, which is
   the exact bug this grid exists to fix. */
.form-grid > .span-1 { grid-column: span 1; }
.form-grid > .span-2 { grid-column: span 2; }
.form-grid > .span-3 { grid-column: span 3; }
.form-grid > .span-4 { grid-column: span 4; }
.form-grid > .span-5 { grid-column: span 5; }
.form-grid > .span-6 { grid-column: span 6; }
.form-grid > .span-7 { grid-column: span 7; }
.form-grid > .span-8 { grid-column: span 8; }
.form-grid > .span-9 { grid-column: span 9; }
.form-grid > .span-10 { grid-column: span 10; }
.form-grid > .span-11 { grid-column: span 11; }
.form-grid > .span-12 { grid-column: span 12; }

/* A field's error and hint belong to the field, so they sit inside its
   column rather than becoming grid items that break the row. */
.form-grid label { margin-bottom: var(--space-2); }

/* Settings forms are read, not just filled in. Capping the measure keeps
   a row of three fields from stretching to the width of a wall-mounted
   monitor, and gives the grid a stable size to divide up. */
.settings-form { max-width: 940px; }

/* ---------------------------------------------------------------------
   Image field — signature, stamp, avatar

   Shows the image, not its filename. "signature.png" does not tell anyone
   whether the right signature is loaded, and this page is the last place
   to find out before it prints on an invoice.
   ------------------------------------------------------------------- */

.image-field { margin-bottom: var(--space-2); }

.image-field-label {
  display: block;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--ink-soft);
}

.image-field-body {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: 6px;
}

.image-preview {
  flex: none;
  display: grid;
  place-items: center;
  width: 88px;
  height: 56px;
  padding: 4px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}

.image-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }

/* Dashed while empty: it reads as a slot waiting to be filled rather than a
   box that failed to load something. */
.image-preview.is-empty { border-style: dashed; background: transparent; }
.image-preview-empty { display: grid; place-items: center; color: var(--ink-soft); opacity: 0.45; }

/* An avatar is a face, so it is shown as one: square and round rather than
   letterboxed into the signature's landscape box. */
.image-preview.is-round {
  width: 64px;
  height: 64px;
  border-radius: 999px;
  padding: 0;
}
.image-preview.is-round img { width: 100%; height: 100%; object-fit: cover; }

.image-field-controls { min-width: 0; flex: 1; }
.image-field-controls input[type="file"] { font-size: 0.85rem; max-width: 100%; }
.image-field-controls .field-hint,
.image-field-controls .field-error { margin-top: 6px; }

/* Clear of the file input above it. A checkbox is drawn at 17px, which WCAG
   2.2 allows only while nothing else sits within 12px of its centre — and at
   the old spacing the file input was 9px away, so the two were one crowded
   pair rather than two separate targets. */
.image-field-controls label.checkbox { margin-top: 10px; }

/* The one-press way to fill an empty template list. Sits under the toolbar
   with its explanation beside it rather than under it, so it reads as one
   offer rather than as a second toolbar. */
.standard-set {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 0 0 var(--space-2);
}

.standard-set .field-hint { margin: 0; }

/* A form that is one control, sitting in a table cell. Without this the
   form's own block layout pushes the row taller than its neighbours. */
.inline-form { display: inline; margin: 0; }

/* ---------------------------------------------------------------------
   API log — what went out and what came back

   Collapsed by default: the summary line carries the verdict, the status
   and the time, which is all that is wanted until something is wrong.
   ------------------------------------------------------------------- */

.api-log-entry {
  border-top: 1px solid var(--rule);
  padding: 8px 0;
}

.api-log-entry:first-of-type { border-top: 0; }

.api-log-entry > summary {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-wrap: wrap;
  cursor: pointer;
  list-style: none;
}

.api-log-entry > summary::-webkit-details-marker { display: none; }
.api-log-entry > summary::before { content: '▸'; color: var(--ink-soft); }
.api-log-entry[open] > summary::before { content: '▾'; }

.api-log-body { padding: 8px 0 4px 18px; }
.api-log-body h3 {
  margin: 10px 0 4px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.api-log-body pre {
  margin: 0;
  padding: 10px 12px;
  background: var(--table-head);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  line-height: 1.5;
  /* A gateway's error page is one very long line; it wraps rather than
     stretching the page sideways. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 320px;
  overflow-y: auto;
}

/* ---------------------------------------------------------------------
   GSTIN lookup — the number and the button that fills the form from it
   ------------------------------------------------------------------- */

.gstin-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
}

/* The input takes the room; the button takes what it needs. */
.gstin-row input { flex: 1; min-width: 0; }
.gstin-row .btn { flex: none; margin-top: 4px; }

/*
 * A GSTIN and a PAN are codes, not words: they are read one character at a
 * time and checked against a certificate. Monospaced so the columns line up
 * when two are compared, and so 0/O and 1/I are told apart — the whole reason
 * anybody looks at these closely. Spaced slightly, because 15 unbroken
 * characters are hard to hold a place in.
 */
input.code-input {
  font-family: var(--font-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  /* 15 characters, their letter-spacing, and the box's own padding. Set as a
     minimum so the field cannot be squeezed below one whole GSTIN. */
  min-width: calc(15ch + (15 * 0.04em) + 26px);
}

/*
 * A field the form works out for itself. Deliberately not `disabled`: the
 * value still posts, still copies, and still reads at full contrast — it is
 * settled, not unavailable, and greying it out would say the wrong thing.
 */
input.derived-input {
  background: var(--table-head);
  cursor: default;
}

input.derived-input:focus { outline: none; box-shadow: none; border-color: var(--rule); }

/* The status line under the field says what happened — which fields were
   filled, which were left as typed, whether the registration is active. It
   turns red only for something that needs acting on. */
.gstin-field .field-hint.is-bad { color: var(--red); }

/*
 * The heading above each group on the party review page: what is wrong, why
 * it matters, and what to do about it. Sits on the table's own header shade
 * so it reads as the key to the rows under it rather than as a card of its
 * own — the two belong together and a gap between them would break that.
 */
.review-head {
  padding: 14px 16px;
  border-bottom: 1px solid var(--rule);
  background: var(--table-head);
  display: grid;
  gap: 4px;
}

.review-head h2 {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: 15px;
}

/* The "what to do" line is the one somebody acts on, so it is not greyed out
   the way the explanation above it is. */
.review-head p:last-child { color: var(--ink); }

/* ---------------------------------------------------------------------
   Quick create — a party or a product without leaving the document

   A panel over the form rather than a page instead of it: the invoice
   underneath is half typed, and the whole point is that it survives.
   Narrower than the drawer because it holds five fields, and above it
   because it usually opens from inside it.
   ------------------------------------------------------------------- */

.quick-scrim {
  position: fixed;
  inset: 0;
  background: rgba(30, 58, 95, 0.28);
  z-index: 80;
}

/* Built like .drawer, because it is one — the same panel from the same edge,
   so a second form does not arrive as a different kind of object. Narrower
   than the document drawer it opens over, and above it, which is what says
   which of the two is on top. */
.quick-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(620px, 100vw);
  display: flex;
  flex-direction: column;
  background: var(--paper);
  box-shadow: var(--shadow-drawer);
  z-index: 81;
}

.quick-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 12px 16px;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  flex: none;
}

.quick-head h2 { margin: 0; font-size: 1.05rem; }

.quick-body {
  padding: var(--space-3);
  overflow-y: auto;
  flex: 1;
}

/* The master's form is written for a page, where a card draws its own frame.
   In a drawer the drawer is the frame, so the inner card would be a box in a
   box. */
.quick-body > .card { border: 0; box-shadow: none; padding: 0; background: transparent; }

.quick-body .form-actions { margin-top: var(--space-3); }

/* The page behind does not scroll while the panel is up: scrolling it moves
   the form out from under a panel that is pinned to the viewport. */
body.quick-open { overflow: hidden; }

/* The way out of "No matches", sitting under the results it belongs to. */
.autocomplete-create {
  display: block;
  width: 100%;
  padding: 8px 10px;
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: var(--table-head);
  border: 0;
  border-top: 1px solid var(--rule);
  cursor: pointer;
}

.autocomplete-create:hover { background: var(--surface); }
.autocomplete-create .ac-main { font-weight: 600; color: var(--amber-ink); }

/* ---------------------------------------------------------------------
   Email design picker — the four letterheads, side by side

   Rendered rather than described: "Bordered card" and "Plain letter" mean
   nothing until the two are next to each other.
   ------------------------------------------------------------------- */

.design-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  gap: var(--space-2);
}

.design-card {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

/* The one in use is marked with the accent, not merely labelled: the point of
   this page is telling four things apart at a glance. */
.design-card.is-current { border-color: var(--amber); box-shadow: 0 0 0 1px var(--amber); }

.design-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 12px 14px;
  border-bottom: 1px solid var(--rule);
}

.design-head form { margin: 0; flex: none; }
.design-head .badge { margin-left: 6px; }
.design-head strong { font-size: 0.95rem; }

.design-frame {
  display: block;
  width: 100%;
  height: 560px;
  border: 0;
  background: var(--paper);
}

@media (max-width: 900px) {
  .design-grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------
   Mail preview — the template as a finished message
   ------------------------------------------------------------------- */

.mail-preview {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.mail-preview-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 12px 16px;
  border-bottom: 1px solid var(--rule);
  background: var(--table-head);
}

.mail-preview-label {
  flex: none;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* Tall enough for a whole short mail without scrolling, which is the length
   these templates should be. */
.mail-preview-frame {
  display: block;
  width: 100%;
  height: 720px;
  border: 0;
  background: var(--paper);
}

/* ---------------------------------------------------------------------
   Placeholder chips — the tokens a mail template may use

   Each carries what it resolves to, not just its name. "{{outstanding}}"
   alone is a guess; "what is still owed on it" is not, and this is the
   only place anyone would find out before a customer reads the result.
   ------------------------------------------------------------------- */

.placeholder-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 6px;
}

.placeholder-chip {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  width: 100%;
  padding: 6px 9px;
  text-align: left;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

/* ---------------------------------------------------------------------
   HTML editor — the mail bodies this app sends

   A body is HTML, and it used to be edited as source: writing a greeting
   meant typing the paragraph tags around it. The words are the content;
   the markup is an implementation detail, available behind Source for the
   times it genuinely matters.
   ------------------------------------------------------------------- */

.html-editor {
  margin-top: 4px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}

.html-editor:focus-within {
  border-color: var(--amber);
  box-shadow: 0 0 0 3px rgba(243, 171, 51, 0.25);
}

.html-editor-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 5px 6px;
  border-bottom: 1px solid var(--rule);
  background: var(--table-head);
}

.html-editor-btn {
  padding: 3px 8px;
  font: inherit;
  font-size: 0.82rem;
  color: var(--ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}

.html-editor-btn:hover:not(:disabled) { background: var(--surface); border-color: var(--rule); }
.html-editor-btn:disabled { opacity: 0.4; cursor: default; }
.html-editor-btn.is-bold { font-weight: 700; }
.html-editor-btn.is-italic { font-style: italic; }

/* Pushed to the end: it switches what the panel is, rather than acting on
   what is in it, so it does not belong among the formatting buttons. */
.html-editor-toggle { margin-left: auto; }
.html-editor-toggle[aria-pressed="true"] { background: var(--surface); border-color: var(--rule); font-weight: 600; }

.html-editor-body {
  min-height: 180px;
  max-height: 460px;
  overflow-y: auto;
  padding: 10px 12px;
  line-height: 1.5;
  outline: none;
}

/* The body renders as the mail will read, so the first paragraph has no
   leading gap and the last no trailing one. */
.html-editor-body > *:first-child { margin-top: 0; }
.html-editor-body > *:last-child { margin-bottom: 0; }
.html-editor-body p { margin: 0 0 0.7em; }
.html-editor-body h2 { font-size: 1.15rem; margin: 0 0 0.5em; }
.html-editor-body h3 { font-size: 1rem; margin: 0 0 0.5em; }
.html-editor-body ul, .html-editor-body ol { margin: 0 0 0.7em; padding-left: 1.4em; }
.html-editor-body a { color: var(--blue); }
.html-editor-body blockquote {
  margin: 0 0 0.7em;
  padding-left: 10px;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
}

/* Source view: the same box, monospaced, with no border of its own since the
   wrapper already draws one. */
.html-editor .html-editor-source {
  display: block;
  width: 100%;
  min-height: 180px;
  margin: 0;
  padding: 10px 12px;
  font-family: var(--font-mono);
  font-size: 0.88rem;
  line-height: 1.5;
  border: 0;
  border-radius: 0;
  resize: vertical;
}

.html-editor .html-editor-source:focus { box-shadow: none; }

.html-editor-link {
  display: flex;
  gap: 6px;
  align-items: center;
  padding: 6px;
  border-top: 1px solid var(--rule);
  background: var(--table-head);
}

.html-editor-link input {
  flex: 1;
  min-width: 0;
  margin-top: 0;
  font-size: 0.85rem;
}

.placeholder-chip:hover { border-color: var(--amber); background: var(--table-head); }
.placeholder-chip:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
.placeholder-chip .mono { font-size: 0.82rem; color: var(--amber-ink); }
.placeholder-meaning { font-size: 0.78rem; color: var(--ink-soft); line-height: 1.3; }

@media (max-width: 900px) {
  /* Below this the twelve columns are too fine to be worth keeping: a
     span-2 would be under 60px. Two halves and full rows read better. */
  .form-grid > .span-1,
  .form-grid > .span-2,
  .form-grid > .span-3,
  .form-grid > .span-4,
  .form-grid > .span-5 { grid-column: span 6; }
  .form-grid > .span-6,
  .form-grid > .span-7,
  .form-grid > .span-8,
  .form-grid > .span-9,
  .form-grid > .span-10,
  .form-grid > .span-11 { grid-column: span 12; }
}

fieldset {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-2);
  margin: 0 0 var(--space-3);
}
legend {
  font-weight: 600;
  padding: 0 6px;
}

/* ---------------------------------------------------------------------
   Cards, tables, badges, alerts
   ------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: var(--space-3);
}

table {
  width: 100%;
  border-collapse: collapse;
}
th, td {
  text-align: left;
  padding: 0 var(--space-2);
  height: var(--row-height);
  border-bottom: 1px solid var(--rule);
}
th {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  font-weight: 600;
}

/* The header row is tinted so it reads as the key to the table rather than
   as its first entry. Paper, not a grey: the same warm neutral the page sits
   on, one step back from the white the rows are on. */
thead th {
  background: var(--table-head);
  border-bottom: 1px solid var(--rule);
}
thead th:first-child { border-top-left-radius: var(--radius); }
thead th:last-child { border-top-right-radius: var(--radius); }
tbody tr:hover { background: rgba(243, 171, 51, 0.06); }

.badge {
  display: inline-block;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--ink-soft);
  white-space: nowrap;
}
.badge-amber { background: #FEF3E0; border-color: #F3D9A3; color: var(--amber-ink); }
.badge-blue { background: #EAF1FE; border-color: #C6D9FC; color: #1D4E9B; }
.badge-green { background: #E7F5EC; border-color: #B9E1C7; color: var(--green); }
.badge-red { background: #FBEAE8; border-color: #F0C0BA; color: var(--red); }

.alert {
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  margin-bottom: var(--space-3);
  border: 1px solid var(--rule);
  background: var(--surface);
}
.alert ul { margin: 0; padding-left: 1.1em; }
.alert-error { border-color: #F0C0BA; background: #FBEAE8; color: #7A2018; }
.alert-success { border-color: #B9E1C7; background: #E7F5EC; color: #14532F; }
.alert-info { border-color: #C6D9FC; background: #EAF1FE; color: #1D4E9B; }

/* ---------------------------------------------------------------------
   App shell — fixed sidebar, top bar, main content
   ------------------------------------------------------------------- */

.app-shell {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--sidebar-width);
  background: var(--nav-bg);
  display: flex;
  flex-direction: column;
  z-index: 10;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

/* The wordmark is drawn in dark ink with two coloured dots. brightness(0)
   flattens all of it to black, invert(1) lifts that to white — one filter,
   no second asset to keep in step with the first. */
.sidebar-brand img {
  height: 40px;
  width: auto;
  display: block;
  filter: brightness(0) invert(1);
}

.sidebar-brand .wordmark {
  font-weight: 600;
  font-size: 1.35rem;
  color: var(--nav-ink-bright);
  letter-spacing: -0.01em;
}

.sidebar-nav { flex: 1; padding: var(--space-2) 0; overflow-y: auto; }
.sidebar-nav a, .sidebar-nav .nav-disabled {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px var(--space-2);
  color: var(--nav-ink-bright);
  font-weight: 500;
  border-left: 3px solid transparent;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.sidebar-nav a:hover { background: var(--nav-bg-soft); color: var(--nav-ink-bright); text-decoration: none; }
.sidebar-nav a.active {
  border-left-color: var(--amber);
  background: var(--nav-bg-soft);
  color: var(--nav-ink-bright);
}
.sidebar-nav a svg { color: var(--nav-ink-bright); transition: color var(--transition-fast); }
.sidebar-nav .nav-disabled svg { color: var(--nav-ink-soft); }
.sidebar-nav a:hover svg { color: var(--nav-ink-bright); }
.sidebar-nav a.active svg { color: var(--amber); }

/*
 * A nav section that opens in place: Reports, with every report under it.
 *
 * The summary is styled to match a plain nav link exactly, because it is one
 * as far as the reader is concerned — the only difference is that it reveals
 * a list instead of navigating.
 */
.nav-section > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px var(--space-2);
  color: var(--nav-ink-bright);
  font-weight: 500;
  border-left: 3px solid transparent;
  cursor: pointer;
  list-style: none;
  transition: background var(--transition-fast), color var(--transition-fast);
}

/* The default disclosure triangle, in both engines that draw one. */
.nav-section > summary::-webkit-details-marker { display: none; }
.nav-section > summary::marker { content: ''; }

.nav-section > summary:hover { background: var(--nav-bg-soft); }
.nav-section > summary svg { color: var(--nav-ink-bright); }

/* The chevron sits at the far end and turns to point up when open. */
.nav-section > summary svg:last-child {
  margin-left: auto;
  transition: transform var(--transition-fast);
}
.nav-section[open] > summary svg:last-child { transform: rotate(180deg); }

/*
 * The reports themselves. Indented to the width of the icon above them so the
 * titles line up with the section's label rather than with its icon, and set
 * a step smaller: this is a list within a place, not a list of places.
 */
.nav-sublist a {
  padding: 7px var(--space-2) 7px 44px;
  font-size: 0.86rem;
  font-weight: 400;
  color: var(--nav-ink-soft);
}
.nav-sublist a:hover { color: var(--nav-ink-bright); }
.nav-sublist a.active {
  color: var(--nav-ink-bright);
  border-left-color: var(--amber);
  background: var(--nav-bg-soft);
  font-weight: 500;
}

.nav-group {
  padding: var(--space-3) var(--space-2) 6px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--nav-ink-soft);
  font-weight: 600;
}

.sidebar-nav .nav-disabled { color: var(--nav-ink-soft); cursor: default; }
.sidebar-nav .nav-soon {
  margin-left: auto;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--nav-ink-soft);
  background: rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  padding: 1px 6px;
}

.main {
  margin-left: var(--sidebar-width);
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.topbar {
  height: var(--topbar-height);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-3);
  gap: var(--space-3);
  position: sticky;
  top: 0;
  z-index: 5;
}

.fy-select {
  display: flex;
  align-items: center;
  gap: 8px;
}
.fy-select label { margin: 0; }
.fy-select select { width: auto; margin: 0; padding: 5px 8px; }

.topbar-user {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.topbar-user .user-name { font-weight: 500; }
.topbar-user .user-role { color: var(--ink-soft); font-size: 0.8rem; }

.content {
  padding: var(--space-3);
  flex: 1;
}

.page-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--space-3);
  gap: var(--space-2);
}

/* ---------------------------------------------------------------------
   KPI tiles — honest empty states, no fabricated numbers
   ------------------------------------------------------------------- */

/* ---------------------------------------------------------------------
   Totals ladder — mirrors the printed invoice exactly (rows, order, labels)
   ------------------------------------------------------------------- */

.totals-ladder { width: 100%; max-width: 360px; margin-left: auto; }
.totals-ladder .row {
  display: flex;
  justify-content: space-between;
  padding: 6px 0;
  border-bottom: 1px solid var(--rule);
}
.totals-ladder .row .label { color: var(--ink-soft); }
.totals-ladder .row .value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.totals-ladder .row.total {
  border-bottom: none;
  border-top: 2px solid var(--ink);
  font-weight: 600;
  font-size: 1.05rem;
  padding-top: 10px;
}

/* ---------------------------------------------------------------------
   Guest shell — login, install, 403/404/500 (centered card, no sidebar)
   ------------------------------------------------------------------- */

.install-body, .guest-body {
  margin: 0;
  min-height: 100vh;
  background: var(--paper);
}

/* ---------------------------------------------------------------------
   Signed-out screens: brand panel left, form right
   ------------------------------------------------------------------- */

.guest-split { display: flex; min-height: 100vh; }

.guest-aside {
  flex: 0 0 60%;
  background: linear-gradient(155deg, oklch(0.36 0.055 252) 0%, oklch(0.30 0.055 252) 55%, oklch(0.25 0.05 252) 100%);
  color: var(--nav-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4) var(--space-5);
  position: relative;
  overflow: hidden;
}

/* One soft warm highlight so the panel is not a flat slab, placed behind the
   documents so they read as lit from the same side. */
.guest-aside::before {
  content: "";
  position: absolute;
  width: 720px;
  height: 720px;
  border-radius: 50%;
  background: radial-gradient(circle, oklch(0.79 0.145 72 / 0.18) 0%, oklch(0.79 0.145 72 / 0) 66%);
  top: -240px;
  right: -220px;
}

/* Two columns: the words, then the documents running the full height beside
   them. Both cells centre on the same axis, so the drawing reads as sitting
   with the text rather than under it. */
.guest-aside-inner {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-4);
  align-items: center;
  width: 100%;
  max-width: 860px;
  max-height: 100%;
}

.guest-aside-copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.guest-aside-brand img { height: 32px; filter: brightness(0) invert(1); }
.guest-aside-brand .wordmark { color: var(--nav-ink-bright); font-size: 1.4rem; font-weight: 600; }

.guest-aside-lede h2 {
  color: var(--nav-ink-bright);
  font-size: 1.7rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
  margin: 0 0 8px;
}
.guest-aside-lede p { color: var(--nav-ink-soft); margin: 0; font-size: 1rem; }

.guest-art-wrap { display: flex; align-items: center; justify-content: center; min-width: 0; height: 100%; }
.guest-art { width: 100%; height: 100%; max-height: min(86vh, 640px); display: block; object-fit: contain; }

/* Facts, not claims. Each line names something the software does, so the
   panel earns its space instead of decorating it. */
.guest-points {
  list-style: none;
  margin: 0;
  padding: var(--space-3) 0 0;
  border-top: 1px solid oklch(0.99 0.005 252 / 0.14);
  display: grid;
  gap: 10px;
}
.guest-points li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  color: var(--nav-ink);
  font-size: 0.88rem;
  line-height: 1.4;
}
.guest-points svg { color: var(--amber); margin-top: 1px; }

/* When the banner is too narrow for two columns, or too short for the
   drawing, the words win: the capability list says more than a picture of
   it. */
@media (max-width: 1180px), (max-height: 660px) {
  .guest-aside-inner { grid-template-columns: minmax(0, 1fr); max-width: 460px; }
  .guest-art-wrap { display: none; }
}

.guest-panel {
  flex: 0 0 40%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: var(--space-5) var(--space-4);
  background: var(--surface);
}

.guest-panel-inner { width: 100%; max-width: 360px; }

.guest-brand { display: none; margin-bottom: var(--space-3); }
.guest-brand img { height: 28px; }
.guest-brand .wordmark { font-size: 1.3rem; font-weight: 600; }

.guest-title { margin-bottom: 4px; font-size: 1.5rem; }
.guest-lede { color: var(--ink-soft); margin-bottom: var(--space-3); }

.guest-foot { margin: var(--space-4) 0 0; font-size: 0.78rem; color: var(--ink-soft); }

/* Below 900px the brand panel goes entirely: on a phone it would be a
   screenful of decoration between the reader and the password box. The
   wordmark returns above the form so the page is still identifiably ours. */
@media (max-width: 900px) {
  .guest-aside { display: none; }
  .guest-panel { flex: 1 1 100%; justify-content: flex-start; padding-top: var(--space-5); }
  .guest-brand { display: flex; }
}

.install-shell { width: 100%; display: flex; justify-content: center; padding: var(--space-4); }
.install-card {
  width: 100%;
  max-width: 520px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-4);
  box-shadow: var(--shadow-card);
}
.install-title { text-align: center; margin-bottom: var(--space-3); }

.guest-form .form-actions { justify-content: stretch; }
.guest-form .btn-primary { width: 100%; justify-content: center; }

/* ---------------------------------------------------------------------
   Error pages

   One column, centred on both axes. The sign-in split layout was doing this
   job and giving six tenths of the window to a panel selling the product,
   which pushed the apology into a narrow column against the right edge.
   ------------------------------------------------------------------- */

.error-body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
}

.error-shell {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4) var(--space-3);
}

/* Everything inside is centred, so the block reads as one thing rather than
   as a stack of separately-aligned pieces. */
.error-inner {
  width: 100%;
  max-width: 440px;
  text-align: center;
}

.error-brand { display: flex; justify-content: center; margin-bottom: var(--space-4); }
.error-brand img { height: 26px; }
.error-brand .wordmark { font-size: 1.3rem; font-weight: 600; }

.error-title { font-size: 1.5rem; margin: 0 0 var(--space-1); }
.error-message { color: var(--ink-soft); margin: 0 0 var(--space-3); }
.error-action { margin: 0; }

.error-foot {
  margin: 0;
  padding: var(--space-3);
  text-align: center;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

.error-code {
  font-family: var(--font-mono);
  font-size: 3rem;
  font-weight: 600;
  line-height: 1;
  text-align: center;
  color: var(--ink-soft);
  margin-bottom: var(--space-2);
}

/* ---------------------------------------------------------------------
   Utility
   ------------------------------------------------------------------- */

.flex { display: flex; }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--space-2); }

@media (max-width: 860px) {
  .sidebar { transform: translateX(-100%); }
  .main { margin-left: 0; }
  .install-grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------
   Billing form — party/product type-ahead, line-item grid
   ------------------------------------------------------------------- */

.autocomplete-wrap { position: relative; }

.autocomplete-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 20;
  margin-top: 2px;
  max-height: 260px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
}
.autocomplete-results[hidden] { display: none; }

.autocomplete-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}
.autocomplete-item:last-child { border-bottom: 0; }
.autocomplete-item:hover, .autocomplete-item.is-active { background: rgba(243, 171, 51, 0.1); }
.autocomplete-item .ac-main { font-weight: 500; }
.autocomplete-item .ac-sub { color: var(--ink-soft); font-size: 0.8rem; }
.autocomplete-empty { padding: 8px 10px; color: var(--ink-soft); font-size: 0.85rem; }

.line-items { display: flex; flex-direction: column; gap: var(--space-2); }

.line-item {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-2);
  background: var(--surface);
}

.line-item-grid {
  display: grid;
  grid-template-columns: 2.2fr 1fr 0.7fr 0.9fr 0.7fr 0.8fr 1fr auto;
  gap: 0 var(--space-2);
  align-items: start;
}
.li-field label {
  margin-bottom: 4px;
}
.li-field input { margin-top: 0; }
.li-total { display: flex; flex-direction: column; }
.li-total-value {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}
/* Every control on the line is one height, so the row reads as a single band
   and the remove button centres against the fields it acts on rather than
   floating between them and their labels. */
.line-item-grid input,
.line-item-grid select,
.li-total-value,
.li-remove-btn { height: var(--control-height); }

.li-remove { display: flex; flex-direction: column; }
.li-remove label { margin-bottom: 4px; min-height: 1.15em; }
.li-remove-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  padding: 0;
  font-size: 1.15rem;
  line-height: 1;
}
.li-description { margin-top: var(--space-2); }
.li-description label { margin-bottom: 4px; }
.li-description textarea { margin-top: 0; }
.li-error { min-height: 0; }

.line-items-actions { margin-top: var(--space-2); }

/* ---------------------------------------------------------------------
   Form field rows

   Fields that are read together sit on one line. The columns are weighted
   by what goes in them, not split evenly: a company name needs room, a
   three-letter currency code does not.
   ------------------------------------------------------------------- */

.field-row {
  display: grid;
  gap: var(--space-2);
  align-items: start;
}
.field-row > div { min-width: 0; }
.field-row label { margin-bottom: 4px; }
.field-row input,
.field-row select { margin-top: 0; }

.field-row-party { grid-template-columns: minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1fr); }
.field-row-one { grid-template-columns: minmax(0, 1fr); }
.field-row-two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.field-row-three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.field-row-four { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* GSTIN, PAN, country, state. The first two are fixed-length codes and are
   given the room to show all of themselves — min-content, so the GSTIN cell
   cannot be squeezed below one whole number plus its Fetch button. The two
   selects share what is left. */
.field-row-party-tax {
  grid-template-columns: minmax(min-content, 1.7fr) minmax(min-content, 1fr) minmax(0, 1.15fr) minmax(0, 1.15fr);
}

/* A drawer is narrower than a page, so four across becomes two rather than
   four columns of nothing. */
@media (max-width: 1100px) {
  .field-row-four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .field-row-party-tax { grid-template-columns: minmax(min-content, 1.4fr) minmax(min-content, 1fr); }
}
.field-row-details { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.field-row-discount { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-width: 420px; }

/* Sits where an input would, so the row's controls keep one baseline even
   though this one is a readout rather than something to fill in. */
.field-badge {
  display: inline-flex;
  align-items: center;
  height: var(--control-height);
  padding: 0 12px;
}

@media (max-width: 860px) {
  .field-row-party, .field-row-details, .field-row-party-tax,
  .field-row-two, .field-row-three, .field-row-four { grid-template-columns: 1fr; }
  /* One per row now, so the code fields no longer need to hold a floor and
     must not force the page wider than the phone it is on. */
  input.code-input { min-width: 0; }
}

/* ---------------------------------------------------------------------
   Discount and totals

   One card. The discount is an input to the ladder, and reading a figure
   in one card that is only explained by a control in another is what made
   this two in the first place.
   ------------------------------------------------------------------- */

.settlement-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  gap: var(--space-3);
  align-items: start;
}
.settlement-card h2 { margin-top: 0; }
.settlement-card .totals-ladder { max-width: none; }

@media (max-width: 900px) {
  .settlement-card { grid-template-columns: 1fr; }
  .settlement-card .totals-ladder { max-width: 380px; margin-left: 0; }
}

@media (max-width: 1100px) {
  .line-item-grid { grid-template-columns: 1fr 1fr; }
  .li-remove { justify-content: flex-end; }
}

/* Reports: the list of available reports, and the month bars. */
.report-list {
  list-style: none;
  padding: 0;
  margin: 8px 0 0;
}

.report-list li {
  padding: 10px 0;
  border-bottom: 1px solid var(--rule, #d8d5cf);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.report-list li:last-child { border-bottom: 0; }

.bar-track {
  background: var(--rule, #d8d5cf);
  border-radius: 2px;
  height: 8px;
  overflow: hidden;
}

.bar-fill {
  background: var(--amber, #f3ab33);
  height: 100%;
  border-radius: 2px;
}

/* Section sub-navigation, e.g. the Config tabs. */
.subnav {
  display: flex;
  gap: var(--space-2);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--space-3);
}

.subnav-item {
  padding: 8px 2px;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  font-weight: 500;
}

.subnav-item:hover { color: var(--ink); text-decoration: none; }

.subnav-item.is-active {
  color: var(--ink);
  border-bottom-color: var(--amber);
}

/* ---------------------------------------------------------------------
   Icons

   One sprite, referenced by <use>. Icons take currentColor, so they
   inherit whatever the text beside them is doing and need no per-context
   variant. They are decorative throughout — every one has a text label —
   so none of them carries meaning on its own.
   ------------------------------------------------------------------- */

svg { flex: none; }

/* A badge carrying an icon keeps it optically centred against the cap height
   rather than the line box. */
.badge svg {
  margin-right: 4px;
  vertical-align: -2px;
}

.badge { display: inline-flex; align-items: center; }


/* Empty states: a large muted mark above a sentence explaining what would
   appear here, rather than a bare "no rows" in a table cell. */
.empty-state {
  text-align: center;
  padding: var(--space-5) var(--space-2);
  color: var(--ink-soft);
}

/* The direct child only: this is the illustration above the message. Without
   the child combinator it also caught the icon inside the button below, which
   took the illustration's pale grey and its 16px of bottom margin — so the
   mark sat above its own label in a colour you could barely see. */
.empty-state > svg {
  color: var(--rule);
  margin-bottom: var(--space-2);
}

.empty-state h2 {
  font-size: 1.05rem;
  color: var(--ink);
  margin-bottom: 4px;
}

.empty-state p { margin: 0 auto; max-width: 42ch; }

.empty-state .form-actions { justify-content: center; }

/* Page-header action groups wrap on a narrow window instead of overflowing. */
.page-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }

/* ---------------------------------------------------------------------
   Topbar: breadcrumb left, financial year and account right
   ------------------------------------------------------------------- */

.breadcrumb {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.95rem;
  min-width: 0;
}
.breadcrumb a { color: var(--ink-soft); font-weight: 500; }
.breadcrumb a:hover { color: var(--ink); }
.breadcrumb .crumb-sep { color: var(--rule); }
.breadcrumb [aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-right { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }

.fy-select { display: flex; align-items: center; gap: 6px; margin: 0; }
.fy-select label { margin: 0; font-size: 0.8rem; color: var(--ink-soft); }
.fy-select select { margin-top: 0; padding: 5px 8px; width: auto; }

/* Account menu. <details> carries the open/close state, so it works with
   JavaScript disabled; app.js only adds click-outside and Escape. */
.user-menu { position: relative; }
.user-menu summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 4px;
  border-radius: 999px;
  cursor: pointer;
  list-style: none;
  border: 1px solid transparent;
}
.user-menu summary::-webkit-details-marker { display: none; }
.user-menu summary:hover { background: var(--paper); border-color: var(--rule); }
.user-menu[open] summary { background: var(--paper); border-color: var(--rule); }

.avatar {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--nav-bg);
  color: var(--nav-ink-bright);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* The uploaded picture, in the same 28px circle the initials occupy, so the
   top bar does not shift when one is added. cover, because a photo that is
   not square would otherwise letterbox inside the circle. */
.avatar-img { object-fit: cover; background: var(--rule); }

.user-menu .user-name { font-weight: 500; font-size: 0.9rem; }

.user-menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  min-width: 232px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(51, 51, 51, 0.12);
  padding: 6px;
  z-index: 40;
}
.user-menu-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  padding: 8px 10px 10px;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 6px;
}
.user-menu-panel a,
.user-menu-panel button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit;
  font-weight: 500;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
}
.user-menu-panel a:hover,
.user-menu-panel button:hover { background: var(--paper); text-decoration: none; }
.user-menu-panel svg { color: var(--ink-soft); }
.user-menu-panel form { margin: 0; }

/* ---------------------------------------------------------------------
   Footer — full width under the content, not inside it
   ------------------------------------------------------------------- */

.app-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: 10px var(--space-3);
  border-top: 1px solid var(--rule);
  background: var(--surface);
  color: var(--ink-soft);
  font-size: 0.8rem;

  /* Pinned like the topbar, so the two frame the content from both sides
     rather than one staying put while the other scrolls away. Sticky rather
     than fixed: it still participates in layout, so a short page puts it at
     the bottom of the content instead of floating over it. */
  position: sticky;
  bottom: 0;
  z-index: 5;
}

/* The shell is a column so the footer can sit at the bottom of short pages
   without floating over a half-empty screen. */
.main { display: flex; flex-direction: column; min-height: 100vh; }
.content { flex: 1; }

/* ---------------------------------------------------------------------
   Right-side drawer

   Forms open here rather than on their own page, so the list behind stays
   in view. Wide by default because the forms in this app are not simple —
   an invoice has a line-item grid — and a phone-width panel would make
   them unusable.
   ------------------------------------------------------------------- */

.drawer-scrim {
  position: fixed;
  inset: 0;
  background: rgba(30, 58, 95, 0.28);
  z-index: 50;
  opacity: 0;
  transition: opacity var(--transition-fast);
}
.drawer-scrim.is-open { opacity: 1; }

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(560px, 100vw);
  background: var(--paper);
  box-shadow: var(--shadow-drawer);
  z-index: 60;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 180ms ease;
}
.drawer.is-open { transform: translateX(0); }

/* An invoice or quotation form needs the width; a party or payment form does
   not. The opening link says which it is. */
/* Wide is for the documents: a line-item row carries eight controls and
   needs the width. Everything else is a short form, and a short form in a
   1100px panel is a column of inputs with half a screen of nothing beside it. */
.drawer.is-wide { width: min(900px, 100vw); }

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
.drawer-head h2 { margin: 0; font-size: 1.1rem; }
.drawer-close { font-size: 1.2rem; line-height: 1; padding: 2px 10px; }

.drawer-body { flex: 1; overflow-y: auto; padding: var(--space-3); }
.drawer-body .page-header { display: none; }

@media (prefers-reduced-motion: reduce) {
  .drawer, .drawer-scrim { transition: none; }
}

/* ---------------------------------------------------------------------
   List toolbar: search, filters and the primary action on one line
   ------------------------------------------------------------------- */

.list-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}

.list-toolbar form {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-wrap: wrap;
  margin: 0;
  flex: 1;
  min-width: 0;
}

/* One height for everything on the line, buttons included. Without it a
   button takes its height from its own padding and sits a few pixels short
   of the field beside it, which reads as misalignment rather than as two
   different kinds of control. */
.list-toolbar input[type="search"],
.list-toolbar select,
.list-toolbar input[type="date"],
.list-toolbar .btn {
  margin-top: 0;
  width: auto;
  height: var(--control-height);
  padding-top: 0;
  padding-bottom: 0;
  line-height: 1;
}
.list-toolbar select { padding-right: 8px; }

/*
 * A select is as wide as its longest option, and the options here are party
 * names somebody else chose — "KAKUBHAI PARIKH EDUCATIONAL SCHOOLS PRIVATE
 * LIMITED" is 51 characters. On a toolbar that deliberately does not wrap,
 * one of those pushes the PDF/CSV/Send buttons off the end of the page.
 * Capped, and truncated: the picked value still reads, and the full name is
 * there the moment the list is opened.
 */
.list-toolbar select {
  max-width: 260px;
  text-overflow: ellipsis;
}

/* The two date boxes of a custom period, which travel together and so are
   shown and hidden together. */
.period-dates { display: inline-flex; gap: var(--space-1); align-items: center; }
.period-dates[hidden] { display: none; }

/* Nothing wraps to a second line: the group is short enough to fit, and a
   toolbar that reflows as you type is worse than one that scrolls. */
.list-toolbar form { flex-wrap: nowrap; }

/* Below a laptop that stops being true. A party name, two dates and three
   buttons do not fit across a tablet, and holding the line there does not
   keep the toolbar tidy — it pushes Send off the edge of the page. Wrapping
   is the lesser of the two. */
@media (max-width: 1100px) {
  .list-toolbar form { flex-wrap: wrap; }
}
.list-toolbar .search-field { position: relative; flex: 1; min-width: 160px; max-width: 340px; }
.list-toolbar .search-field input { width: 100%; padding-left: 32px; }
.list-toolbar .search-field svg {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-soft);
  pointer-events: none;
}

.list-toolbar .toolbar-end { margin-left: auto; display: flex; gap: var(--space-1); }

/* ---------------------------------------------------------------------
   Data tables

   A row is selectable, and the row's actions live in the last column so
   the reader never has to open a record to do the obvious thing to it.
   ------------------------------------------------------------------- */

.data-table tbody tr { transition: background var(--transition-fast); }
.data-table tbody tr:hover { background: rgba(243, 171, 51, 0.05); }
.data-table tbody tr.is-selected { background: rgba(80, 141, 238, 0.08); }

.data-table td, .data-table th { vertical-align: middle; }

/*
 * The totals line, inside the table.
 *
 * A total sitting under its own column is the only place a reader can check
 * that the column adds up without matching a label to a heading first. It is
 * set apart from the rows above by a rule and by weight rather than by a fill,
 * so it still reads as part of the same table and not as a second header.
 */
.data-table tfoot td {
  font-weight: 600;
  color: var(--ink);
  border-top: 2px solid var(--ink);
  background: transparent;
}
.data-table tfoot tr:hover { background: transparent; }

/*
 * A row is one line.
 *
 * A long company name wrapping to two lines makes that row twice as tall as
 * its neighbours, and a list of documents then reads as a ragged column of
 * different-sized blocks rather than as a table you can run your eye down.
 * The wrapper already scrolls sideways, so nothing is lost — and the cell
 * below keeps the full text for anything that does not fit.
 */
.data-table td, .data-table th { white-space: nowrap; }

/*
 * Except a cell that deliberately holds prose: the "what to do" lines on the
 * party review, and a description under an item. Those are meant to wrap.
 */
.data-table td.wrap, .data-table td .text-soft.text-sm { white-space: normal; }

.row-select { width: 34px; text-align: center; }
.row-select input { margin: 0; width: auto; display: inline-block; }

/* Actions stay visible rather than appearing on hover: a control that only
   exists when the pointer is over it cannot be found by anyone navigating
   with a keyboard, and is invisible on a touch screen. */
.row-actions {
  display: flex;
  gap: 2px;
  justify-content: flex-end;
  white-space: nowrap;
}

.row-action {
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
  padding: 0;
}
.row-action:hover {
  background: var(--surface);
  border-color: var(--rule);
  color: var(--ink);
  text-decoration: none;
}
.row-action.is-danger:hover { color: var(--red); border-color: var(--red); }
.row-action[aria-disabled="true"] { opacity: 0.3; pointer-events: none; }
.row-action form { margin: 0; display: contents; }

/* Pagination */
.pagination {
  display: flex;
  gap: var(--space-1);
  justify-content: space-between;
  align-items: center;
  margin-top: var(--space-2);
  flex-wrap: wrap;
}
.pagination .pages { display: flex; gap: 4px; align-items: center; }
.pagination .page-link {
  min-width: 30px;
  height: 30px;
  display: inline-grid;
  place-items: center;
  padding: 0 8px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.pagination .page-link:hover { border-color: var(--ink-soft); text-decoration: none; }
.pagination .page-link.is-current {
  background: var(--nav-bg);
  border-color: var(--nav-bg);
  color: var(--nav-ink-bright);
  font-weight: 600;
}
.pagination .page-gap { color: var(--ink-soft); padding: 0 2px; }

/* Signed-out form: full-width control, one clear primary action. */
.guest-form label { margin-bottom: var(--space-2); color: var(--ink); font-weight: 500; }
.guest-form .form-actions { justify-content: stretch; margin-top: var(--space-3); }
.guest-form .btn-primary { width: 100%; justify-content: center; padding: 10px 16px; font-size: 1rem; }
.guest-help { margin-top: var(--space-3); font-size: 0.85rem; color: var(--ink-soft); }
.alert { display: flex; align-items: flex-start; gap: 8px; }

/* ---------------------------------------------------------------------
   Hit targets

   WCAG 2.2 asks for 24x24 on anything that must be clicked, with an
   exemption for links sitting inline in a sentence. Checkboxes and small
   icon buttons are not exempt, and a browser's default checkbox is 13px.
   ------------------------------------------------------------------- */

input[type="checkbox"], input[type="radio"] {
  width: 17px;
  height: 17px;
  accent-color: var(--nav-bg);
  cursor: pointer;
}

/* A checkbox and its words are one control, so they sit on one line and the
   whole of it is clickable. The label's own block layout and bottom margin
   would otherwise put the words under the box. */
label.checkbox {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  cursor: pointer;
  font-weight: 400;
}
label.checkbox input { margin: 0; flex: none; }

/* The visible box stays 17px; the cell around it carries the rest of the
   target, so the whole cell is clickable without drawing a huge control. */
.row-select {
  padding: 0;
  position: relative;
}
.row-select input[type="checkbox"] {
  margin: 0;
  /* A transparent ring extends the hit area to 34x34 without moving anything. */
  outline-offset: 0;
  box-shadow: 0 0 0 8px transparent;
}
.row-select label,
th.row-select, td.row-select {
  min-width: 34px;
  height: 34px;
  line-height: 34px;
}

.drawer-close {
  width: 32px;
  height: 32px;
  padding: 0;
  display: grid;
  place-items: center;
}

/* ---------------------------------------------------------------------
   Wide tables

   A sales register or a bills list has more columns than a narrow window
   can hold. The table scrolls inside its own card rather than pushing the
   whole page sideways — a page-level horizontal scrollbar moves the nav
   and the header too, which is disorienting.
   ------------------------------------------------------------------- */

.card > table,
.card > .table-scroll { width: 100%; }

.table-scroll {
  overflow-x: auto;
  /* Keeps a focused cell from being clipped against the scroll edge. */
  scroll-padding: var(--space-2);
}

.table-scroll table { min-width: 720px; }

/* The card already clips its corners; the scroller must not paint over them. */
.card > .table-scroll:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.card > .table-scroll:last-child { border-radius: 0 0 var(--radius) var(--radius); }

/* ---------------------------------------------------------------------
   Charts

   Drawn as inline SVG from figures already computed in PHP. Colours come
   from the same tokens as the rest of the app, so a chart never
   introduces a hue that appears nowhere else.
   ------------------------------------------------------------------- */

.chart-card { display: flex; flex-direction: column; }

.chart-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.chart-head h2 { margin: 0; font-size: 1.05rem; }

/* Height from the viewBox's own ratio, so the drawing fills the card's width
   instead of being scaled down to fit a fixed height and letterboxed with a
   band of empty card either side of it. */
.chart { width: 100%; height: auto; display: block; overflow: visible; }
.chart-bar { fill: var(--nav-bg); transition: fill var(--transition-fast); }
.chart-bar:hover { fill: var(--amber); }

/* The second series in a paired chart. Amber against the navy: the two are
   already the app's own pair, so the chart introduces no new hue. */
.chart-bar-alt { fill: var(--amber); transition: fill var(--transition-fast); }
.chart-bar-alt:hover { fill: var(--amber-ink); }

.chart-legend {
  display: flex;
  gap: var(--space-2);
  margin: 0;
  font-size: 0.8rem;
  color: var(--ink-soft);
}
.chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
.chart-legend i { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.chart-legend .swatch-a { background: var(--nav-bg); }
.chart-legend .swatch-b { background: var(--amber); }

/* A ranked list with the bar behind the row. Horizontal because the labels
   are names, and a name set sideways under a column is unreadable. */
.hbars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.hbars li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  column-gap: var(--space-2);
  row-gap: 4px;
  align-items: baseline;
}
/* Placed explicitly: the track spans both columns, and auto-placement would
   push the value that follows it onto a third row. */
.hbar-label {
  grid-area: 1 / 1;
  font-size: 0.85rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hbar-value { grid-area: 1 / 2; font-size: 0.85rem; text-align: right; white-space: nowrap; }
.hbar-track {
  grid-area: 2 / 1 / 3 / 3;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 999px;
  height: 8px;
  overflow: hidden;
}
.hbar-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--nav-bg-soft);
}
/* Later rows step lighter, so rank is legible from the bar alone. */
.hbars li:nth-child(2) .hbar-fill { background: oklch(0.48 0.05 252); }
.hbars li:nth-child(3) .hbar-fill { background: oklch(0.56 0.045 252); }
.hbars li:nth-child(4) .hbar-fill { background: oklch(0.64 0.04 252); }
.hbars li:nth-child(5) .hbar-fill { background: oklch(0.72 0.033 252); }
.hbars li:nth-child(n+6) .hbar-fill { background: oklch(0.80 0.025 252); }
.chart-axis { stroke: var(--rule); stroke-width: 1; }
.chart-label {
  font-family: var(--font-ui);
  font-size: 11px;
  fill: var(--ink-soft);
}

/* The aspect ratio is preserved, so nothing is stretched and label text keeps
   its shape. Set here rather than inherited, because an SVG does not take the
   page's font-size. */
.chart text { font-size: 11px; }

.donut-wrap { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.donut { width: 140px; height: 140px; flex: none; }
.slice-paid { stroke: var(--green); }
.slice-due { stroke: var(--amber); }
.donut-value { font-family: var(--font-mono); font-size: 22px; font-weight: 600; fill: var(--ink); }
.donut-label { font-family: var(--font-ui); font-size: 10px; fill: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.06em; }

.legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.legend li { display: flex; align-items: center; gap: 8px; font-size: 0.88rem; }
.legend strong { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.swatch { width: 10px; height: 10px; border-radius: 2px; flex: none; }
.slice-paid-bg { background: var(--green); }
.slice-due-bg { background: var(--amber); }

/* Ageing: one row per bucket, so the shape of the debt is readable at a
   glance without reading five numbers. */
.age-bars { display: flex; flex-direction: column; gap: 10px; }
.age-row { display: grid; grid-template-columns: 110px minmax(0, 1fr) 130px; align-items: center; gap: var(--space-2); }

/* Below tablet the track has nowhere useful to go, so the label and the
   amount take the full width and the bar sits under them. */
@media (max-width: 860px) {
  .age-row { grid-template-columns: minmax(0, 1fr) auto; }
  .age-track { grid-column: 1 / -1; order: 3; }
}
.age-label { font-size: 0.85rem; color: var(--ink-soft); }
.age-track { background: var(--paper); border: 1px solid var(--rule); border-radius: 999px; height: 12px; overflow: hidden; }
.age-fill { display: block; height: 100%; border-radius: 999px; }
.age-value { font-size: 0.88rem; }

/* Cooler for what is not yet a problem, warmer as it ages. */
.age-current { background: #7FA8D4; }
.age-1-30 { background: var(--blue); }
.age-31-60 { background: var(--amber); }
.age-61-90 { background: #E08A2E; }
.age-90plus { background: var(--red); }

/* Value bands are one measure split five ways, not five severities, so they
   share a colour and let the bar lengths do the comparing. */
.age-value-band { background: var(--nav-bg-soft); }

/* The attachment shown on the compose form: it is not a control, it is a
   statement of what will go with the message. */
.attachment-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--paper);
  margin-top: var(--space-2);
}
.attachment-chip > span { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.attachment-chip .text-soft { font-size: 0.8rem; }
.attachment-chip svg { color: var(--nav-bg); }

/* ---------------------------------------------------------------------
   Public document viewer

   Opened by a customer on a phone, standing somewhere, holding paper. One
   column, generous type, nothing to navigate: they came for one document
   and there is nothing else here.
   ------------------------------------------------------------------- */

.public-body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
}

.public-head {
  padding: var(--space-3);
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
.public-logo { height: 30px; display: block; }
.public-wordmark { font-weight: 600; font-size: 1.15rem; }

.public-main {
  flex: 1;
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-3);
}

.public-foot {
  border-top: 1px solid var(--rule);
  padding: var(--space-2) var(--space-3);
  color: var(--ink-soft);
  font-size: 0.85rem;
}
.public-foot p { margin: 0; }

/* The gate. Narrow, because it holds one field. */
.public-panel {
  max-width: 460px;
  margin: 0 auto;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: var(--space-4);
}
.public-panel h1 { margin: 0 0 var(--space-2); font-size: 1.4rem; }
.public-panel form { margin: 0; }

.public-eyebrow {
  margin: 0 0 4px;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-soft);
}

.public-lede {
  margin: 0 0 var(--space-3);
  color: var(--ink-soft);
  line-height: 1.6;
  max-width: 60ch;
}

.public-resend { margin-top: var(--space-2); text-align: center; }

/* The document. */
.public-doc-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.public-doc-head h1 { margin: 0; font-size: 1.5rem; }

.public-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.public-facts > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.public-facts dt { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-soft); }
.public-facts dd { margin: 0; }
/* A figure in a fact list is read against its label, not against the figure
   below it, so it stays on the left where its label starts. */
.public-facts dd.money { text-align: left; }

/* A line's description is a second thought about the line, not a second
   line, so it sits under the particulars rather than in a column. */
.public-line-note {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 2px;
}

.public-totals { margin: var(--space-3) 0 var(--space-3) auto; }

.public-words {
  margin: 0 0 var(--space-4);
  font-size: 0.9rem;
  color: var(--ink-soft);
  text-align: right;
}

.public-settle, .public-notes { margin-top: var(--space-4); }
.public-settle h2, .public-notes h2 { font-size: 1.05rem; margin: 0 0 var(--space-2); }
.public-notes p { margin: 0; line-height: 1.6; }

/* The document's table is the only thing on the page that may exceed the
   column, so it scrolls inside itself rather than taking the page sideways. */
.public-doc .table-scroll { border: 1px solid var(--rule); border-radius: var(--radius); overflow-x: auto; }

@media (max-width: 640px) {
  .public-main { padding: var(--space-3) var(--space-2); }
  .public-panel { padding: var(--space-3); }
  .public-totals { margin-left: 0; }
}

/* The actions a document page carries, moved into the drawer's header. Set
   small: they share a 56px bar with the title and the close button, and a
   full-size button row would push the title out of the panel. */
.drawer-head-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  flex-wrap: nowrap;
}
.drawer-head-actions form { margin: 0; }
.drawer-head-actions .btn {
  height: 30px;
  padding: 0 10px;
  font-size: 0.82rem;
  white-space: nowrap;
}
/* Below the width where four labelled buttons fit, they keep their icons and
   drop their words rather than wrapping onto a second line. */
@media (max-width: 720px) {
  .drawer-head-actions .btn { padding: 0 8px; }
  .drawer-head-actions .btn span,
  .drawer-head-actions .btn { font-size: 0; gap: 0; }
  .drawer-head-actions .btn svg { font-size: initial; }
}

/* A report's own head: what it is on the left, what run produced it on the
   right, so a printed page and the screen say the same thing. */
.report-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}
.report-facts {
  display: flex;
  gap: var(--space-3);
  margin: 0;
  flex-wrap: wrap;
}
.report-facts > div { display: flex; flex-direction: column; gap: 2px; }
.report-facts dt { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-soft); }
.report-facts dd { margin: 0; font-size: 0.85rem; }

.report-notes { margin: 0; padding-left: 18px; color: var(--ink-soft); line-height: 1.6; }
.report-notes li { margin-bottom: 4px; }

/* A form wrapping one icon button, so a destructive action can be a POST
   without breaking the row of links it sits in. */
.row-action-form { display: inline-flex; margin: 0; }
button.row-action { background: none; font: inherit; cursor: pointer; }

/* A number in a table cell needs room for its digits and no more; left at
   100% it stretches to the column and reads as a text field.

   Qualified with the element on purpose. A bare .num-cell is specificity
   (0,1,0) and loses to the input[type="text"] { width: 100% } rule above it
   at (0,1,1), so the class had no effect at all and every digits box was as
   wide as its column. */
input.num-cell { width: 6em; }
input.prefix-cell { width: 12em; }
.data-table input[type="text"] { margin-top: 0; }

/* Columns sized to their fields, so the last one takes the slack instead of
   every column sharing it equally and stranding each input in white space. */
.numbering-table th:nth-child(1), .numbering-table td:nth-child(1) { width: 20%; }
.numbering-table th:nth-child(2), .numbering-table td:nth-child(2) { width: 22%; }
.numbering-table th:nth-child(3), .numbering-table td:nth-child(3) { width: 13%; }
.numbering-table th:nth-child(4), .numbering-table td:nth-child(4) { width: 17%; }

/* A one-time code is read off a screen and typed in one go, so it is set
   wide and monospaced to make a mistyped digit obvious. */
.otp-input {
  font-family: var(--font-mono);
  font-size: 1.4rem;
  letter-spacing: 0.4em;
  text-align: center;
  padding: 10px;
}

/* ---------------------------------------------------------------------
   Status callout

   Replaces the coloured left stripe. A stripe puts all its meaning in a
   3px edge, which is easy to miss and says nothing on its own; this
   carries a leading icon, a heading and one sentence, so the state is
   legible without decoding a colour.
   ------------------------------------------------------------------- */

.callout {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: var(--space-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  margin-top: var(--space-2);
}

.callout-icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  flex: none;
  background: oklch(0.93 0.02 90);
  color: var(--ink-soft);
}

.callout h2 { margin: 0 0 2px; font-size: 1rem; }
.callout p { margin: 0; color: var(--ink-soft); }
.callout .callout-body { min-width: 0; }

.callout-danger { border-color: oklch(0.82 0.07 27); background: oklch(0.975 0.015 27); }
.callout-danger .callout-icon { background: oklch(0.92 0.05 27); color: var(--red); }

.callout-good { border-color: oklch(0.85 0.05 152); background: oklch(0.975 0.015 152); }
.callout-good .callout-icon { background: oklch(0.93 0.04 152); color: var(--green); }

.callout-note { border-color: oklch(0.86 0.06 72); background: oklch(0.98 0.02 85); }
.callout-note .callout-icon { background: oklch(0.93 0.06 75); color: var(--amber-ink); }

/* ---------------------------------------------------------------------
   Dashboard

   The money is one statement, not a row of tiles. Billed is the whole,
   received is part of it, outstanding is the remainder, and the bar shows
   the relationship the three numbers describe. Presenting them as three
   equal boxes would hide exactly the thing the reader came to see.
   ------------------------------------------------------------------- */

.ledger {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
}

.ledger-empty { margin: 0; color: var(--ink-soft); }

.ledger-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: flex-start;
  margin-bottom: var(--space-3);
}

.ledger-figure { display: flex; flex-direction: column; gap: 2px; }

.ledger-label {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-soft);
  font-weight: 600;
}

/* Billed is the anchor and reads largest; the other two are read against it,
   so they step down rather than competing. */
.ledger-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 2.1rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.ledger-figure.is-good .ledger-value,
.ledger-figure.is-due .ledger-value { font-size: 1.6rem; }
.ledger-figure.is-good .ledger-value { color: var(--green); }
.ledger-figure.is-due .ledger-value { color: var(--amber-ink); }

.ledger-note { font-size: 0.82rem; color: var(--red); }

.ledger-bar {
  display: block;
  height: 8px;
  border-radius: 999px;
  background: oklch(0.93 0.03 75);
  overflow: hidden;
}
.ledger-bar-fill { display: block; height: 100%; background: var(--green); border-radius: 999px; }
.ledger-caption { margin: 8px 0 0; font-size: 0.85rem; color: var(--ink-soft); }

/* ---------------------------------------------------------------------
   Dashboard bands

   Three horizontal bands, read top to bottom: what the money did, what
   drove it, and what is still outstanding. Each band has its own column
   rhythm rather than one grid repeated three times, so the eye can tell
   where one question ends and the next begins without a heading.
   ------------------------------------------------------------------- */

.dash-band {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(230px, 288px);
  gap: var(--space-3);
  /* Stretch, not start: the panel on the right takes the full band height so
     its foot lands on the same line as the chart's, and the graphic inside it
     absorbs the difference. */
  align-items: stretch;
  margin-bottom: var(--space-3);
}
.dash-primary { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
/* Takes the slack when the panel beside it is the taller of the two, so the
   two columns end on the same line whichever one is driving the height. */
.dash-primary > .chart-card { flex: 1 1 auto; }

.dash-grid-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.dash-grid-2 {
  display: grid;
  /* Ageing carries five rows and the longer labels, so it takes the wider
     half rather than splitting the row down the middle. */
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

@media (max-width: 1180px) {
  .dash-grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 1000px) {
  .dash-band, .dash-grid-3, .dash-grid-2 { grid-template-columns: 1fr; }
}

/* The three money figures. Cards here because each is a separate question
   with its own footnote, but they are deliberately not interchangeable: the
   mark is coloured per figure and the value takes the tone of its own news,
   so the row reads as three answers rather than three slots. */
.figure-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
}
@media (max-width: 640px) { .figure-cards { grid-template-columns: 1fr; } }

.figure-card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: var(--space-2) var(--space-2) 14px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto auto;
  column-gap: 10px;
  row-gap: 2px;
  align-items: center;
}
.figure-card .figure-mark { grid-row: 1 / 3; }
.figure-card h2 {
  margin: 0;
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-soft);
  align-self: end;
}
.figure-value {
  margin: 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.7rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.01em;
  align-self: start;
}
.figure-value.is-good { color: var(--green); }
.figure-value.is-due { color: var(--amber-ink); }
.figure-value .is-empty {
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 400;
  color: var(--ink-soft);
}
.figure-note {
  grid-column: 1 / -1;
  margin: 6px 0 0;
  padding-top: 6px;
  border-top: 1px solid var(--rule);
  font-size: 0.82rem;
  color: var(--ink-soft);
}

/* Solid enough to find at a glance; the earlier tint was invisible. */
.figure-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-sm);
  flex: none;
}
.mark-sales { background: oklch(0.93 0.03 252); color: var(--nav-bg); }
.mark-receipts { background: oklch(0.93 0.045 152); color: var(--green); }
.mark-due { background: oklch(0.94 0.055 78); color: var(--amber-ink); }

/* Hex fallbacks for anything without OKLCH support. */
@supports not (color: oklch(0.5 0.1 200)) {
  .mark-sales { background: #DCE6F4; }
  .mark-receipts { background: #DCEEE2; }
  .mark-due { background: #FAEBCE; }
}

/* A list of things to start, not four cards pretending to be equal choices. */
.dash-actions { display: flex; flex-direction: column; }
.dash-actions h2 {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-soft);
  margin: 0 0 6px;
}
.dash-actions a {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px;
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-weight: 500;
  border: 1px solid transparent;
}
.dash-actions a:hover {
  background: var(--paper);
  border-color: var(--rule);
  text-decoration: none;
}
.dash-actions a svg { color: var(--amber-ink); flex: none; margin-top: 2px; }
.dash-actions a > span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.dash-actions a strong { font-weight: 600; }
/* The second line says what the action is for. Muted, because it is read
   once and then never again. */
.dash-actions a > span { font-size: 0.8rem; color: var(--ink-soft); font-weight: 400; }
.dash-actions a strong { font-size: 0.92rem; color: var(--ink); }

/* ---------------------------------------------------------------------
   The quote-bill-collect loop

   Three states of one document, not three separate things: the node fills
   in as the money gets closer, so the row reads left to right as progress
   rather than as a legend. Grows to absorb whatever height the charts
   beside it leave over, which is what keeps the two columns' feet level.
   ------------------------------------------------------------------- */

.flow-graphic {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  margin: 0 0 var(--space-2);
  padding: var(--space-2) 4px;
  min-height: 92px;
  border-bottom: 1px solid var(--rule);
}

.flow-track {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  position: relative;
  /* Capped, because stacked on a narrow window the panel runs the full page
     width and three nodes strung across it stop reading as one sequence. */
  max-width: 320px;
  margin: 0 auto;
  width: 100%;
}

/* The connector sits behind the nodes and stops short of both ends, so it
   reads as joining them rather than running past them. */
.flow-track::before {
  content: '';
  position: absolute;
  top: 18px;
  left: 16%;
  right: 16%;
  height: 1px;
  background: var(--rule);
}

.flow-step {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
}

.flow-node {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--surface);
}

/* Outline, then solid, then solid and green: the further right, the more of
   the document is real money. */
.flow-node.is-quote {
  border: 1px solid var(--rule);
  color: var(--amber-ink);
}
.flow-node.is-bill {
  background: var(--nav-bg);
  border: 1px solid var(--nav-bg);
  color: var(--nav-ink-bright);
}
.flow-node.is-paid {
  background: var(--green);
  border: 1px solid var(--green);
  color: var(--nav-ink-bright);
}

/* Read by a screen reader, not drawn. Clipped rather than display:none,
   because a hidden element is skipped by assistive software entirely. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.flow-graphic figcaption {
  max-width: 34ch;
  margin: 0 auto;
  font-size: 0.76rem;
  line-height: 1.45;
  color: var(--ink-soft);
  text-align: center;
  text-wrap: balance;
}

/* Stacked, the panel is no longer beside anything to line up with, so the
   graphic stops claiming height it does not need. */
@media (max-width: 1000px) {
  .flow-graphic { flex: none; min-height: 0; }
}

/* ---------------------------------------------------------------------
   Narrow screens

   The sidebar is a fixed 224px, which is a quarter of a tablet's width
   spent on navigation that is read once. Below 1000px it collapses to
   icons: the destinations stay reachable and keyboard-navigable, and the
   content gets the room. Structural, not a hamburger behind a script.
   ------------------------------------------------------------------- */

@media (max-width: 1000px) {
  :root { --sidebar-width: 68px; }

  .sidebar-brand { justify-content: center; padding: var(--space-2) 8px; }
  .sidebar-brand img { height: 26px; }

  .sidebar-nav a, .sidebar-nav .nav-disabled {
    justify-content: center;
    padding: 12px 0;
    border-left-width: 0;
    border-right: 3px solid transparent;
  }
  .sidebar-nav a.active { border-right-color: var(--amber); }

  /* The label is hidden visually but stays in the accessibility tree, so the
     link is still announced by name and the title gives a pointer user the
     same information on hover. */
  .sidebar-nav a > span:not(.nav-soon),
  .sidebar-nav .nav-disabled > span:not(.nav-soon) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .nav-group { text-align: center; font-size: 0.6rem; padding: var(--space-2) 2px 4px; }
  .nav-soon { display: none; }

  .content { padding-left: var(--space-2); padding-right: var(--space-2); }
  .app-footer { padding-left: var(--space-2); padding-right: var(--space-2); }
}

/* The footer's two halves must be allowed to wrap and to break long text,
   or one long sentence pushes the page wider than the window. */
.app-footer > span { min-width: 0; overflow-wrap: anywhere; }
