/* ---------- Typefaces ---------- */
/* Served from this domain rather than fonts.googleapis.com. A Google Fonts <link> costs a
   visitor two extra DNS lookups and two TLS handshakes to a host the browser has no
   connection to yet, all of it in front of the first paint, and on a South African mobile
   connection that is the slowest part of loading the page. This is the same file Google
   would have served: Inter is open source (OFL), so hosting it here is allowed and
   nothing about how it renders changes.

   One family for both body and headings now, used to be paired with Bricolage
   Grotesque for headings, but that read as too playful for what should look like a
   professional invoicing tool. Inter is a variable font, so one file covers every
   weight the site uses (400 for body text up to 800 for headings): Google's own
   stylesheet declares a separate block per weight pointing at the same URL. Declared
   as a weight range here instead.

   Two subsets, split exactly as Google splits them. The browser downloads latin-ext
   only if a page actually contains a character in its range, a customer name with a
   Polish or Czech letter in it, so the ordinary page load fetches only the latin file.
   Any character outside both ranges falls through to the next font in the stack, which
   is what the system-ui fallback below is for. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("../fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("../fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --ink: #12261f;          /* deep forest near-black, text */
  --ink-soft: #4a5c53;     /* muted body text */
  --paper: #f5f6f2;        /* warm off-white background */
  --card: #ffffff;
  /* Deliberately not green: WhatsApp invoicing tools default to green almost without
     exception (it's the WhatsApp brand color), which is exactly what makes this app
     hard to tell apart from others in the category at a glance. This blue keeps the
     confident, "money handled correctly" feel without the borrowed association. */
  --brand: #2c5aa0;
  --brand-deep: #1e3f73;
  /* The text/icon counterpart to --brand-deep, split out because the two roles pull
     in opposite directions once dark mode exists: --brand-deep is only ever a solid
     fill with white text sitting on top of it (.btn-cta:hover), which needs to stay a
     deep, saturated blue in both themes so the white text keeps its contrast. Every
     other place that reads as "brand-colored text on a light or tinted surface"
     (pills, badges, links) uses --brand-ink instead, which is free to turn light in
     dark mode without breaking a button that depends on staying dark. Identical to
     --brand-deep's value here, so light mode looks exactly as it did before this
     split existed. */
  --brand-ink: #1e3f73;
  --brand-tint: #e8eef8;   /* soft brand wash */
  --brand-tint-line: #c9d9ee; /* border that pairs with --brand-tint */
  --amber: #d98a1f;        /* rand amounts pop */
  --line: #e4e7e2;
  --shadow: 0 18px 40px -18px rgba(18, 38, 31, 0.28);
  --radius: 7px;

  /* Same --deep/--ink split as brand, and for the same reason: .confirm-yes.confirm-danger
     and .quote-pill-decline:hover fill solid with white text on top and must stay a
     saturated red in both themes, while .badge-overdue/.text-overdue/.link-danger-btn
     read danger-colored text against a light or tinted surface and need to lighten in
     dark mode instead. --danger-ink also absorbs the handful of near-identical reds
     (#b42318, #B42318, #7a271a) that were scattered through the file before this
     existed: they were never a deliberate second shade, just drift, so this is one
     token doing the job all of them did. */
  --danger: #b3261e;
  --danger-deep: #922017;
  --danger-ink: #b3261e;
  --danger-tint: #fdeaea;
  --danger-tint-line: #f3c6c0;
  /* No solid-fill-with-white-text use of the warning color exists anywhere, so unlike
     brand/danger it needs only one role. */
  --warning: #7a4d05;
  --warning-tint: #fff3d6;
  /* A second, paler wash for the .questions panel, kept apart from --warning-tint
     because the two were already visibly different washes (a small pill's saturated
     yellow vs. a bigger panel's softer one): collapsing them into one would be an
     actual visual change, not just tidying drift. */
  --warning-soft-tint: #fdf7ec;
  --warning-soft-border: #f0d9a8;
  --bubble-bg: #f0f2ee;    /* the bot's side of a sample conversation (.bubble-bot) */
  --neutral-tint: #eef0ec; /* .badge-draft's wash, a plain "nothing to report yet" grey */
  --neutral-ink: #5b6b62;
  /* The white gap-ring in a focused control's box-shadow (see .btn:focus etc.), it is
     meant to match whatever surface the control is sitting on, which in dark mode is
     --card rather than literal white. */
  --focus-ring-gap: #fff;

  /* Feed the palette into Bootstrap's own variables so its components
     (buttons, links, navbar) pick up the brand colors automatically. */
  --bs-body-color: var(--ink);
  --bs-body-bg: var(--paper);
  --bs-link-color: var(--brand-ink);
  --bs-link-hover-color: var(--brand);
  --bs-primary: var(--brand);
  --bs-primary-rgb: 44, 90, 160;
}

/*
 * Dark mode, toggled by site.js setting data-bs-theme on <html> (see .theme-toggle
 * below) and persisted in localStorage. Bootstrap 5.3 already ships its own component
 * styles keyed off this same attribute, which is why it's reused here rather than a
 * separate data-theme of our own: one attribute for both, instead of two to keep
 * in sync.
 *
 * Every rule elsewhere in this file reads a variable rather than a literal color for
 * anything that should adapt, so this block only has to redeclare the tokens
 * themselves: var() resolves against whatever is cascaded on the element actually
 * using it, not wherever the alias was declared, so overriding --paper, --ink and so
 * on here is enough to flow through the Bootstrap aliases above too.
 *
 * Never applied to the customer-facing public document/statement pages (Public/Document,
 * Public/Statement) or the PDF preview: those are rendered through _PublicLayout,
 * which has no theme toggle and never sets this attribute, so they stay exactly as
 * they look today regardless of the viewer's own preference. That is deliberate: a
 * shared invoice is meant to look like a printed document, not like the app.
 */
[data-bs-theme="dark"] {
  --ink: #e8ece9;
  --ink-soft: #a3b0a8;
  --paper: #12161a;
  --card: #1b2126;
  --brand-tint: #16283f;
  --brand-tint-line: #24405f;
  --brand-ink: #8fb4e8;
  --amber: #e8a53d;
  --line: #2a3238;
  --shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.6);

  --danger-ink: #ef5350;
  --danger-tint: #3a1414;
  --danger-tint-line: #5c2020;

  --warning: #f0c04a;
  --warning-tint: #3a2f10;
  --warning-soft-tint: #2c2410;
  --warning-soft-border: #4a3d18;

  --bubble-bg: #232a2f;
  --neutral-tint: #262b28;
  --neutral-ink: #b7c2ba;

  --focus-ring-gap: var(--card);
}

/* Sticky footer: the page is a flex column so the footer is pushed to the bottom on
   short pages and simply follows the content on long ones. Replaces the scaffold's
   absolute-positioning approach, which overlapped content and could not cope with a
   footer that wraps onto two lines on narrow screens. */
body {
  font-family: 'Inter', system-ui, sans-serif;
  background-image: radial-gradient(rgba(18, 38, 31, 0.04) 1px, transparent 1px);
  background-size: 22px 22px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.page-content {
  flex: 1 0 auto;
}

.footer {
  flex-shrink: 0;
}

h1, h2, h3, h4, .navbar-brand {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--ink);
}

a {
  text-decoration: none;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem var(--focus-ring-gap), 0 0 0 0.25rem var(--brand-tint);
}

/* ---------- Nav ---------- */
.navbar {
  background: var(--card) !important;
  border-bottom: 1px solid var(--line);
}

.brand-mark {
  width: 32px;
  height: 32px;
  background: var(--brand);
  border-radius: 4px 4px 4px 2px; /* chat-bubble corner */
  display: inline-grid;
  place-items: center;
  flex: none;
}
.brand-mark svg { width: 17px; height: 17px; display: block; }

.navbar-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 22px;
}

.nav-link.text-dark {
  color: var(--ink) !important;
}
.nav-link.text-dark:hover {
  color: var(--brand-ink) !important;
}

/* The greeting sits among the nav links but is not one: muted and unclickable, so
   nobody taps their own name expecting it to go somewhere. */
.nav-greeting {
  color: var(--ink-soft) !important;
  cursor: default;
}

/* The hamburger is used almost exclusively on phones, where a fingertip needs far more
   to aim at than Bootstrap's desktop-sized defaults offer. Padding rather than a bigger
   icon: the tap target grows without the glyph shouting. */
.navbar-toggler {
  padding: 10px 14px;
  min-width: 48px;
  min-height: 48px;
}

/* Sits between the brand and the hamburger while the menu is collapsed (see the d-lg-none
   on it in _Layout). Pushed hard right by the auto margin so it lands next to the hamburger
   rather than floating in the middle of the bar. */
.nav-login-compact {
  display: inline-block;
  margin-left: auto;
  margin-right: 8px;
  padding: 11px 16px;
  font-weight: 700;
  color: #fff;
  background: var(--brand);
  border-radius: 4px;
  text-decoration: none;
}
.nav-login-compact:hover,
.nav-login-compact:focus {
  background: var(--brand-deep);
  color: #fff;
}

/* Below -lg is where the menu actually collapses, so these rules only ever style the
   dropped-down menu, never the desktop row of links. Kept in step with the
   navbar-expand-lg in _Layout: the two have to name the same breakpoint or the menu is
   styled as a column while Bootstrap is still laying it out as a row. */
@media (max-width: 991.98px) {
  .navbar-collapse {
    padding: 6px 2px 10px;
  }

  /* Full-width rows with generous height, so anywhere on the line is a hit, including
     the "Log out" button, which is a form button dressed as a link. */
  .navbar-collapse .nav-link,
  .navbar-collapse .btn-link.nav-link {
    font-size: 17px;
    padding: 13px 10px;
    width: 100%;
    text-align: left;
  }

  .navbar-collapse .nav-item form {
    width: 100%;
  }

  /* Hairlines between rows, so neighbouring targets read as separate things to tap. */
  .navbar-collapse .nav-item + .nav-item {
    border-top: 1px solid var(--line);
  }

  /* The account links arrive as a second list (_LoginPartial); rule a line above them
     too, so the seam between the two lists matches the seams within them. */
  .navbar-collapse .navbar-nav + .navbar-nav {
    border-top: 1px solid var(--line);
  }

  /* Only the open menu becomes a column, hence .show/.collapsing rather than
     .navbar-collapse on its own: this file loads after Bootstrap, so a display rule on
     the bare class would beat .collapse:not(.show) { display: none } and leave the menu
     permanently dropped down. */
  .navbar-collapse.show,
  .navbar-collapse.collapsing {
    display: flex;
    flex-direction: column;
    /* Bootstrap sets align-items: center on .navbar-collapse for the desktop row. Once
       this is a column that centres every row horizontally and shrinks each list to its
       own text width, so it has to be undone here or the menu stops being left-aligned
       full-width rows. */
    align-items: stretch;
  }

  /* Light/dark is a set-once preference, not something anyone opens the menu to reach,
     so it goes under the links instead of sitting above them in source order. Ordered
     rather than moved in the markup, so the desktop row keeps the toggle where it is. */
  .navbar-collapse .navbar-nav { order: 1; }
  .navbar-collapse .theme-toggle {
    order: 2;
    align-self: flex-start;
    margin: 12px 10px 2px;
  }
}

/* Above -lg the menu runs as one row rather than stacked rows, so the hairlines above
   don't apply: a small middot between links does the same job of showing Home, Privacy,
   Terms and Contact us as separate links rather than one run-on phrase. Only between
   items in the same list, so it never lands between this nav and _LoginPartial's own.
   The dot goes on .nav-link itself, not .nav-item: .nav-link carries the padding that
   sets the link text's vertical position, and a dot on the li instead sits at the li's
   own top edge, above the text rather than beside it. Drawn as a small filled circle
   rather than a "\00b7" character: a middot glyph sits at a different height in every
   font and never centers reliably against the link text next to it, where a plain box
   with vertical-align: middle does. */
@media (min-width: 992px) {
  /* A label never breaks across two lines. If the row does run out of room on a narrow
     desktop window, whole links wrap instead of "Report a / bug" splitting mid-phrase and
     leaving its dot stranded. */
  .navbar-nav .nav-link {
    white-space: nowrap;
  }

  .navbar-nav .nav-item + .nav-item .nav-link::before {
    content: "";
    display: inline-block;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--ink-soft);
    margin-right: 8px;
    vertical-align: middle;
  }
}

.pill {
  font-size: 13px;
  font-weight: 600;
  color: var(--brand-ink);
  background: var(--brand-tint);
  border: 1px solid var(--brand-tint-line);
  padding: 6px 13px;
  border-radius: 999px;
  letter-spacing: 0.01em;
}

/* ---------- Bottom page navigation ---------- */
/* Back links live at the foot of each page rather than in the heading: on a phone the
   bottom is where the thumb already is once the content has been read, and the heading
   stays about what the page is, not where to go next. */
.page-bottom-nav {
  margin-top: 28px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: flex-end; /* where the thumb rests on a phone held right-handed */
}
.page-bottom-nav .back-link {
  padding: 12px 26px; /* grown from the base .back-link, this is the page's main way back */
  min-width: 160px;
  text-align: center;
  font-size: 15px;
}

/* Two actions offered as equals: same width, whatever their labels measure. */
.btn-pair {
  display: flex;
  gap: 8px;
}
.btn-pair .btn {
  flex: 1 1 0;
  text-align: center;
}

/* Phone treatment for action buttons. A ragged wrap of differently-sized buttons
   reads as clutter on a small screen, so:
   - buttons slim down a notch (the desktop padding is generous for a thumb anyway);
   - the dashboard's quick-start row centres its two buttons instead of wrapping them
     ragged-left, since a centred pair reads as one deliberate choice rather than a list;
   - a page-heading .btn-pair drops below the title and shares the full width. */
@media (max-width: 575px) {
  /* Written as .btn.btn-cta rather than .btn-cta: the button sizes further down this file
     are the same specificity and come later, so a single class here lost to them and only
     the font-size (which nothing else sets) was ever getting through. */
  .btn.btn-cta,
  .btn.btn-quiet {
    padding: 10px 14px;
    font-size: 15px;
    /* Slimmer than the desktop 48px, but still a comfortable target: below this the
       narrower padding above starts making buttons awkward to hit accurately. */
    min-height: 44px;
    /* Narrower than the desktop minimum on purpose. At 180px a pair of buttons no longer
       fits across a 360px phone and every row breaks to one button per line, which is a
       lot of scrolling for a page that is mostly buttons. */
    min-width: 160px;
  }
  .quick-start-actions {
    display: flex;
    justify-content: center;
    width: 100%;
  }
  .quick-start-actions .btn {
    text-align: center;
    padding-left: 8px;
    padding-right: 8px;
    font-size: 14.5px;
  }
}

/* ---------- Landing hero ---------- */
/* Held well inside the container: centred text is only readable in short lines, and the
   full 1140px would give a measure nobody can track back from. The lead is narrower
   still, roughly 60 characters, for the same reason. */
.hero-centred {
  max-width: 760px;
  margin: 0 auto;
  text-align: center;
}
.hero-lead {
  max-width: 58ch;
  margin-left: auto;
  margin-right: auto;
}

/* The hero's two calls to action are one set and are sized as one: same height, same
   width, so they read as two choices rather than one real button with some spare text
   next to it. Colour does the ranking: only "Try it now" is filled brand. */
.hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}
/* A hero action that posts (the demo login) has to be wrapped in a form, and that form
   would otherwise become the flex item and take the sizing below, leaving the button
   inside it to size itself off its label. display: contents lifts the button back out so
   it lines up with the plain links beside it. */
.hero-actions form { display: contents; }
/* Fixed basis, no shrink: on a phone they drop to one per row at full button width
   rather than squeezing two narrow ones side by side and wrapping the labels. */
.hero-actions .btn-cta,
.hero-actions .btn-quiet {
  flex: 0 0 236px;
  max-width: 100%;
  justify-content: center;
  min-width: 0;
  padding: 14px 22px;
  font-size: 16px;
  line-height: 1.5;
}

/* ---------- Buttons ---------- */
/* One size for every button, whatever colour it is. Matching padding alone does not get
   there: .btn-quiet carries a border and .btn-cta does not, so the two end up a couple of
   pixels apart, and buttons wrapped in their own <form> (mark paid, change due date) keep
   their natural height rather than stretching with the row beside them. A shared minimum
   height with the label centred in it settles both, and it is a bigger target for a thumb
   than the padding alone was giving. The transparent border on the borderless ones keeps
   the box arithmetic identical, so nothing depends on the minimum to line up. */
.btn-cta,
.btn-quiet,
.btn-danger-cta,
.btn-install {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
}

.btn-cta {
  font-weight: 700;
  color: #fff;
  background: var(--brand);
  /* Transparent rather than none, so this measures the same as the bordered .btn-quiet
     beside it, see the note above. */
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 12px 20px;
  min-width: 200px;
}
.btn-cta:hover,
.btn-cta:focus {
  background: var(--brand-deep);
  color: #fff;
}

/* The one destructive primary action in the app: deleting an account for good. Shaped
   exactly like .btn-cta so it sits where a primary button sits, coloured danger so it is
   never pressed in mistake for the brand-coloured button beside it. --danger stays
   saturated in both themes (see the note at the top of this file), so white text on it
   reads either way. */
.btn-danger-cta {
  font-weight: 700;
  color: #fff;
  background: var(--danger);
  /* Same reason as .btn-cta above: matches the box of a bordered button. */
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 12px 20px;
  min-width: 200px;
}
.btn-danger-cta:hover,
.btn-danger-cta:focus {
  background: var(--danger-deep);
  color: #fff;
}

/* App-install button. Louder than a plain secondary button on purpose, on a phone the
   installed app is the whole point, so when the browser offers it we want it noticed
   rather than blending in. Ships hidden (the [hidden] attribute in the markup) and is
   revealed by site.js only once installing is genuinely on offer. */
.btn-install {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  color: var(--brand-ink);
  background: var(--brand-tint);
  border: 1.5px solid var(--brand);
  border-radius: 4px;
  padding: 12px 20px;
  cursor: pointer;
}
.btn-install:hover,
.btn-install:focus {
  background: var(--brand);
  color: #fff;
}
.btn-install svg { width: 18px; height: 18px; flex-shrink: 0; }
/* [hidden] must win even though the flex display above would otherwise show it. */
.btn-install[hidden] { display: none; }

/* A button that has to read as an ordinary inline link. The PWA install has to be a
   button, because it opens the browser's own prompt rather than going anywhere, but in
   the landing page's cards it sits where a link would and should look like one. */
.link-button {
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-weight: 600;
  /* --brand-ink, not --brand-deep: this is brand-coloured text on a surface, which has to
     lighten in dark mode. --brand-deep stays a dark navy there because it is a fill colour
     with white on top of it, and text painted in it disappears into the dark background. */
  color: var(--brand-ink);
  cursor: pointer;
}
.link-button:hover,
.link-button:focus {
  text-decoration: underline;
}

/* The coloured word in the landing page headline. Light mode keeps exactly the shade it
   always had; dark mode swaps to the text-role token, because --brand is a fill colour and
   painted on the dark background it reads at roughly 2.4:1, under the bar even for
   display-sized text. Written as an override rather than by changing the token, so nothing
   else that fills with --brand moves with it. */
.hero-accent { color: var(--brand); }
[data-bs-theme="dark"] .hero-accent { color: var(--brand-ink); }

/* A link that has to be found rather than merely be available: the demo account, the
   install offers on the landing page. Shaped as a button rather than as text with a colour,
   because as small print under two real buttons these were being missed entirely.
   Deliberately the same language as .btn-install (tinted fill, brand border, the same 9px
   radius every button on the site uses), and a size down from the hero's own buttons so it
   reads as the third way in rather than competing with the first two. The tint carries the
   legibility, so it holds up in dark mode without leaning on the text colour alone. */
.link-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  background: var(--brand-tint);
  border: 1.5px solid var(--brand);
  border-radius: 4px;
  padding: 10px 18px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  color: var(--brand-ink);
  text-decoration: none;
  cursor: pointer;
}
.link-cta:hover,
.link-cta:focus-visible {
  background: var(--brand);
  color: #fff;
  text-decoration: none;
}

/* ---------- Feature grid on the landing page ---------- */
.feature-card {
  height: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 22px;
}

/* ---------- "What it does" list on the landing page ---------- */
/* Plain lines rather than another row of .feature-card boxes: this replaced two grids of
   cards behind a fold, and the point of replacing them was to have less on the page, not
   the same amount styled differently. A tick rather than a bullet, since every line here
   is a thing ChatBill already does, not a menu of choices. */
.whatit-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.whatit-list li {
  position: relative;
  padding: 6px 0 6px 30px;
  color: var(--ink-soft);
}
.whatit-list li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--brand-ink);
  font-weight: 700;
}

/* ---------- Closed-beta notice on the landing page ---------- */
.beta-notice {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: var(--shadow);
  padding: 22px;
  max-width: 440px;
}
.beta-notice .pill {
  display: inline-block;
  margin-bottom: 12px;
}
.beta-notice p {
  color: var(--ink-soft);
  font-size: 15px;
  margin-bottom: 18px;
}

/* ---------- "More actions" under a document's button row ---------- */
/* Every action a document has in one row is a wall on a phone, and it gives Delete the
   same weight as Send. What finishes a job stays in the row; the edits and the undos go
   in here. A <details> rather than a scripted dropdown, so every action stays an ordinary
   form post with its data-confirm intact and the whole thing still opens with JavaScript
   off. Dressed like .btn-quiet, but left-aligned rather than centred, since it sits under
   the row it belongs to. */
.more-actions {
  margin-top: 12px;
}
.more-actions > summary {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  cursor: pointer;
  list-style: none;
  font-weight: 700;
  font-size: 16px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 12px 20px;
  min-height: 48px;
}
.more-actions > summary::-webkit-details-marker { display: none; }
.more-actions > summary:hover,
.more-actions > summary:focus {
  background: var(--brand-tint);
  border-color: var(--brand-tint-line);
  color: var(--brand-ink);
}
.more-actions > summary::after {
  content: "";
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.more-actions[open] > summary::after {
  transform: translateY(2px) rotate(225deg);
}
/* Wraps like the row above it, and centred for the same reason that row is: a stretched
   flex item makes the buttons wrapped in a <form> taller than the plain links beside them. */
.more-actions-body {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
/* In step with the .btn sizes at the top of this file, which slim down at the same width. */
@media (max-width: 575px) {
  .more-actions > summary {
    padding: 10px 14px;
    font-size: 15px;
    min-height: 44px;
  }
}

/* ---------- Credit waiting to be spent on the document you are looking at ---------- */
/* Sits directly above the button row rather than in the details list, because it is not a
   fact about the invoice: it is money the tradesman can take off it right now, and he was
   scrolling past it. Brand tint rather than the warning wash .notice-card uses: this is
   good news, the customer is already in funds. */
.credit-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--brand-tint-line);
  border-radius: 6px;
  background: var(--brand-tint);
}
.credit-note strong {
  display: block;
  font-size: 17px;
  color: var(--ink);
}
.credit-note span {
  color: var(--ink-soft);
  font-size: 14px;
}
/* On a phone the button goes full width under the wording instead of squeezing beside it:
   .btn-cta is 200px wide on its own, which leaves the sentence three words to a line. */
@media (max-width: 575px) {
  .credit-note { align-items: stretch; }
  .credit-note form { width: 100%; }
  .credit-note form .btn { width: 100%; }
}

/* Secondary action that sits next to the brand-colored button without competing with it. */
.btn-quiet {
  font-weight: 700;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 12px 20px;
  min-width: 200px;
}
.btn-quiet:hover,
.btn-quiet:focus {
  background: var(--brand-tint);
  border-color: var(--brand-tint-line);
  color: var(--brand-ink);
}

/* ---------- Waitlist form ---------- */
.waitlist-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 7px;
  box-shadow: var(--shadow);
}
.waitlist-card .form-control {
  background: var(--card);
  border: none;
  font-size: 16px;
  padding: 12px 14px;
}
.waitlist-card .form-control:focus {
  box-shadow: none;
}

.success-card {
  display: none;
  background: var(--brand-tint);
  border: 1px solid var(--brand-tint-line);
  border-radius: 6px;
  padding: 18px 20px;
  box-shadow: var(--shadow);
}
.success-card.show { display: block; }
.success-card strong {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 700;
  color: var(--ink);
  display: block;
  margin-bottom: 3px;
  font-size: 17px;
}

/* The working state a button goes into once its form is on its way (see site.js). Sized in
   em so it matches whatever button it lands in, and drawn in currentColor so it works on
   the brand button, the secondary one and the red one without a variant for each. Opacity
   rather than the disabled attribute: a disabled submit button is dropped from the form
   data, taking its name and value with it. */
.btn-spinner {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-right: 0.5em;
  vertical-align: -0.08em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.6s linear infinite;
}
.is-busy {
  pointer-events: none;
  opacity: 0.72;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

/* Still shows the state, just without the spin. */
@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation: none; opacity: 0.6; }
}

/* The failed twin of .success-card, same shape so a panel that shows one or the other
   doesn't change size with the verdict. Tokens rather than literals, so it follows the
   theme like everything else. */
.warn-card {
  display: none;
  background: var(--danger-tint);
  border: 1px solid var(--danger-tint-line);
  border-radius: 6px;
  padding: 18px 20px;
  box-shadow: var(--shadow);
}
.warn-card.show { display: block; }
.warn-card strong {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 700;
  color: var(--danger-ink);
  display: block;
  margin-bottom: 3px;
  font-size: 17px;
}

.microtrust {
  font-size: 13.5px;
  color: var(--ink-soft);
}
.microtrust svg { width: 15px; height: 15px; flex: none; color: var(--brand); }

/* ---------- Auth forms ---------- */
.auth-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px;
  max-width: 420px;
  margin: 0 auto;
}
/* Signup carries more fields than the other auth forms (name pair, contact
   details, optional business block), so it gets extra width to keep the
   two-column name row from feeling cramped. */
.auth-card-wide {
  max-width: 540px;
}
.auth-card .form-label {
  font-weight: 600;
  font-size: 14px;
}
.auth-card .form-control {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 10px 13px;
}
.auth-card .form-control:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 0.2rem var(--brand-tint);
}
.auth-card .btn-cta {
  width: 100%;
}
.auth-card .validation-summary-errors ul {
  margin-bottom: 0;
  padding-left: 1.1em;
}

/* ---------- Settings (profile) pages ---------- */
.settings-nav {
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 10px;
  box-shadow: var(--shadow);
}
.settings-nav a {
  padding: 10px 14px;
  border-radius: 4px;
  color: var(--ink);
  font-weight: 600;
  font-size: 14.5px;
}
.settings-nav a:hover {
  background: var(--paper);
  color: var(--brand-ink);
}
.settings-nav a.active {
  background: var(--brand-tint);
  color: var(--brand-ink);
}

/* The side menu (Views/Shared/_SideMenu), on for every account. Styled off .settings-nav
   above deliberately: it is the same thing, a column of destinations beside the page, and
   two navs in one app that look different for no reason is one too many.

   Hidden outright below 992px. Below that Bootstrap's .container is 720px or less, and a
   220px column out of it leaves the invoice tables it sits beside nothing to work with. The
   dashboard tiles come back at exactly the same width (see .side-menu ~ main below), so a
   narrow window always has one or the other and never neither, which is why the switch
   between them is made here in CSS rather than by the server deciding what to render. */
.side-menu {
  display: none;
}
@media (min-width: 992px) {
  /* Only when there is a menu to place: every other page keeps the plain block layout it
     has always had, rather than every page in the app becoming a flex container. */
  .page-content:has(> .side-menu) {
    display: flex;
    align-items: flex-start;
    gap: 28px;
  }
  /* min-width:0 or a wide table inside main refuses to shrink and pushes the menu off. */
  .page-content:has(> .side-menu) > main {
    flex: 1 1 auto;
    min-width: 0;
  }
  .side-menu {
    display: flex;
    flex-direction: column;
    gap: 4px;
    flex: 0 0 220px;
    /* Stays put while a long list of invoices scrolls past it: the whole point of moving
       navigation out of the page. Offset clears the gap under the navbar. */
    position: sticky;
    top: 24px;
    margin-top: 24px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 10px;
    box-shadow: var(--shadow);
  }
  .side-menu a {
    padding: 10px 14px;
    border-radius: 4px;
    color: var(--ink);
    font-weight: 600;
    font-size: 14.5px;
  }
  .side-menu a:hover {
    background: var(--paper);
    color: var(--brand-ink);
  }
  .side-menu a.active {
    background: var(--brand-tint);
    color: var(--brand-ink);
  }
  /* The account, not the work. A rule and a little air separate it from the list above
     rather than a heading, which would be three words to label one link. */
  .side-menu-account {
    margin-top: 6px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
    border-radius: 0 0 4px 4px;
  }
  /* The dashboard tiles are the same links laid out across the page, so showing both is
     just saying everything twice. Hidden rather than not rendered, so shrinking the window
     hands them straight back at the breakpoint above.

     Only the dashboard's own set: .quick-links is also how the reports index lists the six
     reports, and the side menu has one "Reports" entry rather than those six, so hiding
     them there took away the only way into the customer ledger. */
  .side-menu ~ main .quick-links-dash {
    display: none;
  }
}

.settings-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px;
}
.settings-card .form-label {
  font-weight: 600;
  font-size: 14px;
}
.settings-card .form-control {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 10px 13px;
}
.settings-card .form-control:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 0.2rem var(--brand-tint);
}
.settings-card .form-text {
  color: var(--ink-soft);
  font-size: 13px;
}
/* The current VAT pricing, set apart from the button that changes it, a tradesman should
   be able to see which way his prices are read without reading the button. */
.vat-treatment {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 14px 16px;
}

/* ---------- List toolbar & pager (Invoices, Quotes, Statements) ---------- */
.list-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}
.list-toolbar-search { flex: 1 1 220px; }
.list-toolbar-month { flex: 0 1 160px; }
.list-toolbar-pagesize { flex: 0 1 120px; }
/* The reports' year picker. Narrower than the page-size box next to it in the other
   toolbars, since four digits is all it ever holds. */
.list-toolbar-year { flex: 0 1 110px; }
/* The reports toolbar has no search box to explain itself, so the one control there gets
   a visible label rather than only an aria-label. */
.report-year-label {
  align-self: center;
  font-weight: 600;
  font-size: 14px;
  color: var(--ink-soft);
}
/* Only rendered while a month is actually filtering, so it must not take space from the
   controls that are always there. */
.list-toolbar-clear { flex: 0 0 auto; }
@media (max-width: 991.98px) {
  .list-toolbar-search { flex: 1 1 100%; }
}

.list-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 18px;
}
.list-pager-status {
  color: var(--ink-soft);
  font-size: 14px;
  white-space: nowrap;
}

/* ---------- Reports ---------- */
/* A figure is easier to place against the month beside it than against nothing, so the
   income and customer tables carry a bar drawn to scale. It is a reading aid only, the
   number is always right there, which is why it's hidden on a phone (d-none d-md-*) and
   aria-hidden everywhere: nothing is lost by not seeing it. */
.report-bar {
  display: inline-block;
  vertical-align: middle;
  width: 100%;
  min-width: 56px;
  height: 8px;
  border-radius: 999px;
  background: var(--paper);
  border: 1px solid var(--line);
  overflow: hidden;
}
.report-bar-fill {
  display: block;
  height: 100%;
  background: var(--brand);
}
.report-bar-value {
  display: block;
  font-size: 12px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* The bar always follows a right-aligned money column, and .statement-table .num sets
   padding-right: 0 so the figure sits flush against whatever is next. Nothing then
   separates them: the bar starts against the last digit, and the two headings run
   together as "BilledShare". The gap has to live on this column rather than on .num,
   which is deliberately flush everywhere else. */
.statement-table .report-bar-cell { padding-left: 16px; }
/* The standing notes under a report table, how a figure was arrived at, what it leaves
   out. Same voice as .form-text, but these sit under a table rather than a field. */
.report-note {
  color: var(--ink-soft);
  font-size: 13.5px;
  margin: 14px 0 0;
}

/* ---------- Admin ---------- */
/* The reports tab strip shares these rules rather than copying them: it is the same
   thing, one row of sibling pages, and two nearly-identical strips would drift. */
.admin-tabs,
.report-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 26px;
  border-bottom: 1px solid var(--line);
}
.admin-tabs a,
.report-tabs a {
  padding: 9px 15px;
  font-weight: 600;
  font-size: 14.5px;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.admin-tabs a:hover,
.report-tabs a:hover { color: var(--ink); }
.admin-tabs a.active,
.report-tabs a.active {
  color: var(--brand-ink);
  border-bottom-color: var(--brand);
}

/* ---------- Dashboard quick links ---------- */
.quick-links {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
  margin-bottom: 26px;
  /* Every tile the same height whatever its subtitle does. Without this a row is only as
     tall as its own tallest card, so "What each customer owes you" wrapping to two lines
     makes one row taller than the next and the block looks like two separate things. */
  grid-auto-rows: 1fr;
}
/* auto-fit fits as many as it can, which strands the last row whenever the number of tiles
   is not a multiple of the number that fit: at a desktop width it picks five, so eight tiles
   come out 5+3 and six come out 5+1, with the leftovers sitting against a wide empty gap.

   Where the count is known and fixed, the page names the column count that divides it
   evenly instead: ten into five, six into three. Only above 992px: below it auto-fit is
   right, because five tiles across a tablet are too narrow to read. Anything with a count
   that varies is left to auto-fit throughout. */
@media (min-width: 992px) {
  .quick-links-cols-3 {
    grid-template-columns: repeat(3, 1fr);
  }
  .quick-links-cols-4 {
    grid-template-columns: repeat(4, 1fr);
  }
}
/* Five needs the room three and four do not: at 992px it is under 190px a tile, which is
   narrower than the minimum auto-fit is allowed to go. Below 1200px the Pro dashboard is
   left to auto-fit, which strands two rather than squashing all ten. */
@media (min-width: 1200px) {
  .quick-links-cols-5 {
    grid-template-columns: repeat(5, 1fr);
  }
}
.quick-links a {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  text-decoration: none;
  transition: border-color .15s ease, transform .15s ease;
}
.quick-links a:hover {
  border-color: var(--brand);
  transform: translateY(-1px);
}
.quick-links .ql-mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 4px;
  background: var(--brand-tint);
  color: var(--brand-ink);
}
.quick-links .ql-mark svg { width: 20px; height: 20px; }
.quick-links .ql-text { display: flex; flex-direction: column; min-width: 0; }
.quick-links .ql-title { font-weight: 700; color: var(--ink); }
.quick-links .ql-sub { font-size: 13px; color: var(--ink-soft); }

.panel-link {
  font-size: 14px;
  font-weight: 600;
  color: var(--brand-ink);
  text-decoration: none;
}
.panel-link:hover { text-decoration: underline; }

.tab-count {
  display: inline-block;
  margin-left: 4px;
  padding: 1px 6px;
  font-size: 12px;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink-soft);
}

/* ---------- Dashboard quick start ----------
   Sits above the stat tiles so a phone screen reaches it without scrolling: the chat
   itself lives on its own page (see the comment on BotController.Index), but starting
   one used to mean a full scroll down the dashboard first. */
.quick-start-panel { margin-bottom: 20px; }
.quick-start-compact {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
}
.quick-start-label {
  font-weight: 700;
  color: var(--ink);
}
.quick-start-form .form-control { margin-bottom: 10px; }

/* ---------- Bot chat ---------- */
.chat-thread {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 380px;
  overflow-y: auto;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--paper);
  margin-bottom: 12px;
}
/* The chat has a page of its own, so the thread gets the room the screen allows rather
   than the 380px it had while sharing the dashboard with everything else. */
.chat-thread.is-tall {
  max-height: min(58vh, 620px);
  min-height: 260px;
}
.chat-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: nowrap;
  margin-bottom: 18px;
}
.chat-page-head > :first-child {
  min-width: 0;
  flex: 1 1 auto;
}

/* The dashboard's way in to the chat. */
.chat-cta-lead {
  color: var(--ink-soft);
  font-size: 14.5px;
  margin-bottom: 10px;
}
.chat-cta-eg {
  font-size: 13.5px;
  color: var(--ink-soft);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 9px 11px;
  margin-bottom: 14px;
  word-break: break-word;
}
/* The unanswered question, quoted back. Clamped because some bot replies run long and
   this is a nudge to go and finish, not the conversation itself. */
.chat-cta-quote {
  max-width: 100%;
  margin-bottom: 14px;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: pre-line;
}

.chat-msg {
  max-width: 85%;
  padding: 9px 13px;
  border-radius: 6px;
  font-size: 14.5px;
  line-height: 1.45;
  white-space: normal;
  word-break: break-word;
}
.chat-msg.from-bot {
  align-self: flex-start;
  background: var(--card);
  border: 1px solid var(--line);
  border-bottom-left-radius: 4px;
}
.chat-msg.from-me {
  align-self: flex-end;
  background: var(--brand);
  color: #fff;
  border-bottom-right-radius: 4px;
}
.chat-msg em { opacity: .85; }
.chat-msg a { color: inherit; }

/* The forward-to-customer button. Deliberately the loudest thing in the thread: it is
   what the tradesman came to do, and on a phone it has to be a thumb-sized target. */
.chat-wa {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 6px 0 2px;
  padding: 9px 15px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff !important;
  font-weight: 600;
  text-decoration: none;
  min-height: 40px;
}
.chat-wa::before,
.chat-wa-btn::before {
  content: "";
  width: 17px;
  height: 17px;
  background: currentColor;
  /* WhatsApp glyph, inlined so the button needs no network request and cannot
     half-render on a bad signal. */
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2zm5.8 14.14c-.25.69-1.43 1.32-1.99 1.4-.51.08-1.15.11-1.86-.12-.43-.14-.98-.32-1.69-.62-2.97-1.28-4.91-4.27-5.06-4.47-.15-.2-1.21-1.6-1.21-3.06s.77-2.17 1.04-2.47c.27-.3.59-.37.79-.37.2 0 .39 0 .57.01.18.01.43-.07.67.51.25.6.85 2.06.92 2.21.07.15.12.32.02.52-.1.2-.15.32-.3.5-.15.18-.31.39-.44.53-.15.15-.3.31-.13.6.17.3.77 1.27 1.65 2.05 1.14 1.01 2.09 1.33 2.39 1.48.3.15.47.12.65-.07.18-.2.75-.87.95-1.17.2-.3.4-.25.67-.15.27.1 1.72.81 2.02.96.3.15.5.22.57.35.07.12.07.72-.18 1.41z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.chat-wa:hover { filter: brightness(1.07); }

/* Replaces the message box once the chat has handed over an invoice or quote: there is
   nothing left to type, so the way out is what's on screen instead. */
.chat-done {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
/* Carries the same WhatsApp mark as the in-thread link (see .chat-wa::before above), but
   on the squared button shape the rest of the app's actions use, a pill here would read
   as a status badge rather than something to press. */
.chat-wa-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* The message box on its own row, with Send and Cancel chat as normal-sized buttons
   side by side underneath. Send lives inside .chat-form together with the textarea,
   and Cancel is a separate <form>, so both forms render as `display: contents` here,
   that lifts the textarea and both buttons into .chat-compose's own flex-wrap row,
   where the textarea's 100% flex-basis forces a wrap and the buttons fall in together
   on the line below it, rather than being stuck at the width of whichever form they're in. */
.chat-compose {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 8px;
}
/* Right-aligned has room to breathe on a wider screen, but on a phone-width chat panel
   right-aligned reads as shoved into a corner, centered sits better once the row is
   this narrow. */
@media (max-width: 575px) {
  .chat-compose { justify-content: center; }
}
.chat-compose .chat-form,
.chat-compose .chat-cancel {
  display: contents;
}

.chat-compose .btn { flex: 0 0 auto; }
/* A textarea, not the single-line input this replaced, so a long message, the usual
   case on a phone typing out several line items, wraps and stays fully visible
   instead of scrolling sideways inside the box. Resizable vertically only, so the Send
   button beside it never has to follow the box sideways. flex-basis 100% is what forces
   Send and Cancel chat onto the row below it (see .chat-compose above). */
.chat-textarea {
  flex: 1 1 100%;
  resize: vertical;
  min-height: 64px;
  line-height: 1.4;
}

.chat-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
  font-size: 12.5px;
  color: var(--ink-soft);
}

/* ---------- Parsed message ---------- */
.parsed-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.parsed-field {
  flex: 1 1 200px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
}
.parsed-field .k {
  display: block;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-weight: 700;
}
.parsed-field .v { font-weight: 600; }
.parsed-field .missing { color: var(--danger-ink); font-weight: 600; }

.questions {
  padding: 14px 16px;
  border: 1px solid var(--warning-soft-border);
  border-radius: 5px;
  background: var(--warning-soft-tint);
}
.questions h3 { font-size: 15px; margin-bottom: 8px; }
.questions ul { margin: 0 0 8px; padding-left: 1.15em; }
.questions li { margin-bottom: 4px; }
.questions p { font-size: 13px; color: var(--ink-soft); }

.statement-table tfoot tr.sub td {
  padding-top: 6px;
  font-weight: 400;
  color: var(--ink-soft);
}

.alert-card {
  padding: 14px 16px;
  border: 1px solid var(--danger-tint-line);
  border-radius: 5px;
  background: var(--danger-tint);
  color: var(--danger-ink);
  font-weight: 600;
}

/* The same shape as .alert-card, for something that needs reading rather than worrying
   about: a standing note at the top of an admin page. Every colour is a variable, so it
   flips with the theme: this replaces a hardcoded #fff3d6 dropped inline on .success-card,
   which left dark mode drawing light text on a light panel and reading as an empty box. */
.notice-card {
  padding: 14px 16px;
  border: 1px solid var(--warning-soft-border);
  border-radius: 5px;
  background: var(--warning-soft-tint);
  color: var(--ink);
}
.notice-card strong { display: block; margin-bottom: 4px; }
.notice-card span { color: var(--ink-soft); }

/* ---------- Logged bot transcripts (admin) ---------- */
/* Read as a conversation rather than a table: what an admin is looking for is where the
   bot answered the wrong thing, and that only shows when the two sides sit as they were
   said. Deliberately plainer than the real chat bubbles: this is a record being read,
   not a chat being had. */
.chat-line {
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.chat-line:last-child { border-bottom: none; }
.chat-line-who {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 3px;
}
.chat-line-when {
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  margin-left: 8px;
}
/* Whitespace kept: a reply is several lines and reads as nonsense run together. */
.chat-line-text { white-space: pre-wrap; }
.chat-line-bot .chat-line-text { color: var(--ink-soft); }

/* ---------- Demo mode ---------- */
/* A standing strip under the navbar while the books are made up. Deliberately the warning
   colour rather than the brand's: it is a state to leave, not a feature to enjoy. Full
   width so it reads as part of the chrome rather than as content on the page. */
.demo-mode-bar {
  background: var(--warning-tint);
  border-bottom: 1px solid var(--warning-soft-border);
  color: var(--warning);
  font-size: 14px;
}
.demo-mode-bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 9px;
  padding-bottom: 9px;
}
/* Wraps rather than truncates: on a phone the sentence is already cut down to the two
   words that matter (see _DemoBanner), and what is left must stay readable. */
.demo-mode-bar-text { min-width: 0; }
.demo-mode-bar-action {
  flex: 0 0 auto;
  font-weight: 600;
  color: var(--warning);
  text-decoration: underline;
  white-space: nowrap;
}
.demo-mode-bar-action:hover { color: var(--ink); }

/* ---------- Deployment banner ---------- */
/* Only ever rendered when Deployment:Label is set, so the live site never carries this.
   Deliberately one fixed colour pair rather than theme variables: purple appears nowhere
   else in the palette, so the strip cannot be mistaken for brand furniture, and a test
   environment should look identical whichever theme the tester is in. It sits above the
   navbar rather than inside it so it survives the page being scrolled to the top on a
   phone, which is where a screenshot of "this looks wrong" is usually taken. */
.env-bar {
  background: #5b2a86;
  color: #fff;
  padding: 7px 12px;
  font-size: 13px;
  line-height: 1.35;
  text-align: center;
}
.env-bar strong {
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The footer half of the same signal. Small, because by the time somebody is at the bottom
   of the page the strip at the top has already done the work, and this is only here so a
   screenshot that misses the top still says which box it came from. */
.env-mark {
  display: inline-block;
  margin-right: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  background: #5b2a86;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The signup checkbox, boxed so it reads as an offer rather than another field. */
.demo-optin {
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--paper);
}

/* ---------- Price list ---------- */
.keyword-chip {
  display: inline-block;
  margin: 0 4px 3px 0;
  padding: 2px 9px;
  font-size: 12.5px;
  border-radius: 999px;
  background: var(--paper);
  border: 1px solid var(--line);
  color: var(--ink-soft);
}
.unit-label {
  display: block;
  font-size: 12.5px;
  color: var(--ink-soft);
  font-weight: 400;
}
.link-danger-btn {
  background: none;
  border: none;
  padding: 0 0 0 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--danger-ink);
  cursor: pointer;
}
.link-danger-btn:hover { text-decoration: underline; }

/* ---------- Statements ---------- */
.statement-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  /* nowrap on purpose: the title block shrinks and wraps its own text (min-width: 0
     below) rather than the back button dropping onto its own line on a narrow phone. */
  flex-wrap: nowrap;
  gap: 12px;
  margin-bottom: 20px;
}
.statement-head > :first-child {
  min-width: 0;
  flex: 1 1 auto;
}
/* The nowrap above suits a lone back button, but a .btn-pair beside a title leaves
   both fighting over a phone's width, there the pair drops below the title and the
   two buttons share the full row. */
@media (max-width: 575px) {
  .statement-head:has(.btn-pair) { flex-wrap: wrap; }
  .statement-head .btn-pair { flex: 1 1 100%; }
}
/* A real button, not just a link: installed PWAs show no browser chrome, so this is
   often the only way back to the list a user came from. Brand blue rather than the
   neutral button styling elsewhere, so it reads clearly against a busy header on a
   small screen instead of blending into the background. */
.back-link {
  display: inline-block;
  flex: 0 0 auto;
  font-size: 14px;
  font-weight: 700;
  color: var(--brand-ink);
  background: var(--brand-tint);
  border: 1px solid var(--brand-tint-line);
  border-radius: 4px;
  padding: 8px 16px;
  text-decoration: none;
  white-space: nowrap;
}
.back-link:hover,
.back-link:focus {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

a.statement-row { text-decoration: none; color: inherit; }
a.statement-row:hover { background: var(--paper); }

.text-overdue { color: var(--danger-ink); font-weight: 600; }

/* Wide tables scroll inside their panel rather than the page going sideways. */
/* min-width: 0 so this actually scrolls rather than growing. A table's minimum content
   width otherwise pushes right through an auto-width scroller and widens everything
   around it, which on a phone widens the page itself. */
.table-scroll { overflow-x: auto; min-width: 0; max-width: 100%; }
.statement-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14.5px;
}
.statement-table th {
  text-align: left;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-weight: 700;
  padding: 0 10px 8px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.statement-table td {
  padding: 11px 10px 11px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
/* Same fix as .doc-table .num: padding-right stays 0 so Amount sits flush against the
   table's right edge, but with no left padding either, adjacent numeric columns (Qty,
   Price) had nothing separating them, invisible until a wide amount closes the gap. */
.statement-table .num { text-align: right; padding-left: 10px; padding-right: 0; }
.statement-table .ref-cell { font-weight: 600; white-space: nowrap; }

/* NewFormal's line-items table (add-a-client invoice/quote) keeps Description, Qty,
   Unit price and the remove button as fixed-width table columns down to phone widths,
   which left too little room for the price-list picker and description input: their
   text was truncating instead of just wrapping. Below 576px only the description
   breaks out: it takes the full row on its own, and Qty, Unit price and the remove
   button stay together on the line underneath, as they do on a wide screen. */
.line-items-table .line-field-label {
  display: none;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-weight: 700;
  margin-bottom: 2px;
}
@media (max-width: 575.98px) {
  .line-items-table thead { display: none; }
  .line-items-table tr.line-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    column-gap: 10px;
    row-gap: 6px;
    padding: 12px 0;
    border-bottom: 1px solid var(--line);
  }
  .line-items-table td {
    display: block;
    border-bottom: none;
    padding: 0;
  }
  .line-items-table .line-desc { flex: 1 1 100%; }
  /* flex-basis 0 with min-width 0 lets the two number boxes shrink to share one line
     rather than one of them wrapping away onto a third. */
  .line-items-table .num {
    flex: 1 1 0;
    min-width: 0;
    text-align: left;
    padding-left: 0;
  }
  .line-items-table .line-remove { flex: 0 0 auto; }
  /* The hidden thead takes Qty and Unit price's headings with it, so each number box
     carries its own label here. */
  .line-items-table .line-field-label { display: block; }
}

/* The parsed-message preview (Try/Preview demo) squeezes 4 columns into a phone-width
   panel. The uppercase, nowrap headers were the biggest offender, headers held their
   full width on one line regardless of how narrow the column's actual data is, so
   letting headers wrap and trimming the type scale buys back enough room that
   .table-scroll's horizontal bar shouldn't be needed here. The item label reuses
   .ref-cell for its bold weight but overrides the nowrap: a matched item's description
   can run longer than the short phrases .ref-cell was written for, and holding it to
   one line would reopen the overflow this block exists to close. */
@media (max-width: 480px) {
  .parsed-lines-table { font-size: 12.5px; }
  .parsed-lines-table th {
    white-space: normal;
    padding-right: 6px;
  }
  .parsed-lines-table td { padding-right: 6px; }
  .parsed-lines-table .ref-cell { white-space: normal; }
  .parsed-lines-table .num { padding-left: 6px; }
}
.statement-table tfoot td {
  border-bottom: none;
  padding-top: 14px;
  font-weight: 700;
}
.statement-table tfoot .balance { font-size: 17px; color: var(--amber); }

.logo-preview {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
}
.logo-preview img {
  /* Bounded the same way the PDF bounds it, so what is shown here is a fair likeness. */
  max-height: 52px;
  max-width: 180px;
  object-fit: contain;
}

/* The template preview iframe on the invoice template settings page. Always white: a PDF
   preview is a printed document, not app chrome, the same reasoning as the public
   document/statement pages never picking up dark mode. */
.template-preview-frame {
  width: 100%;
  height: 70vh;
  min-height: 420px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #fff;
}

.colour-field {
  display: flex;
  gap: 8px;
  align-items: center;
}
.colour-field input[type="color"] {
  flex: 0 0 auto;
  width: 46px;
  padding: 4px;
}
.colour-field input[type="text"] {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-transform: uppercase;
}

.mini-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px dashed var(--line);
  font-size: 14.5px;
}
.mini-row:last-child { border-bottom: none; }
.mini-row .k { color: var(--ink-soft); }
.mini-row .v {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  white-space: nowrap;
}
.mini-row .path {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.visits-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
.visits-table th,
.visits-table td {
  text-align: left;
  padding: 9px 20px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.visits-table th {
  font-weight: 600;
  color: var(--ink-soft);
  font-size: 12.5px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.visits-table tbody tr:last-child td { border-bottom: none; }
.visits-table .mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
}
.visits-table .nowrap { white-space: nowrap; color: var(--ink-soft); }

/* ---------- Dashboard ---------- */
.dash-greeting-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 12px;
  margin-bottom: 4px;
}
.dash-greeting {
  font-size: clamp(26px, 3.6vw, 34px);
  margin-bottom: 0;
}
.dash-sub {
  color: var(--ink-soft);
  margin-bottom: 28px;
}
/* The plan badge is a link (to Subscription/Index), not a status tag, so unlike a
   plain .badge-status it gets an underline-on-hover and a pointer, same "this is
   clickable, not just information" cue the pill-vs-button pass gave .pdf-link. */
.plan-badge {
  text-decoration: none;
  cursor: pointer;
}
.plan-badge:hover { text-decoration: underline; }

.stat {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: var(--shadow);
  padding: 20px;
  height: 100%;
}
/* Stat cards on the dashboard are links into the filtered invoice list;
   keep them looking like cards, not hyperlinks. */
a.stat {
  display: block;
  text-decoration: none;
  color: inherit;
}
a.stat:hover {
  border-color: var(--brand-ink);
}
.stat .label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 8px;
}
.stat .value {
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 800;
  font-size: 28px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.stat .meta {
  font-size: 13.5px;
  color: var(--ink-soft);
  margin-top: 6px;
}
.stat.is-owed .value { color: var(--amber); }
.stat.is-overdue { border-color: var(--danger-tint-line); }
.stat.is-overdue .value { color: var(--danger-ink); }
.stat.is-paid .value { color: var(--brand-ink); }

.panel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line);
}
.panel-head h2 {
  font-size: 17px;
  margin: 0;
}
.panel-body { padding: 20px; }
/* The line-items table on an invoice/quote's own Details page is a plain Bootstrap .table
   with four columns, no phone treatment of its own, and no room to spare once "Unit price
   (incl VAT)" is in the header, it's the widest thing on the page below 400px or so. */
@media (max-width: 575px) {
  .panel-body > table.table {
    font-size: 12.5px;
  }
  .panel-body > table.table th,
  .panel-body > table.table td {
    padding-left: 4px;
    padding-right: 4px;
  }
}
/* .panel-head is shared by ~50 views, almost all of which put one short badge in the
   second slot, so it stays a plain nowrap row by default. The statement page instead
   puts up to five buttons there, and "Statement of account" sharing a row with them
   squeezed the button group into less width than the panel actually had, wrapping it
   two or three to a line with a stray button left dangling below a lot of empty space.
   .panel-actions marks that button group so only pages using it get this treatment: the
   heading takes its own line and the button group drops below it at the panel's full
   width, where it wraps evenly because it is no longer sharing a row with anything.
   Unconditional rather than only on a phone, unlike .statement-head's .btn-pair above,
   since five buttons crowd a tablet-width panel too. */
.panel-head:has(.panel-actions) { flex-wrap: wrap; }
/* The controls keep their natural button size and pack from the left under the heading,
   reading in the order the row is written (month first): on a wide screen the spare
   width sits to the right rather than each button stretching to fill its line. Opening
   "Your own dates" drops only its form onto a full-width line of its own; see
   .statement-range further down. */
.panel-head .panel-actions {
  flex: 1 1 100%;
}
/* The month select is grown to the buttons' 48px and their 200px minimum so the strip
   reads as one set of same-sized controls; a long month name can still widen it past
   that, the way a long label widens a button. Elsewhere (the ledger's panel head) the
   same form keeps its small select. */
.panel-head .panel-actions .form-select {
  min-height: 48px;
  min-width: 200px;
}
/* Tracks the phone button minimum further up, so select and buttons shrink together. */
@media (max-width: 575px) {
  .panel-head .panel-actions .form-select { min-width: 160px; }
}

/* The add/edit form that sits above a long list (clients, price list), shut until it is
   wanted so the list stays the first thing on the page. The <summary> wears .panel-head, so
   it is the same furniture as every other panel and only the chevron says it opens.
   min-height and the padding it inherits keep it a comfortable tap target on a phone. */
.record-form > summary {
  cursor: pointer;
  /* Both needed: list-style kills the marker in Firefox, the pseudo-element in Safari.
     Without them the browser's own triangle sits alongside our chevron. */
  list-style: none;
}
.record-form > summary::-webkit-details-marker { display: none; }
.record-form > summary::after {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--ink-soft);
  border-bottom: 2px solid var(--ink-soft);
  transform: translateY(-2px) rotate(45deg);
}
.record-form[open] > summary::after {
  transform: translateY(2px) rotate(225deg);
}
.record-form > summary:hover h2,
.record-form > summary:focus-visible h2 {
  color: var(--brand-ink);
}

.inv-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--line);
}
.inv-row:last-child { border-bottom: none; }
.inv-row .who { font-weight: 600; color: var(--ink); }
.inv-row .ref { font-size: 13px; color: var(--ink-soft); }
.inv-row-link { color: inherit; text-decoration: none; }
.inv-row-link:hover .who { text-decoration: underline; }
.inv-row .amt {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}
/* On a phone, five things across (name, badge, amount, PDF, Send) squeeze the customer
   name down to a sliver, and whatever doesn't fit wraps raggedly on its own. Same card
   shape as the stacked tables instead: the name block owns the first line, the badge
   and money share the second (.amt's auto margin pushes the money right), and the
   PDF/Send pills get the bottom line to themselves. */
@media (max-width: 575px) {
  .inv-row { flex-wrap: wrap; row-gap: 6px; }
  .inv-row > :first-child { flex: 1 1 100%; }
  .inv-row .action-pills {
    flex: 1 1 100%;
    justify-content: flex-start;
  }
}

/* ---------- Help manual (Views/Help) ---------- */
.manual-hub {
  display: grid;
  gap: 12px;
  margin-top: 8px;
}
.manual-guide-card {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 18px 20px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: var(--shadow);
  color: inherit;
  text-decoration: none;
}
.manual-guide-card:hover { border-color: var(--brand-ink); }
.manual-guide-mark {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 4px;
  background: var(--brand-tint);
  color: var(--brand-ink);
}
.manual-guide-mark svg { width: 22px; height: 22px; }
.manual-guide-title { font-weight: 700; margin-bottom: 3px; }
.manual-guide-sub { color: var(--ink-soft); font-size: 14px; }

.manual { max-width: 760px; margin: 0 auto; }
.manual-steps {
  list-style: none;
  margin: 32px 0 0;
  padding: 0;
  counter-reset: none;
}
.manual-step {
  padding: 28px 0;
  border-top: 1px solid var(--line);
}
.manual-step:first-child { border-top: none; padding-top: 12px; }
.manual-step-body h2 {
  font-size: 19px;
  margin-bottom: 8px;
}
/* A heading above a run of steps, for the one guide that covers two separate jobs on two
   separate pages. Set apart from the step headings under it so a reader can tell "here is
   the second half of this guide" from "here is the next step". */
.manual-section {
  margin: 40px 0 0;
  padding-bottom: 10px;
  border-bottom: 2px solid var(--brand);
  font-size: 21px;
}
.manual-section:first-of-type { margin-top: 28px; }
.manual-step-body p {
  color: var(--ink-soft);
  margin-bottom: 10px;
}
.manual-step-body p:last-child { margin-bottom: 0; }
/* Only the export guide lists things (which file is which, which package to pick), and a
   bulleted list has to read as the same voice as the paragraphs around it rather than
   jumping darker. */
.manual-step-body ul {
  color: var(--ink-soft);
  margin: 0 0 10px;
  padding-left: 1.2em;
}
.manual-step-body li { margin-bottom: 6px; }
/* A quieter, boxed-off note rather than a plain paragraph, an aside about how a step
   differs for Basic or for a quote, not the main instruction. */
.manual-note {
  padding: 10px 14px;
  background: var(--paper);
  border-radius: 4px;
  font-size: 14px;
}
.manual-step-shot {
  margin-top: 16px;
  border: 1px solid var(--line);
  border-radius: 5px;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.manual-step-shot img {
  display: block;
  width: 100%;
  height: auto;
}
/* A <picture> is inline by default, so the block image inside it would sit on a text
   baseline and leave a few pixels of line box under it, visible here because the card
   has a border drawn tight around the shot. */
.manual-step-shot picture {
  display: block;
}
/* A step with both a web and an Android screenshot, the phone shot is a tall, narrow
   capture next to the wide desktop one, so it gets a capped width rather than sharing
   the flex row equally, or it would dwarf the web screenshot beside it. */
.manual-step-shots {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin-top: 16px;
}
.manual-step-shots .manual-step-shot {
  margin-top: 0;
  flex: 1 1 0;
  min-width: 0;
}
.manual-step-shots .manual-step-shot-mobile {
  flex: 0 0 220px;
}
.manual-step-shot-caption {
  margin-top: 6px;
  font-size: 12.5px;
  color: var(--ink-soft);
  text-align: center;
}
/* On a phone the pair stays a pair. Stacking them used to push the Android shot a screen
   below the website one, which reads as only one screenshot being there, and the point of
   showing two is that they are seen against each other. Both shrink to share the width
   instead.
   Below 768px the website card is already showing its own phone capture (the <picture> in
   the help views swaps to it at 767.98px), so by here both cards hold a phone-shaped image
   and an even split is the honest one: the Android card gives up the fixed 220px it needs beside
   a wide desktop screenshot, which on a 360px screen would leave the website about 120px. */
@media (max-width: 700px) {
  .manual-step-shots {
    gap: 10px;
  }
  .manual-step-shots .manual-step-shot-mobile {
    flex: 1 1 0;
  }
  .manual-step-shot-caption {
    font-size: 11.5px;
  }
}

/* ---------- Statement age analysis (see _StatementAging.cshtml) ---------- */
/* The five columns every tradesman knows from his suppliers' statements. A wrapping flex
   row rather than a table: on a phone five money columns can't sit side by side, and this
   lets them fall onto two or three lines instead of shrinking to nothing or scrolling
   sideways, which for a summary this short would bury half of it. */
.aging {
  margin-top: 20px;
  border-top: 1px solid var(--line);
  padding-top: 14px;
}
.aging-head {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 10px;
}
.aging-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.aging-cell {
  flex: 1 1 110px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
}
.aging-label {
  font-size: 12px;
  color: var(--ink-soft);
}
.aging-amount {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Only a bucket that's actually late and actually holds money, see the partial. */
.aging-cell.is-overdue .aging-amount { color: var(--danger); }

/* ---------- Row as link (the lists and the stacked tables) ---------- */
/* The whole row opens what it describes, not just the name: a row is one target under a
   thumb, and hitting a short name exactly is fiddly on a phone. The anchor still wraps
   only the title text, so it reads as a proper link to a screen reader and copies as one;
   a transparent ::after stretched across the row is what widens the tap area. Everything
   else interactive in the row (PDF/Send/Edit pills, Remove buttons) is lifted above
   that layer, or the overlay would swallow their taps. The cost is that text inside a row
   can no longer be selected, which is the accepted trade for this pattern.
   Statements/Index doesn't need any of it: that row is already one big anchor. */
.inv-row,
.stack-table tbody tr { position: relative; }
.inv-row-link::after,
.row-link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.row-link { color: inherit; text-decoration: none; }
.row-link:hover { text-decoration: underline; }
.inv-row .action-pills,
.stack-table .stack-actions { position: relative; z-index: 1; }
/* Only rows that actually lead somewhere get the hover cue, same wash a.statement-row
   already uses, so a hoverable row looks the same everywhere. */
.inv-row:has(.inv-row-link):hover,
.stack-table tbody tr:has(.row-link):hover { background: var(--paper); }

/* A squared-off corner (not the 999px pill radius badges use) is what tells a tradesman
   this is a button, not a status tag: PDF/Send/Edit sit right next to badge-status pills
   in every row and the two were easy to mistake for each other at pill-and-pill. */
.pdf-link {
  display: inline-block;
  font-size: 12px;
  font-weight: 700;
  color: var(--brand-ink);
  background: var(--brand-tint);
  border: 1px solid var(--brand-tint-line);
  padding: 5px 11px;
  border-radius: 2px;
  white-space: nowrap;
  box-shadow: 0 1px 2px rgba(18, 38, 31, 0.06);
}
/* The same pill on a form's submit button: a row's actions are half links and half
   posts, and a <button> inherits neither the page font nor the pointer cursor. */
button.pdf-link {
  font-family: inherit;
  line-height: inherit;
  cursor: pointer;
}
.pdf-link:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

.badge-status {
  font-size: 12px;
  font-weight: 700;
  padding: 3px 9px;
  border-radius: 999px;
  white-space: nowrap;
}
.badge-draft { background: var(--neutral-tint); color: var(--neutral-ink); }
.badge-sent { background: var(--warning-tint); color: var(--warning); }
.badge-paid { background: var(--brand-tint); color: var(--brand-ink); }
.badge-overdue { background: var(--danger-tint); color: var(--danger-ink); }

.setup-item {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 0;
  border-bottom: 1px dashed var(--line);
}
.setup-item:last-child { border-bottom: none; }
.setup-item .tick {
  width: 22px;
  height: 22px;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 700;
}
.setup-item.done .tick { background: var(--brand-tint); color: var(--brand-ink); }
.setup-item.todo-step .tick { background: var(--warning-tint); color: var(--warning); }
.setup-item .txt { font-size: 14.5px; }
.setup-item.done .txt { color: var(--ink-soft); }

.empty-state {
  text-align: center;
  padding: 36px 20px;
}
.empty-state .mark {
  width: 52px;
  height: 52px;
  margin: 0 auto 14px;
  background: var(--brand-tint);
  border-radius: 6px 6px 6px 2px;
  display: grid;
  place-items: center;
}
.empty-state .mark svg { width: 26px; height: 26px; color: var(--brand-ink); }
.empty-state p { color: var(--ink-soft); margin-bottom: 0; }

/* ---------- Long-form policy pages ---------- */
.policy {
  max-width: 68ch;
  margin: 0 auto;
  padding: 8px 0 48px;
}
.policy h1 {
  font-size: clamp(30px, 4vw, 40px);
  margin-bottom: 8px;
}
.policy .updated {
  color: var(--ink-soft);
  font-size: 14px;
  margin-bottom: 32px;
}
.policy h2 {
  font-size: 20px;
  margin: 36px 0 12px;
}
.policy h3 {
  font-size: 16px;
  font-weight: 700;
  margin: 22px 0 8px;
}
.policy p,
.policy li {
  color: var(--ink-soft);
  margin-bottom: 12px;
}
.policy ul {
  padding-left: 1.2em;
}
.policy a {
  color: var(--brand-ink);
  font-weight: 600;
}
.policy .lead {
  font-size: 17px;
}
.policy table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 16px;
  font-size: 14.5px;
}
.policy th,
.policy td {
  text-align: left;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.policy th {
  color: var(--ink);
  font-weight: 600;
}
.policy .table-scroll {
  overflow-x: auto;
}

/* Unfilled placeholders: deliberately loud so they cannot ship unnoticed. */
.todo {
  background: var(--warning-tint);
  border-bottom: 2px solid var(--amber);
  color: var(--warning);
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 2px;
}

/* ---------- Signature: chat -> invoice demo ---------- */
.demo {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px;
}

.demo-avatar {
  width: 34px;
  height: 34px;
  flex: none;
  background: var(--brand-tint);
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--brand-ink);
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 700;
  font-size: 14px;
}

.bubble {
  align-self: flex-end;
  max-width: 82%;
  background: var(--brand);
  color: #fff;
  padding: 11px 14px;
  border-radius: 6px 6px 2px 6px;
  font-size: 15px;
  line-height: 1.4;
  box-shadow: 0 2px 6px -2px rgba(44, 90, 160, 0.4);
  opacity: 0;
  transform: translateY(8px);
  animation: rise 0.5s ease forwards;
}
.bubble .time { font-size: 11px; opacity: 0.8; display: block; text-align: right; margin-top: 3px; }

/* The bot's side of a sample conversation: same shape as .bubble, mirrored to the
   other side and in grey rather than brand, so a static sample thread reads as "them"
   replying rather than a second thing you said. */
.bubble-bot {
  align-self: flex-start;
  max-width: 82%;
  background: var(--bubble-bg);
  color: var(--ink);
  padding: 11px 14px;
  border-radius: 6px 6px 6px 2px;
  font-size: 15px;
  line-height: 1.4;
  opacity: 0;
  transform: translateY(8px);
  animation: rise 0.5s ease forwards;
}

/* ---------- Footer ---------- */
.footer {
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 14px;
}
.footer a {
  color: var(--ink);
  font-weight: 600;
}
.footer a:hover {
  color: var(--brand-ink);
}
/* The footer was four independent items under justify-content-between with flex-wrap, which
   spreads badly at every width except the widest. Once any two items share a wrapped line,
   space-between shoves them to opposite edges, which is how the Payfast chip and the
   Tolerance one ended up at arm's length with a chasm between them on a small laptop.

   So: centre everything while it is wrapped, and only spread out at the width where all
   three zones genuinely fit on one line. Centred wrapping has no chasms to produce, and the
   footer reads as deliberate at every size rather than only at the one it was designed at. */
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px 28px;
  text-align: center;
}
/* The two brand marks travel as one group. They are the same kind of thing to a reader, and
   grouping them means a wrap can only ever put both on a new line, never strand the
   builder's credit on a line by itself. */
.footer-marks {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px 20px;
}
/* Deliberately not a flex row: the separators between these links are bare text nodes, and
   each would become a flex item of its own with the spacing that implies. Ordinary inline
   text wraps them correctly for free. The roomier line height is for when they do wrap on a
   phone, so two rows of links are not a thumb's width apart. */
.footer-links {
  line-height: 2;
}
@media (min-width: 992px) {
  .footer-inner {
    justify-content: space-between;
    text-align: left;
  }
}

/* Centred on wide screens by sitting between the two other footer items; on a phone the
   footer wraps and it becomes its own line, which is why it carries no fixed width. The
   width/height on the <img> match the artwork's viewBox so the row does not reflow when
   the SVG finishes loading; the CSS height is what actually sizes it. */
.payfast-mark img {
  height: 26px;
  width: auto;
  display: block;
}
/* The wordmark is Payfast blue on transparent, which all but disappears on our dark
   background. Rather than recolour someone else's logo, dark mode gives it the light
   surface it was drawn for. The padding is what makes it read as a deliberate chip
   instead of a hole in the footer. */
[data-bs-theme="dark"] .payfast-mark {
  background: #fff;
  border-radius: 3px;
  padding: 5px 10px;
  display: inline-block;
  line-height: 0;
}

/* The builder's credit. inline-flex keeps the words and the logo on one baseline and stops
   the gap collapsing when the footer wraps on a phone. The artwork is the button version,
   drawn on its own ground, so it needs no theme handling of its own: the height here is
   what sizes it and width auto keeps the proportions if the artwork is ever replaced. */
.built-by {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}
.built-by img {
  height: 30px;
  width: auto;
  display: block;
}

/* ---------- Dialogs: confirm, the new invoice/quote popup, and the statement reminder
   channel picker (see site.js) ---------- */
/* All three share one shell so a popup looks the same whatever asked for it. */
.confirm-dialog,
.quick-start-dialog,
.reminder-dialog {
  border: none;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 24px;
  width: min(92vw, 420px);
  background: var(--card, #fff);
  color: var(--ink);
}
.confirm-dialog::backdrop,
.quick-start-dialog::backdrop,
.reminder-dialog::backdrop {
  background: rgba(18, 38, 31, 0.45);
}
.quick-start-dialog .quick-start-dialog-title,
.reminder-dialog .reminder-dialog-title {
  font-size: 19px;
  margin: 0 0 14px;
}
.quick-start-dialog .form-control { width: 100%; }
/* The fork's three answers stack, each full width, see the note in the partial. Same
   shape for the reminder picker's WhatsApp/Email/Never mind. */
.quick-start-dialog .quick-start-choices,
.reminder-dialog .reminder-choices {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.quick-start-dialog .quick-start-choices .btn,
.reminder-dialog .reminder-choices .btn {
  width: 100%;
  text-align: center;
}
/* The buttons inside are the same pair pattern as the confirm dialog's, so they stack
   full-width on a phone for the same reason. */
@media (max-width: 480px) {
  .quick-start-dialog .quick-start-actions { flex-direction: column; }
  .quick-start-dialog .quick-start-actions .btn { width: 100%; }
}
.confirm-dialog .confirm-title {
  font-size: 19px;
  margin: 0 0 8px;
}
.confirm-dialog .confirm-message {
  color: var(--ink-soft);
  font-size: 15px;
  margin: 0 0 20px;
}
/* Only present when the form asked for a detail with data-confirm-field (see site.js);
   empty and invisible on a plain yes/no, so the margin goes on the rows, not here. */
.confirm-dialog .confirm-field {
  margin-bottom: 16px;
}
.confirm-dialog .confirm-field label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 4px;
}
/* Full width: on a phone a date box left at its natural width sits oddly in a dialog
   this narrow, and the reference is free text that wants the room. */
.confirm-dialog .confirm-field .form-control,
.confirm-dialog .confirm-field .form-select {
  width: 100%;
}
/* The clear-and-confirm button under a field (data-confirm-field-clear in site.js).
   Quiet and small: it sits below the box it empties, not beside the real actions at the
   foot of the dialog, so it reads as part of that field rather than as a third answer. */
.confirm-dialog .confirm-field-clear {
  margin-top: 8px;
  font-size: 14px;
  padding: 6px 12px;
}
/* Who would be folded in, inside the combine-clients dialog (Views/Clients/Index). A list
   rather than a sentence: each line is one customer the tradesman has to recognise, and
   three of them run together unreadably in prose. */
.confirm-dialog .combine-list {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
}
.confirm-dialog .combine-list li {
  padding: 10px 0;
  border-top: 1px solid var(--line);
}
.confirm-dialog .combine-list .ref {
  display: block;
  font-size: 13px;
  color: var(--ink-soft);
}
/* Rendered by the server with `open` so it shows without JavaScript, where it is an
   ordinary block in the page rather than a modal, centred and full width, so it reads as
   the thing being asked rather than as a stray panel. */
.confirm-dialog[data-open-on-load]:not(:modal) {
  margin: 24px auto;
  border: 1px solid var(--line);
}

/* The type-the-word box (data-confirm-word in site.js). Set apart from the ordinary
   detail fields above it by a rule and danger-coloured label, because it is not a detail
   being collected: it is the thing standing between a thumb and a deleted record. */
.confirm-dialog .confirm-word {
  border-top: 1px solid var(--line);
  padding-top: 14px;
}
.confirm-dialog .confirm-word label {
  color: var(--danger-ink);
}
.confirm-dialog .confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}
/* Held until the word is typed. Visibly out of action rather than merely inert, so the
   box above reads as the reason and not as an optional extra. */
.confirm-dialog .confirm-yes:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* Hovering a held button must not darken it the way a live one does, that reads as
   "press me" on the one control the dialog is deliberately withholding. */
.confirm-dialog .confirm-yes:disabled:hover {
  background: var(--brand);
}
.confirm-dialog .confirm-yes.confirm-danger:disabled:hover {
  background: var(--danger);
}
/* On a phone the two buttons share the row awkwardly; stack them full-width with the
   confirming button on top, where the thumb lands first. */
@media (max-width: 480px) {
  .confirm-dialog .confirm-actions { flex-direction: column-reverse; }
  .confirm-dialog .confirm-actions .btn { width: 100%; }
}
.confirm-dialog .confirm-yes.confirm-danger {
  background: var(--danger);
}
.confirm-dialog .confirm-yes.confirm-danger:hover {
  background: var(--danger-deep);
}

/* ---------- Animations ---------- */
@keyframes rise { to { opacity: 1; transform: translateY(0) scale(1); } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }
  .bubble, .bubble-bot { opacity: 1; transform: none; }
  .typing { display: none; }
}

/* The four quote actions (PDF, Send, Accepted, Declined) all render as the same pill
   shape so the row reads as one control strip rather than a mix of pill links and bare
   text buttons. Declined alone goes red: it's the only destructive answer, and one red
   pill per row is enough of a warning. */
.action-pills {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 6px;
}
.quote-pill {
  /* Buttons don't inherit the page font on their own the way the <a> pills do. */
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  padding: 5px 11px;
  /* Squared off like .pdf-link, not the 999px badge-status pills, same reasoning:
     Accepted/Declined sit beside a status badge in the row and must read as buttons. */
  border-radius: 2px;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(18, 38, 31, 0.06);
}
.quote-pill-accept {
  background: var(--brand-tint);
  border: 1px solid var(--brand-tint-line);
  color: var(--brand-ink);
}
.quote-pill-accept:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
.quote-pill-decline {
  background: var(--danger-tint);
  border: 1px solid var(--danger-tint-line);
  color: var(--danger-ink);
}
.quote-pill-decline:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}

/* The shared phone treatment for management grids (quotes, clients, price list,
   statement detail). Multi-column tables with actions never fit a phone, and
   .table-scroll's sideways bar buries the actions off-screen. The page sits in a
   Bootstrap .container, so the widths that matter are the container's tiers, not the
   viewport: 720px between 768–991px viewport, 540px below that. At 720px the grid fits
   once the action pills are allowed to wrap onto a second line (rule below); at 540px
   it doesn't fit at all, so below 768px the table stops being a grid and each row
   stacks its details as its own bordered block: nothing scrolls horizontally and
   every action stays reachable. The view marks each cell's role: stack-title (the
   bold first line), stack-meta (the soft reference line), stack-amt (money, pushed
   right), stack-actions (full-width strip at the bottom). */
/* Hidden wherever the table is still a grid: the column heading says it instead. Turned
   back on in the stacked block below. */
.stack-label { display: none; }
@media (min-width: 768px) and (max-width: 991px) {
  .stack-table .action-pills {
    flex-wrap: wrap;
    /* Wide enough for two pills per line; without a cap the cell just grows and the
       table overflows the 720px container again. */
    max-width: 190px;
    margin-left: auto;
  }
}
@media (max-width: 767px) {
  .stack-table thead { display: none; }
  /* Each row becomes a card shaped like the invoices list: the title cell owns the
     first line, the meta cells form a soft reference line, and the badge, money and
     actions flow underneath. The tr turns into a flex row and CSS order pulls the
     title/meta cells to the front regardless of column order. */
  .stack-table tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 10px;
    row-gap: 4px;
    padding: 12px 0;
    border-bottom: 1px solid var(--line);
  }
  .stack-table td {
    display: block;
    border-bottom: none;
    padding: 0;
    white-space: normal;
  }
  .stack-table .num {
    text-align: left;
    padding-left: 0;
  }
  .stack-table .stack-title {
    flex: 1 1 100%;
    order: -2;
    font-weight: 600;
  }
  .stack-table .stack-meta {
    order: -1;
    font-size: 13px;
    color: var(--ink-soft);
    font-weight: 400;
  }
  /* "issued 21 Jul" and the price's unit ride inline in the meta line instead of
     taking their own row. */
  .stack-table .stack-meta .unit-label,
  .stack-table .stack-amt .unit-label { display: inline; }
  /* A report table carries several money columns, and stacked they lose the header row
     that said which was which, "R400 R1 200" reads as nothing. This names the figure
     on a phone only; on the grid the <th> above it already does that job. */
  .stack-table .stack-label { display: inline; }
  .stack-table .stack-amt {
    margin-left: auto;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  .stack-table .stack-actions { flex: 1 1 100%; }
  .stack-table .action-pills {
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 6px;
  }
  .stack-table tfoot tr { border-bottom: none; }
  .stack-table tfoot td { padding: 2px 0; }
}

/* stack-figures: for the tables that carry several money columns at once, the ledger
   above all (charged, received and a running balance on every line). The treatment
   above pushes each figure right on the same flex line, which works for the one amount
   a quote or an invoice row carries; with three of them they wrap mid-figure and the
   labels come adrift from the money they name. Here each figure takes its own full
   width line, the label on the left and the amount on the right, which is how the same
   numbers are read on a bank statement. Nothing changes above 767px: the column
   headings are back and the grid is unchanged. */
@media (max-width: 767px) {
  .stack-table.stack-figures .stack-amt {
    flex: 1 1 100%;
    /* Undoes the shared rule's push to the right: these are rows now, not one line. */
    margin-left: 0;
    display: flex;
    justify-content: space-between;
    gap: 12px;
  }
  /* The heading is not the figure, and at 600 next to a 600 amount it read as one. */
  .stack-table.stack-figures .stack-label {
    font-weight: 400;
    color: var(--ink-soft);
  }
  .stack-table.stack-figures .stack-blank { display: none; }
  /* The closing total lines up under the balances it is the end of, rather than sitting
     against the label on the left. */
  .stack-table.stack-figures tfoot .balance { margin-left: auto; }
}

/* The month picker sitting in a statement's button row. Sized to its own text rather than
   stretching like a form control normally would, so it reads as one more control in the row
   instead of a field the row was built around. */
.statement-month-form {
  display: flex;
  align-items: center;
  gap: 8px;
}
.statement-month-form .form-select {
  width: auto;
  min-width: 150px;
}

/* "Your own dates" in the statement's button row. The summary is dressed as one more
   small button beside the month select, so a from-and-to is offered exactly where anyone
   wanting a period looks; a <details> for the same reason the more-actions menu is one:
   no script, and the form inside stays an ordinary GET. */
.statement-range > summary {
  /* The .btn classes do the dressing; these strip the disclosure triangle the button
     must not carry. */
  list-style: none;
  cursor: pointer;
}
.statement-range > summary::-webkit-details-marker { display: none; }
/* The form is the details' next sibling, not its child, switched by the open state.
   Inside the details it could never take a row of the strip for itself: putting the
   whole details on a full-width row strands the month select beside empty space, and
   display: contents doesn't free the form either, because Chromium boxes a details'
   body in an internal block slot and that content-sized slot becomes the flex item.
   As a sibling the summary keeps its place in the row and the form is a row item in
   its own right; the toggle is still the native details one, no script. */
.statement-range-form { display: none; }
.statement-range[open] + .statement-range-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  /* A full line of its own; the buttons after it reflow underneath. */
  flex: 1 1 100%;
  /* Matches the row's own 8px gap, so the 200px From, To and Show boxes land exactly
     on the column grid of the buttons above and below. */
  gap: 8px;
}
/* From and To sit above their boxes rather than beside them: on a phone the pair still
   fits one screen row each, and the label can't wrap away from its own field. */
.statement-range-form label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 13px;
  color: var(--ink-soft);
}
.statement-range-form .form-control {
  width: auto;
  /* Matched to the buttons above and the Show button alongside, height and minimum
     width both, so the open form reads as part of the same strip rather than a
     smaller thing dropped under it. */
  min-height: 48px;
  min-width: 200px;
}
/* Tracks the phone button minimum, same as the month select above. */
@media (max-width: 575px) {
  .statement-range-form .form-control { min-width: 160px; }
}

/* The brought-forward line on a statement. Softer than a transaction row because it is not
   one: it is where the month starts from, and reading as an invoice would have the customer
   asking what he was charged for on the first. */
.statement-carried td {
  color: var(--ink-soft);
  font-style: italic;
}
.statement-carried .stack-amt { font-style: normal; }

/* ---------- Public document page (the customer's view) ---------- */
.public-body { background: var(--paper); }

.demo-banner {
  max-width: 720px;
  margin: 0 auto;
  padding: 12px 16px 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 14px;
  color: var(--ink-soft);
}

.doc-page {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px 16px 40px;
  /* width: 100% is what keeps a wide table inside this scrolling rather than widening the
     page. The body is a flex column (see the sticky-footer note near the top), and a flex
     item with auto side margins is sized shrink-to-fit, which takes its width from its
     content's intrinsic size. A scroll container's intrinsic size is its content's, not
     its own box, so .table-scroll passed the statement table's six-column minimum width
     straight up to this element, which grew past the viewport and opened on a phone
     looking zoomed in. Giving it a definite width stops that; max-width and the auto
     margins still centre it on a desktop exactly as before. */
  width: 100%;
  min-width: 0;
}
/* Deliberately literal white, not var(--card): this page is meant to look like a
   printed document regardless of the viewer's own theme preference, and is served
   through _PublicLayout, which never sets data-bs-theme, so it stays this way. */
.doc-card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 28px;
}
/* Older, narrower phones. 28px a side is 56px of the 320px they have, which the line
   items cannot give up: the document ends up wider than the screen and opens looking
   zoomed in. The margin here is a document's margin, not a layout, so trimming it costs
   nothing anyone reads. */
@media (max-width: 380px) {
  .doc-card { padding: 18px; }
  .doc-page { padding-left: 10px; padding-right: 10px; }
}
.doc-head {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.doc-seller { font-size: 18px; font-weight: 800; color: var(--ink); font-family: 'Inter', system-ui, sans-serif; }
.doc-seller-sub { font-size: 13px; color: var(--ink-soft); }
.doc-title-block { text-align: right; }
.doc-title { font-size: 22px; font-weight: 800; color: var(--brand); letter-spacing: .02em; font-family: 'Inter', system-ui, sans-serif; }
.doc-number { font-weight: 700; }

.doc-stamp {
  margin-top: 16px;
  padding: 9px 14px;
  border-radius: 4px;
  font-weight: 700;
  font-size: 14px;
}
.doc-stamp.good { background: var(--brand-tint); color: var(--brand-ink); border: 1px solid var(--brand-tint-line); }
.doc-stamp.warn { background: var(--danger-tint); color: var(--danger-ink); border: 1px solid var(--danger-tint-line); }

.doc-for { margin: 18px 0 6px; }
.doc-for .k { display: block; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); font-weight: 700; }
.doc-for .v { font-size: 17px; font-weight: 600; }

.doc-table { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 15px; }
/* Line items wrap hardest on phones, where the description column is squeezed by the
   qty/rate/amount columns next to it, a smaller size buys back room without touching
   the table's structure. */
@media (max-width: 575px) {
  .doc-table { font-size: 13px; }
  /* The headers are the widest thing on this page once a VAT vendor's basis note is on
     them: "Unit price (incl VAT)" and "Amount (incl VAT)" held on one line each give the
     table a minimum width a phone cannot fit, and the whole document ends up wider than
     the screen, it reads as the page having opened zoomed in. Wrapping costs a second
     line in the header row and nothing else; the numbers underneath still don't wrap.
     Written against thead/tbody rather than a bare .doc-table th: the base rules further
     down this file are the same specificity and come later, so a plainer selector here
     would silently lose to them. */
  .doc-table thead th { white-space: normal; padding-right: 6px; }
  .doc-table tbody td { padding-right: 6px; }
  .doc-table thead .num,
  .doc-table tbody .num { padding-left: 6px; }
}
.doc-table th {
  text-align: left;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-weight: 700;
  padding: 0 8px 8px 0;
  border-bottom: 2px solid var(--brand);
  white-space: nowrap;
}
.doc-table td { padding: 11px 8px 11px 0; border-bottom: 1px solid var(--line); }
/* padding-right stays 0 so Amount sits flush against the table's right edge, but that
   rule was also zeroing the left side, so Qty and Unit price had no gap from the
   column to their right, invisible on wide screens where there's slack to spare, but
   the columns crowd out that slack first on a phone, so a R1,000+ amount ends up
   touching the qty next to it. */
.doc-table .num { text-align: right; padding-left: 8px; padding-right: 0; }

.doc-totals { margin: 16px 0 0; margin-left: auto; max-width: 320px; }
/* Deliberately not called "row": Bootstrap's own .row is a flex grid with negative
   margins, and sharing the name made the totals stack instead of sitting label-left,
   amount-right. */
.doc-totals .trow { display: flex; justify-content: space-between; padding: 4px 0; color: var(--ink-soft); }
.doc-totals .grand {
  margin-top: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--ink);
  font-weight: 800;
  font-size: 19px;
}
.doc-totals .grand span:last-child { color: var(--amber); }

.doc-notice { margin-top: 18px; font-size: 13px; color: var(--ink-soft); font-style: italic; }

/* The pay-online button, above the bank details. Full width on a phone because this is the
   one thing on the page a customer is meant to press, and most of them are reading it in
   WhatsApp's browser one-handed. */
.doc-pay { margin-top: 22px; }
.doc-pay-btn {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 17px;
  padding: 14px 18px;
}
/* The wordmark is Payfast pink-and-blue on transparent, which all but disappears against
   the brand-green button. Rather than recolour someone else's logo, it gets the white chip
   it was drawn for: same treatment as the dark-mode footer mark, see .payfast-mark. */
.doc-pay-logo {
  height: 20px;
  width: auto;
  vertical-align: middle;
  margin-right: 8px;
  margin-bottom: 2px;
  background: #fff;
  border-radius: 3px;
  padding: 3px 6px;
}
.doc-pay-note {
  margin: 10px 0 0;
  font-size: 12.5px;
  color: var(--ink-soft);
  text-align: center;
}

.doc-bank {
  margin-top: 22px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--paper);
}
.doc-bank .k { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); font-weight: 700; margin-bottom: 8px; }
.doc-bank .pairs { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 4px 20px; }
.doc-bank .pairs div { display: flex; justify-content: space-between; gap: 12px; font-size: 14px; }
.doc-bank .pairs span:first-child { color: var(--ink-soft); }
.doc-bank .pairs span:last-child { font-weight: 600; }
.doc-bank .ref-note { margin: 10px 0 0; font-size: 13px; color: var(--ink-soft); }

.doc-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 24px; }
.doc-actions .btn { flex: 1 1 200px; text-align: center; }

.doc-foot { text-align: center; margin-top: 18px; font-size: 12.5px; color: var(--ink-soft); }

/* ---------- Share panel ---------- */
.share-box {
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 14px 16px;
  background: var(--paper);
}
.share-url {
  display: block;
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--card);
  margin: 8px 0 12px;
}

.send-link {
  background: var(--brand-tint);
  color: var(--brand-ink);
  border-color: var(--brand-tint-line);
}

.pdf-viewer-body {
  margin: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
}
.pdf-viewer-bar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 16px;
  background: var(--card);
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}
.pdf-viewer-back {
  font-weight: 600;
  color: var(--brand-ink);
  white-space: nowrap;
}
.pdf-viewer-title {
  flex: 1 1 auto;
  color: var(--ink-soft);
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pdf-viewer-download {
  font-weight: 600;
  font-size: 14px;
  color: var(--brand-ink);
  background: none;
  border: 0;
  padding: 0;
  white-space: nowrap;
}
.pdf-viewer-download:hover { color: var(--brand); }

.pdf-viewer-pages {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.pdf-viewer-loading { color: var(--ink-soft); }
/* Deliberately literal white: this is a rendered image of an actual PDF page, which is
   physically white paper, theming it would make it look wrong against the real file.
   PdfViewer.cshtml also sets Layout = null and never sets data-bs-theme, so the rest
   of this page (.pdf-viewer-bar, .pdf-viewer-body) reads its variables but always gets
   the light-mode :root defaults, leaving the whole viewer consistently light rather
   than a dark bar around a white page. */
.pdf-viewer-page {
  max-width: 100%;
  height: auto;
  background: #fff;
  box-shadow: var(--shadow);
}

/* ---------- Dark mode toggle (see _Layout.cshtml and site.js) ---------- */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 42px;
  height: 42px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--card);
  color: var(--ink-soft);
  cursor: pointer;
}
.theme-toggle:hover,
.theme-toggle:focus-visible {
  border-color: var(--brand);
  color: var(--brand-ink);
}
.theme-toggle-icon {
  width: 19px;
  height: 19px;
}
/* Which glyph is showing says what a click does, not what mode is active: a moon
   ("switch to dark") in light mode, a sun ("switch to light") in dark mode. Swapped
   by the attribute site.js already sets on <html>, so no script has to run just to
   keep the icon in step with it. */
.theme-toggle .icon-sun { display: none; }
[data-bs-theme="dark"] .theme-toggle .icon-sun { display: block; }
[data-bs-theme="dark"] .theme-toggle .icon-moon { display: none; }

/* ---------- Two-factor enrolment and recovery codes ---------- */
/* The QR sits on a forced white plate rather than the page background. Scanners read a
   code by contrast, and in dark mode QRCoder's black modules on a dark panel are close to
   unreadable: the plate keeps it scannable in both themes without generating the SVG
   twice. */
.twofactor-qr {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 12px;
  margin: 12px 0;
  width: max-content;
  max-width: 100%;
}
/* Sized by width alone, with height following the viewBox's square aspect. Pinning both
   would crop the code on a narrow phone, which is what a fixed height did before. */
.twofactor-qr svg {
  display: block;
  width: 190px;
  max-width: 100%;
  height: auto;
}

/* Monospace so the manual-entry key can be read a group at a time, and selectable in one
   go for anyone pasting it into a desktop authenticator.

   Wrapping is deliberately left at its default so lines only ever break at the spaces
   between groups. word-break: break-all here split a group across two lines mid-run
   ("XLXI" showing as "X" then "LXI"), which reads as two separate groups, anyone typing
   the key in by hand from that gets a key that doesn't work and no clue why. */
.twofactor-key {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px;
  line-height: 1.7;
  letter-spacing: 0.06em;
  color: var(--ink);
  background: var(--brand-tint);
  border-radius: 3px;
  padding: 8px 10px;
  margin-top: 6px;
  user-select: all;
}

.twofactor-steps {
  padding-left: 20px;
  color: var(--ink-soft);
  font-size: 15px;
}
.twofactor-steps > li { margin-bottom: 14px; }
.twofactor-steps > li::marker { color: var(--ink); font-weight: 700; }

/* Two columns on anything wider than a phone, one below that: ten codes in a single column
   on a 360px screen pushes the "I've saved them" button off the bottom, and the button is
   the thing that has to be seen. */
.recovery-codes {
  list-style: none;
  padding: 14px 16px;
  margin: 0 0 14px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--brand-tint);
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px 16px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px;
  color: var(--ink);
  user-select: all;
}
@media (max-width: 480px) {
  .recovery-codes { grid-template-columns: 1fr; }
}

/* ---------- Key/value facts in an admin panel ---------- */
/* Label-and-value pairs that are read rather than managed: the staged Android build, its
   size and checksum. A two-column grid rather than a stack-table: nothing here is a row
   with actions, and it collapses to one column on a phone where the labels would otherwise
   squeeze the values to a few characters wide. */
.admin-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 14px;
  margin: 0;
  font-size: 14.5px;
}
.admin-facts dt {
  color: var(--ink-soft);
  font-weight: 600;
  white-space: nowrap;
}
.admin-facts dd {
  margin: 0;
  color: var(--ink);
  min-width: 0;
}
@media (max-width: 575.98px) {
  .admin-facts { grid-template-columns: 1fr; gap: 2px; }
  .admin-facts dd { margin-bottom: 8px; }
}

/* A 64-character hex digest has no spaces in it, so it is the one value here that has to be
   allowed to break mid-run rather than overflow the panel. */
.admin-hash {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  word-break: break-all;
  color: var(--ink-soft);
}

/* Panels on Business settings that belong to one payment mode. [hidden] needs saying
   explicitly because Bootstrap's reset sets display on form containers, which beats the
   browser default for the attribute and would leave hidden panels on screen. */
.payment-mode-panel[hidden] { display: none; }
