/* ---------------------------------------------------------------------
   Atelier Order Book
   --------------------------------------------------------------------- */

:root {
  --paper: #f7f5f1;        /* bone / ivory ground */
  --paper-deep: #dadada;
  --ink: #1c1a17;          /* near-black charcoal, not pure #000 */
  --ink-soft: #7e7a78;
  --hairline: #83898d;

  /* status colors — kept distinct from each other */
  --status-pending: #ba25ba;    /* purple */
  --status-progress: #2f6fa6;   /* steel blue */
  --status-ready: #c98f00;      /* gold/amber */
  --status-delivered: #2f7d4f;  /* forest green */
  --status-rush: #c23b34;       /* brick red */

  --day-today: #c23b34;       /* brick red */

  /* Medium grey for a count that is deliberately non-committal — the lead
     badge. Darker than --ink-soft so white text on it stays legible at
     badge size, and neutral because an unread enquiry might be a client,
     a supplier or spam and the app has no business guessing which. */
  --badge-neutral: #6b7075;

  --radius: 2px;
  --font-display: sans-serif;
  --font-body: "Inter", -apple-system, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background-image: none;
}

.ledger {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  max-width: 1200px;
  width: 100%;
  margin: 40px auto 0;
  padding: 0 24px;
}

/* ---------------------------------------------------------------------
   View switch (Calendar / Timeline)
   --------------------------------------------------------------------- */

.view-switch {
  max-width: 1200px;
  width: 100%;
  margin: 24px auto 0;
  padding: 0 24px;
  display: flex;
  gap: 4px;
  position: relative;
}

.view-switch__toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
}

.view-switch__toggle:hover {
  border-color: var(--ink);
}

.view-switch__links {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1;
}

.view-switch__link {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  padding: 6px 14px;
  border-radius: var(--radius) var(--radius) 0 0;
}

.view-switch__link.is-active {
  color: var(--ink);
  background: var(--paper);
}

.view-switch__logout {
  margin-left: auto;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--paper);
  background: var(--ink);
  padding: 6px 14px;
  border-radius: var(--radius);
  align-self: center;
}

.view-switch__logout:hover {
  background: #000;
}

/* ---------------------------------------------------------------------
   Header
   --------------------------------------------------------------------- */

.ledger__header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: end;
  gap: 24px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--ink);
  position: relative;
}

.ledger__mark h1 {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 600;
  color: var(--ink);
}

.ledger__nav {
  display: flex;
  align-items: center;
  gap: 14px;
  justify-self: center;
}

.ledger__month {
  text-align: center;
  min-width: 148px;
}

.ledger__month-name {
  display: block;
  font-size: 1.4rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.ledger__year {
  display: block;
  font-size: 1.2rem;
  color: var(--ink-soft);
  letter-spacing: 0.08em;
}

.stamp {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--ink);
  font-size: 0.95rem;
  transition: background 0.15s ease, color 0.15s ease;
}

.stamp:hover {
  background: var(--ink);
  color: var(--paper);
}

.ledger__summary {
  justify-self: end;
  text-align: right;
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.ledger__summary-count {
  font-size: 1.9rem;
  font-weight: 600;
  line-height: 1;
}

.ledger__summary-label {
  font-size: 0.68rem;
  line-height: 1.3;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ---------------------------------------------------------------------
   Legend
   --------------------------------------------------------------------- */

.legend-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 18px;
  padding: 18px 2px 22px;
}

.legend-row .legend {
  padding: 0;
}

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  padding: 18px 2px 22px;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
}

.legend__new-order {
  font-size: 0.72rem;
  padding: 7px 14px;
  text-decoration: none;
  margin-left: auto;
}

/* A quiet link sitting in a legend row beside the filter buttons — the
   hidden-clients archive, and anything else that's a sideways move rather
   than an action. Same weight and hover as .back-link, which is the same
   kind of thing; its own class only because .back-link carries a bottom
   margin for the top of a detail page. */
.legend__aside {
  font-size: 0.72rem;
  color: var(--ink-soft);
  text-decoration: none;
}

.legend__aside:hover {
  color: var(--ink);
  text-decoration: underline;
}

/* Connect + "sync everything", side by side under the account list. Its own
   class rather than reusing .settings-source-add's layout alone, which
   stacks a single form's fields rather than sitting two forms in a row. */
.integration-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* Two or more controls at the right-hand end of a .legend-row. The push
   lives on the group rather than on .legend__new-order, so the buttons stay
   together instead of the first one being shoved right and the rest
   trailing after it. */
.legend__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.legend__actions .btn-primary,
.legend__actions .btn-secondary {
  font-size: 0.72rem;
  padding: 7px 14px;
}

/* Precedes the status filter buttons in .legend-row — plain text, same
   weight as "Sort by", so it reads as a caption rather than another
   clickable filter. */
.legend__label {
  font-size: 0.72rem;
  color: var(--ink-soft);
}

/* Extra breathing room beyond .legend-row's own 18px gap, so the sort
   control doesn't read as part of the status filter group next to it. */
.timeline__sort {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 24px;
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.timeline__sort select {
  font-family: inherit;
  font-size: 0.76rem;
  color: var(--ink);
  padding: 5px 8px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
}

.legend__item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: none;
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  color: var(--ink);
  cursor: pointer;
}

.legend__item:hover {
  color: var(--status-progress);
}

.legend__item--off {
  color: var(--ink-soft);
  text-decoration: line-through;
}

/* Sizes an icon-btn-style SVG (e.g. the "Show hidden" toggle's hide icon)
   dropped into a plain .legend__item — those buttons carry no width/height
   of their own, and stroke="currentColor" already ties the icon's color to
   .legend__item/.legend__item--off without any extra rule needed here. */
.legend__item svg {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

.legend__item--off .dot {
  opacity: 0.35;
}

.legend__item--static {
  cursor: default;
  color: var(--ink-soft);
}

.legend__item--static:hover {
  color: var(--ink-soft);
}

.legend__item--static .timeline__star {
  margin-left: 0;
}

.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  display: inline-block;
}

/* Tentative reuses --badge-neutral rather than minting a fifth hue: that
   token already means "deliberately non-committal, nobody has decided yet",
   which is exactly what a tentative order is. Confirmed and In progress
   share the steel blue because they're one stage under two names (see
   Order.display_status) — a second colour would imply a second stage.
   Cancelled is --ink-soft: dead, not urgent, so it gets no weight at all. */
.dot--tentative, .chip--tentative { --chip-color: var(--badge-neutral); }
.dot--confirmed, .chip--confirmed { --chip-color: var(--status-progress); }
.dot--in_progress, .chip--in_progress { --chip-color: var(--status-progress); }
.dot--ready, .chip--ready { --chip-color: var(--status-ready); }
.dot--delivered, .chip--delivered { --chip-color: var(--status-delivered); }
.dot--cancelled, .chip--cancelled { --chip-color: var(--ink-soft); }
/* .dot--rush isn't produced by the STATUS_LABELS loop (rush isn't in it) —
   it's used by hand for the timeline's rush key, below the table with the
   returning-client star. `.pill--rush` likewise: the orders list renders it
   as a second pill *beside* the stage pill, not instead of it. */
.dot--rush, .chip--rush { --chip-color: var(--status-rush); }

.dot--tentative { background: var(--badge-neutral); }
.dot--confirmed { background: var(--status-progress); }
.dot--in_progress { background: var(--status-progress); }
.dot--ready { background: var(--status-ready); }
.dot--delivered { background: var(--status-delivered); }
.dot--cancelled { background: var(--ink-soft); }
.dot--rush { background: var(--status-rush); }

/* ---------------------------------------------------------------------
   Calendar grid
   --------------------------------------------------------------------- */

.calendar {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  background: var(--paper);
  border: 1px solid var(--hairline);
}

.calendar thead th {
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: left;
  padding: 12px 10px 10px;
}

.day {
  vertical-align: top;
  border: 1px solid var(--hairline);
  padding: 8px 8px 10px;
  height: 108px;
  width: 14.28%;
  position: relative;
}

.day--empty {
  background: var(--paper-deep);
}

.day--quiet .day__number {
  color: var(--ink-soft);
}

.day--today {
  background: #fbfaf7;
  box-shadow: inset 0 0 0 2px var(--day-today);
}

.day__number {
  font-size: 0.76rem;
  font-weight: 500;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 6px;
}

.day__rivet {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--day-today);
  display: inline-block;
}

.day__events {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.chip {
  border-left: 4px solid var(--chip-color, var(--status-progress));
  background: rgba(255, 255, 255, 0.5);
  padding: 3px 6px;
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 0.66rem;
  line-height: 1.25;
  cursor: default;
  overflow: hidden;
}

.chip__client {
  display: block;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip__item {
  display: block;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip__title {
  display: block;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chip__time {
  display: block;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------------------------------------------------------------------
   Timeline (Gantt-style) view
   --------------------------------------------------------------------- */

.timeline {
  position: relative;
  border: 1px solid var(--hairline);
  background: var(--paper);
}

.timeline__corner-patch {
  position: absolute;
  top: -1px;
  left: -1px;
  width: 160px;
  height: 34px;
  background: var(--paper);
  pointer-events: none;
}

.timeline__row {
  display: grid;
  grid-template-columns: 160px 1fr;
  border-bottom: 1px solid var(--hairline);
  min-height: 44px;
}

.timeline__row[hidden] {
  display: none;
}

.timeline__row:last-child {
  border-bottom: none;
}

.timeline__row--header {
  min-height: 34px;
  border-bottom: 1px solid var(--ink);
}

.timeline__label {
  padding: 10px 12px;
  font-size: 0.74rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  border: none;
  border-right: 1px solid var(--hairline);
  display: flex;
  align-items: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  background: none;
  font-family: inherit;
  text-align: left;
  width: 100%;
  cursor: pointer;
}

.timeline__row:not(.timeline__row--header) .timeline__label {
  background: var(--ink);
  color: #fff;
}

.timeline__row--header .timeline__label {
  background: var(--paper);
  cursor: default;
}

.timeline__label:hover {
  color: var(--status-progress);
}

.timeline__label:hover .timeline__label-name {
  text-decoration: underline;
}

.timeline__star {
  width: 11px;
  height: 11px;
  margin-left: 6px;
  flex-shrink: 0;
  color: var(--status-ready);
}


.timeline__track {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--window-days), 1fr);
  align-items: center;
  /* faint zebra striping, one band per week, independent of window length */
  background-image: repeating-linear-gradient(
    to right,
    transparent 0,
    transparent 12.5%,
    rgba(0, 0, 0, 0.03) 12.5%,
    rgba(0, 0, 0, 0.03) 25%
  );
}

.timeline__track--header {
  grid-template-columns: repeat(var(--week-count), 1fr);
  background-image: none;
}

.timeline__week-label {
  font-size: 0.66rem;
  font-weight: 600;
  color: var(--ink-soft);
  letter-spacing: 0.04em;
  padding-left: 6px;
  border-left: 1px solid var(--hairline);
  display: flex;
  align-items: center;
  height: 100%;
}

.timeline__week-label:first-child {
  border-left: none;
}

.timeline__bar {
  height: 24px;
  margin: 4px 2px;
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  padding: 0 8px;
  overflow: hidden;
  background: var(--chip-color, var(--status-progress));
  cursor: pointer;
  transition: filter 0.15s ease;
}

.timeline__bar:hover {
  filter: brightness(1.1);
}

.timeline__bar--confirmed { --chip-color: var(--status-progress); }
.timeline__bar--in_progress { --chip-color: var(--status-progress); }
.timeline__bar--ready { --chip-color: var(--status-ready); }
.timeline__bar--delivered { --chip-color: var(--status-delivered); }

/* Tentative is outlined, not filled. Every other bar is a block of studio
   time that's been committed; this one hasn't been, and a solid bar in a
   fifth colour would still read as booked. Dashes say "pencilled in" in a
   way no hue can. Label flips to ink since there's no dark fill under it. */
.timeline__bar--tentative {
  --chip-color: var(--badge-neutral);
  background: transparent;
  border: 1px dashed var(--badge-neutral);
}

.timeline__bar--tentative .timeline__bar-label {
  color: var(--ink-soft);
}

/* A rush order's bar is red outright, overriding its stage colour —
   urgency is the thing you need to see first when scanning a schedule,
   and a marker layered over the stage colour was too quiet to do that job.
   Declared after the stage rules on purpose: same specificity, so source
   order is what makes it win.

   Rush only ever sits on `confirmed` (Order.can_rush), so this replaces
   exactly one colour — the steel blue — and never hides whether something
   is ready or delivered. Which is why losing the stage to the colour is
   affordable here in a way it wasn't when rush was itself a status. The
   key for it sits under the table with the returning-client star, since
   it isn't a stage you can filter to. */
.timeline__bar--rush { --chip-color: var(--status-rush); }

/* Orders that start/end outside the visible window get a cut/angled edge
   instead of a flat one, as a visual cue there's more off-screen */
.timeline__bar--open-start {
  clip-path: polygon(6px 0, 100% 0, 100% 100%, 6px 100%, 0 50%);
}

.timeline__bar--open-end {
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 50%, calc(100% - 6px) 100%, 0 100%);
}

.timeline__bar--open-start.timeline__bar--open-end {
  clip-path: polygon(6px 0, calc(100% - 6px) 0, 100% 50%, calc(100% - 6px) 100%, 6px 100%, 0 50%);
}

.timeline__bar-label {
  font-size: 0.68rem;
  font-weight: 600;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.timeline__bar-type {
  font-weight: 400;
  opacity: 0.85;
}

.timeline__empty {
  padding: 32px 16px;
  text-align: center;
  font-size: 0.8rem;
  color: var(--ink-soft);
  font-style: italic;
}

/* Key for the returning-client star and the red rush bar, below the table
   rather than in .legend-row — neither is a filter, so neither should sit
   among the buttons that are. (Rush is reachable from the sort instead.) */
.timeline__legend-note {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 12px 2px 0;
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.timeline__legend-note .timeline__star {
  margin-left: 0;
}

.timeline__legend-sep {
  color: var(--paper-deep);
}

/* ---------------------------------------------------------------------
   Footer
   --------------------------------------------------------------------- */

.site-footer {
  flex-shrink: 0;
  max-width: 1200px;
  width: 100%;
  margin: 0 auto;
  padding: 24px 24px 32px;
}

.site-footer p {
  margin: 0;
  font-size: 0.74rem;
  color: var(--ink-soft);
  font-style: italic;
}

/* One line: version · Privacy Policy · Terms of Service. The links are not
   italic — the version's italic reads as an aside, where a link needs to read
   as something to click. */
.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  font-style: normal !important;
}

.site-footer__version {
  font-style: italic;
}

.site-footer__legal a {
  color: var(--ink-soft);
  text-decoration: none;
}

.site-footer__legal a:hover {
  color: var(--ink);
  text-decoration: underline;
}

/* ---------------------------------------------------------------------
   Legal pages (privacy policy, terms of service)
   --------------------------------------------------------------------- */

/* Long-form prose, so it gets a reading measure rather than the app's full
   1200px grid width. */
.legal-doc {
  max-width: 760px;
  padding-bottom: 40px;
}

.legal-doc h2 {
  margin: 32px 0 10px;
  font-size: 1.02rem;
  font-weight: 600;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--paper-deep);
}

.legal-doc h3 {
  margin: 22px 0 8px;
  font-size: 0.9rem;
  font-weight: 600;
}

.legal-doc p,
.legal-doc li {
  font-size: 0.86rem;
  line-height: 1.65;
}

.legal-doc ul {
  margin: 10px 0;
  padding-left: 20px;
}

.legal-doc li {
  margin-bottom: 7px;
}

.legal-doc a {
  color: var(--status-progress);
}

.legal-lede {
  color: var(--ink-soft);
}

.legal-doc code {
  font-family: inherit;
  font-weight: 500;
  background: var(--paper-deep);
  padding: 1px 4px;
  border-radius: var(--radius);
}

.legal-table {
  width: 100%;
  border-collapse: collapse;
  margin: 12px 0;
}

.legal-table th,
.legal-table td {
  text-align: left;
  vertical-align: top;
  font-size: 0.83rem;
  line-height: 1.6;
  padding: 8px 12px 8px 0;
  border-bottom: 1px solid var(--paper-deep);
}

.legal-table th {
  font-weight: 600;
  white-space: nowrap;
}

.legal-crosslink {
  margin-top: 32px;
  padding-top: 16px;
  border-top: 1px solid var(--ink);
  font-size: 0.8rem;
}

/* ---------------------------------------------------------------------
   Modals (customer / order quick view, opened from the timeline)
   --------------------------------------------------------------------- */

dialog.modal {
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  padding: 0;
  max-width: 420px;
  width: 90%;
  box-shadow: 0 12px 32px rgba(28, 26, 23, 0.18);
}

dialog.modal::backdrop {
  background: rgba(28, 26, 23, 0.45);
}

.modal__form,
.modal__body {
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.modal__title {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0;
}

.modal__subtitle {
  margin: -8px 0 0;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

/* Says who an invitation would reach, under the client select. Sits tight to
   the field above it so it reads as that field's own note, not a new row. */
.modal__hint {
  margin: -10px 0 0;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

.modal__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.modal__field input,
.modal__field select,
.modal__field textarea {
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  padding: 8px 10px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
  resize: vertical;
}

.modal__field input:focus,
.modal__field select:focus,
.modal__field textarea:focus {
  outline: none;
  border-color: var(--ink);
}

/* A checkbox reads as "[x] label", not as a label stacked above a control,
   so this variant runs as a row and opts out of the text-input styling
   above — which would otherwise put a border and 8px of padding around a
   13px box. Accent is the rush red, so the control looks like what it does
   to the bar. */
.modal__field--check {
  flex-direction: row;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.modal__field--check input[type="checkbox"] {
  flex: none;
  width: auto;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  accent-color: var(--status-rush);
}

/* A date and a time side by side in one field slot (an event's start/end).
   Wraps rather than squeezing, same as .field-row on the settings forms. */
.modal__pair {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.modal__pair input {
  flex: 1 1 130px;
  min-width: 0;
}

.modal__pair input:disabled {
  color: var(--ink-soft);
  background: var(--paper-deep);
}

/* Derived values shown inside a modal field slot (e.g. an order total,
   which is computed from line items and so isn't editable here). */
.modal__readout {
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  padding: 8px 0;
}

.modal__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 6px;
  flex-wrap: wrap;
}

.modal__link {
  font-size: 0.76rem;
  color: var(--status-progress);
  text-decoration: none;
}

.modal__link:hover {
  text-decoration: underline;
}

.modal__buttons {
  display: flex;
  gap: 8px;
}

.btn-primary,
.btn-secondary {
  font-size: 0.76rem;
  font-weight: 600;
  padding: 8px 16px;
  border-radius: var(--radius);
  cursor: pointer;
  border: 1px solid var(--ink);
  font-family: inherit;
  white-space: nowrap;
}

.btn-primary {
  background: var(--ink);
  color: var(--paper);
}

.btn-primary:hover {
  background: #000;
}

.btn-secondary {
  background: transparent;
  color: var(--ink);
}

.btn-secondary:hover {
  background: var(--paper-deep);
}

/* ---------------------------------------------------------------------
   Customer / order detail pages
   --------------------------------------------------------------------- */

.back-link {
  display: inline-block;
  font-size: 0.76rem;
  color: var(--ink-soft);
  text-decoration: none;
  margin-bottom: 20px;
}

.back-link:hover {
  color: var(--ink);
  text-decoration: underline;
}

.detail-header {
  border-bottom: 1px solid var(--ink);
  padding-bottom: 16px;
  margin-bottom: 24px;
}

.detail-header--with-stat {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
}

.detail-header h1 {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 600;
}

/* Shape/typography come from the shared .pill rule further down; this
   only picks the colour and the bits specific to sitting inside an <h1>. */
.pill--returning {
  --pill-color: var(--status-ready);
  gap: 5px;
  margin-left: 10px;
  vertical-align: middle;
}

.pill--returning svg {
  width: 11px;
  height: 11px;
}

.client-stats {
  display: flex;
  gap: 24px;
  margin: 0 0 28px;
}

.client-stats--inline {
  margin: 0;
  flex-shrink: 0;
  text-align: right;
}

.client-stats--inline .client-stats__item {
  align-items: flex-end;
}

.client-stats__item {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.client-stats__value {
  font-size: 1.3rem;
  font-weight: 600;
}

.client-stats__label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-soft);
}

.detail-subtitle {
  margin: 6px 0 0;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.detail-subtitle a {
  color: var(--status-progress);
  text-decoration: none;
}

.detail-subtitle a:hover {
  text-decoration: underline;
}

.detail-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 360px;
  margin-bottom: 40px;
}

.detail-form label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.detail-form fieldset {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
  padding: 16px;
  border: 1px dashed var(--hairline);
  border-radius: var(--radius);
}

.detail-form fieldset[hidden] {
  display: none;
}

.detail-form legend {
  padding: 0 4px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.checkbox-group {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.detail-form .checkbox-group__item {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  font-size: 0.85rem;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
}

.checkbox-group__item input[type="checkbox"] {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  accent-color: var(--ink);
}

.detail-form input,
.detail-form select,
.detail-form textarea {
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
  padding: 10px 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
  resize: vertical;
}

.detail-form input:focus,
.detail-form select:focus,
.detail-form textarea:focus {
  outline: none;
  border-color: var(--ink);
}

.login-page {
  max-width: 400px;
  margin: 80px auto 0;
}

.login-error {
  margin: 0;
  font-size: 0.8rem;
  color: var(--status-rush);
}

.detail-orders h2,
.detail-documents h2,
.detail-payments h2,
.detail-lines h2,
.detail-invoice h2,
.detail-lifecycle h2,
.invoice-admin h2 {
  font-size: 0.9rem;
  font-weight: 600;
  margin: 0 0 10px;
}

.detail-order-list,
.doc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.detail-order-list li {
  font-size: 0.84rem;
  color: var(--ink-soft);
  display: flex;
  align-items: center;
  gap: 6px;
}

.detail-order-list a {
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
}

.detail-order-list a:hover {
  color: var(--status-progress);
}

/* Secondary link in a row that already has one — the order is the subject,
   the client is context, so it shouldn't compete for weight. */
.detail-order-list__client {
  font-weight: 400;
  color: var(--ink-soft);
}

.doc-list__item {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.84rem;
  padding: 8px 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.doc-list__label {
  font-weight: 600;
  color: var(--ink);
}

.doc-list__filename {
  color: var(--ink-soft);
}

.doc-list__delete-form {
  margin-left: auto;
}

.doc-list__delete {
  font-family: inherit;
  font-size: 0.72rem;
  color: var(--status-rush);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  text-decoration: underline;
}

.doc-list__delete:hover {
  color: #000;
}

.doc-list__amount {
  margin-left: auto;
  font-size: 0.8rem;
  color: var(--ink);
}

/* Sits after .doc-list__amount, so the Remove button keeps its own
   margin-left rather than both fighting for the same auto gap. */
.doc-list__amount + .doc-list__delete-form {
  margin-left: 16px;
}

.doc-list__ref {
  font-size: 0.74rem;
}

/* One sub-section per document type, once any are configured — each with
   its own heading and its own .doc-explorer grid below. */
.doc-explorer__section {
  margin-bottom: 20px;
}

.doc-explorer__section h3 {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ink-soft);
  margin: 0 0 8px;
}

/* Document explorer — order page "Documents" section. A card grid rather
   than the .doc-list row shape used elsewhere on this page: thumbnails
   need room the row layout doesn't have. */
.doc-explorer {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 14px;
  margin: 0 0 16px;
}

.doc-explorer__item {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

/* The "Add file" tile — a <form> styled and sized like a document card so
   it sits in the grid as a peer of the documents themselves, dashed to
   read as an add affordance rather than existing content (same dashed
   convention as .detail-form fieldset elsewhere on this page). */
.doc-explorer__add {
  border-style: dashed;
  padding: 0;
}

.doc-explorer__add-trigger {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px;
  cursor: pointer;
  color: var(--ink-soft);
}

.doc-explorer__add-trigger:hover {
  color: var(--ink);
}

.doc-explorer__add-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid currentColor;
  border-radius: 50%;
}

.doc-explorer__add-icon svg {
  width: 16px;
  height: 16px;
}

.doc-explorer__add-label {
  font-size: 0.76rem;
  font-weight: 600;
}

.doc-explorer__thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100px;
  background: var(--paper-deep);
  border-radius: var(--radius);
  overflow: hidden;
}

.doc-explorer__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.doc-explorer__ext {
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}

.doc-explorer__filename {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.doc-explorer__size {
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.doc-explorer__actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: auto;
  padding-top: 4px;
}

.doc-explorer__delete-form {
  display: contents;
}

/* Small square icon button — download/open/delete on a document card.
   First use of an icon-button component in the app; built to the same
   inline-SVG-stroke-currentColor convention as the calendar nav chevrons. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-soft);
  cursor: pointer;
  text-decoration: none;
}

.icon-btn svg {
  width: 14px;
  height: 14px;
}

.icon-btn:hover {
  color: var(--ink);
  border-color: var(--ink);
}

.icon-btn--danger:hover {
  color: var(--status-rush);
  border-color: var(--status-rush);
}

/* Visually hidden, not display:none — stays keyboard/screen-reader
   reachable, it just isn't the thing rendered. Standard "sr-only" shape. */
.doc-explorer__upload-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.doc-explorer__quota {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 480px;
}

.quota-bar {
  flex: 1 1 auto;
  height: 6px;
  border-radius: var(--radius);
  background: var(--paper-deep);
  overflow: hidden;
}

.quota-bar__fill {
  height: 100%;
  background: var(--status-progress);
}

.doc-explorer__quota-label {
  flex-shrink: 0;
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.detail-payments__add,
.detail-inline-add {
  margin-top: 16px;
  max-width: 320px;
}

.detail-form__note {
  margin-top: -6px;
}

.line-totals {
  max-width: 480px;
  margin: 14px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--hairline);
}

.line-totals p {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  margin: 0 0 6px;
  font-size: 0.84rem;
  color: var(--ink-soft);
}

.line-totals strong {
  font-weight: 500;
  color: var(--ink);
}

.line-totals__total {
  padding-top: 8px;
  border-top: 1px solid var(--ink);
  font-weight: 600;
  color: var(--ink) !important;
}

/* Something the user should notice and can act on — distinct from
   .detail-note, which is passive explanation. */
.warning-note {
  max-width: 480px;
  margin: 14px 0 0;
  padding: 10px 12px;
  border-left: 3px solid var(--status-ready);
  background: rgba(201, 143, 0, 0.07);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--ink);
}

.warning-note a {
  color: var(--status-progress);
}

/* The urgent variant — red instead of the default amber — for the "out of
   stock" tier, so it matches its own red nav badge while the plain amber
   .warning-note carries the softer "running low" tier beside it. */
.warning-note--alert {
  border-left-color: var(--day-today);
  background: rgba(194, 59, 52, 0.07);
}

/* Impersonation banner — a platform admin working inside somebody else's
   company. Full-bleed and directly under the nav, unlike .warning-note,
   which sits inside a page's own column: this is a fact about the whole
   session, not about the page being looked at, and it has to be equally
   unmissable on all of them.

   Amber for the same reason the low-stock badge is (see hard rule 7):
   "needs attention soon" rather than "broken". Red would be wrong — this
   is a state the admin chose and can leave in one click, not a fault. */
.impersonation-banner {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  align-items: center;
  justify-content: space-between;
  padding: 8px 20px;
  border-bottom: 1px solid var(--status-ready);
  background: rgba(201, 143, 0, 0.14);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--ink);
}

.impersonation-banner form {
  margin: 0;
}

/* Platform announcement — a fact about the installation, shown whether or
   not anyone is signed in (see base.html). Purple (--status-pending)
   rather than the impersonation banner's amber, and deliberately not the
   same colour: amber here is already claimed for "needs attention soon"
   (hard rule 7), and an announcement isn't that — it's the app's own
   "something worth knowing" colour, the same one behind the new-client
   and synced-event badges elsewhere. Structurally simpler than
   .impersonation-banner (no button, no flex row) since there's nothing to
   act on here, only something to read. pre-wrap so an admin's intentional
   line break in the textarea survives into the rendered banner. */
.announcement-banner {
  padding: 8px 20px;
  border-bottom: 1px solid var(--status-pending);
  background: rgba(186, 37, 186, 0.1);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--ink);
  white-space: pre-wrap;
}

/* Uniform gap between page sections (order/client/settings/invoice pages).
   Deliberately on the section wrapper, not on whatever happens to be its
   last child (a form, a list, a note) — those vary per section and were
   producing inconsistent gaps before. Margin collapsing between a
   border/padding-less section and its last child means this 40px simply
   wins over a smaller child margin (e.g. .settings-form's 8px) rather than
   stacking with it. */
.detail-orders,
.detail-documents,
.detail-payments,
.detail-lines,
.detail-invoice,
.detail-lifecycle,
.invoice-admin {
  margin-bottom: 40px;
}

/* Both the order and client pages put this directly under a form's Save
   button, and .detail-form's own margin isn't enough to stop "This order" /
   "This client" reading as part of the form it follows. Matches the gap the
   Documents block already sits at. */
.detail-lifecycle {
  margin-top: 32px;
}

/* Lifecycle actions on the order page — rush toggle, cancel, delete. Each
   is its own little <form>, so they need laying out as a row rather than
   inheriting the stacked .detail-form grid above them. */
.lifecycle-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.lifecycle-actions form {
  margin: 0;
}

/* ---------------------------------------------------------------------
   Invoices
   --------------------------------------------------------------------- */

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

.invoice-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.84rem;
  border: 1px solid var(--hairline);
  background: var(--paper);
}

.invoice-table th {
  text-align: left;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 10px 12px;
  border-bottom: 1px solid var(--ink);
  white-space: nowrap;
}

.invoice-table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}

.invoice-table tr:last-child td {
  border-bottom: none;
}

.invoice-table a {
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
}

.invoice-table a:hover {
  color: var(--status-progress);
}

.invoice-table .is-numeric {
  text-align: right;
}

.invoice-table th.is-numeric {
  font-family: var(--font-body);
}

/* Orders / Clients / Invoices list tables — no mono font for now. */
.invoice-table--no-mono .is-numeric {
  font-family: var(--font-body);
}

/* The printable invoice document's own numeric columns aren't covered by
   .invoice-table--no-mono (a different table class), so scoped here. */
.invoice-doc .is-numeric {
  font-family: var(--font-body);
}

/* A <button> that reads exactly like .invoice-table a — for the inventory
   item name, which has to be a button (it opens an edit modal) rather than
   a link. */
.link-button {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--ink);
  font-weight: 600;
  cursor: pointer;
}

.link-button:hover {
  color: var(--status-progress);
}

/* Flags a negative quantity_on_hand on the inventory list — allowed, not
   blocked (see inventory/services.py), so this is the only signal that
   something's oversold. */
.inventory-qty--negative {
  color: var(--day-today);
  font-weight: 600;
}

/* The amber companion: a quantity that's low but still positive — at or below
   the item's own warning point (see InventoryItem.is_low_stock). Same
   gold/amber as the low-stock nav badge, one tier down from the red negative
   flag above. */
.inventory-qty--low {
  color: var(--status-ready);
  font-weight: 600;
}

/* The inventory list's Notes cell. A note is free text that can run to a
   paragraph, and the table already scrolls horizontally (.table-scroll), so
   an unclamped one would either set the height of its row or push every
   column after it off screen. Clamped to a single ellipsized line with the
   full text on the cell's title attribute; the edit modal is where a long
   note is actually read and written. */
.inventory-notes {
  display: block;
  max-width: 22ch;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--ink-soft);
}

/* A row of icon-btns inside a table cell — the inventory list's Actions
   column (hide/unhide, delete). */
.icon-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Same idea as .doc-list__delete-form/.doc-explorer__delete-form: a <form>
   wrapping a single icon button among flex siblings shouldn't itself take
   up flex layout space. Generic (not scoped to one component) since both
   the inventory table and the order Materials tab's .doc-list rows need it. */
.icon-form {
  display: contents;
}

/* Icon-button equivalent of .doc-list__delete-form — sits where the old
   text "Remove" button did, holding an Edit + Delete icon pair. */
.doc-list__actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}

.doc-list__amount + .doc-list__actions {
  margin-left: 16px;
}

/* Recent activity (audit log) — "Detail" can be a long freeform message, so
   this variant fixes the table to the container width and wraps text
   instead of forcing a horizontal scroll. */
.invoice-table--wrap {
  table-layout: fixed;
}

.invoice-table--wrap td {
  white-space: normal;
  word-break: break-word;
}

.invoice-table--wrap th:nth-child(1), .invoice-table--wrap td:nth-child(1) { width: 16%; }
.invoice-table--wrap th:nth-child(2), .invoice-table--wrap td:nth-child(2) { width: 18%; }
.invoice-table--wrap th:nth-child(3), .invoice-table--wrap td:nth-child(3) { width: 52%; }
.invoice-table--wrap th:nth-child(4), .invoice-table--wrap td:nth-child(4) { width: 14%; }

.is-mono {
  font-size: 0.78rem;
}

.detail-uninvoiced {
  margin-top: 40px;
}

.invoice-ref {
  font-size: 0.74rem;
  color: var(--ink-soft);
  text-decoration: none;
}

.invoice-ref:hover {
  color: var(--status-progress);
}

/* Status pills. --pill-color per status keeps this to one rule, matching
   how chip/dot/bar share --chip-color. */
.pill {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--pill-color, var(--ink-soft));
  border: 1px solid var(--pill-color, var(--hairline));
  border-radius: 999px;
}

.pill--draft { --pill-color: var(--ink-soft); }
.pill--sent { --pill-color: var(--status-progress); }
.pill--paid { --pill-color: var(--status-delivered); }
.pill--void { --pill-color: var(--status-rush); }

/* Order statuses, same colors as the timeline/calendar dots+bars. */
.pill--tentative { --pill-color: var(--badge-neutral); }
.pill--confirmed { --pill-color: var(--status-progress); }
.pill--in_progress { --pill-color: var(--status-progress); }
.pill--ready { --pill-color: var(--status-ready); }
.pill--delivered { --pill-color: var(--status-delivered); }
.pill--cancelled { --pill-color: var(--ink-soft); }
.pill--rush { --pill-color: var(--status-rush); }

/* A cancelled order's row in the orders list / a client's orders tab.
   Muted rather than badged: it isn't undecided, worth knowing, urgent or
   broken, so it earns none of the four badge weights — it's just over.
   The item name takes the strikethrough; striking a whole row of numbers
   makes them hard to read for no gain. */
.is-cancelled {
  color: var(--ink-soft);
}

.is-cancelled td:first-child {
  text-decoration: line-through;
  text-decoration-color: var(--ink-soft);
}

.is-cancelled a {
  color: var(--ink-soft);
}

/* Sortable column headers (orders_list.html / clients_list.html) */
.sort-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: inherit;
  text-decoration: none;
}

.sort-link:hover {
  color: var(--status-progress);
}

.sort-link.is-active {
  color: var(--ink);
}

.sort-link.is-numeric {
  justify-content: flex-end;
  width: 100%;
}

.sort-arrow {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
}

/* The printable document itself */
.invoice-doc {
  border: 1px solid var(--hairline);
  background: var(--paper);
  padding: 32px;
  margin-bottom: 40px;
}

.invoice-doc__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--ink);
}

.invoice-doc__company {
  margin: 0;
  font-size: 1.4rem;
  font-weight: 600;
}

.invoice-doc__meta {
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.invoice-doc__issuer-address {
  margin: 8px 0 0;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--ink-soft);
  white-space: pre-line;  /* keeps the address's own line breaks */
}

/* Registration numbers wrap onto as many lines as they need rather than
   forcing the header wider. */
.invoice-doc__issuer-reg {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 16px;
  margin: 8px 0 0;
  font-size: 0.72rem;
  color: var(--ink-soft);
}

.invoice-doc__pay {
  margin-top: 32px;
  max-width: 480px;
}

.invoice-doc__pay p {
  margin: 8px 0 0;
  font-size: 0.82rem;
  line-height: 1.5;
  white-space: pre-line;
}

.invoice-doc__ref {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

.invoice-doc__number {
  font-size: 1rem;
  font-weight: 500;
}

.invoice-doc__parties {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 24px;
  padding: 24px 0;
}

.invoice-doc__label {
  display: block;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.invoice-doc__party {
  margin: 6px 0 16px;
  font-size: 0.86rem;
  line-height: 1.5;
  white-space: pre-line;  /* keeps the address's own line breaks */
}

.invoice-doc__party a {
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
}

.invoice-doc__party a:hover {
  color: var(--status-progress);
}

.invoice-doc__totals {
  margin: 20px 0 0;
  margin-left: auto;
  max-width: 280px;
}

.invoice-doc__totals p {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  margin: 0 0 6px;
  font-size: 0.86rem;
}

.invoice-doc__totals p:not(.invoice-doc__total):not(.invoice-doc__balance) {
  color: var(--ink-soft);
}

.invoice-doc__total {
  padding-top: 8px;
  border-top: 1px solid var(--hairline);
  font-weight: 600;
}

.invoice-doc__balance {
  padding-top: 8px;
  border-top: 1px solid var(--ink);
  font-weight: 600;
}

.invoice-doc__payments {
  margin-top: 32px;
}

.invoice-doc__payments ul {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.8rem;
  color: var(--ink-soft);
  max-width: 480px;
}

.invoice-doc__payments li {
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.invoice-doc__payment-amount {
  margin-left: auto;
  color: var(--ink);
}

.invoice-doc__notes {
  margin: 24px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.invoice-admin__buttons {
  display: flex;
  gap: 8px;
}

/* ---------------------------------------------------------------------
   Analytics
   --------------------------------------------------------------------- */

.analytics-section {
  margin-bottom: 40px;
}

.analytics-section h2 {
  font-size: 0.9rem;
  font-weight: 600;
  margin: 0 0 14px;
}

.analytics-section__heading {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Drag handle for the reorderable Analytics sections and cards. Same
   six-dot mark as .settings-source-list__grip, but this one sits on a page
   that isn't a settings list, so it stays faint until the thing it moves is
   hovered rather than reading as page furniture. */
.analytics-grip {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  width: 10px;
  color: var(--ink-soft);
  cursor: grab;
  opacity: 0.35;
  transition: opacity 0.12s ease;
}

.analytics-section:hover > .analytics-section__heading > .analytics-grip,
.stat-card:hover > .analytics-grip,
.analytics-grip:focus-visible {
  opacity: 1;
}

.stat-card > .analytics-grip {
  position: absolute;
  top: 14px;
  right: 12px;
}

.is-dragging {
  opacity: 0.4;
}

.analytics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
}

.stat-card {
  position: relative;  /* the drag grip is pinned to the top-right corner */
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 34px 18px 20px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
}

.stat-card__value {
  font-size: 1.7rem;
  font-weight: 600;
  color: var(--ink);
}

.stat-card__label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-soft);
}

.stat-card--list {
  gap: 10px;
}

.stat-card__ranked-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.stat-card__ranked-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.84rem;
}

.stat-card__ranked-list a {
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
}

.stat-card__ranked-list a:hover {
  color: var(--status-progress);
}

.detail-note {
  margin-top: 12px;
  font-size: 0.76rem;
  color: var(--ink-soft);
  font-style: italic;
}

.checkbox-group__hidden-tag {
  font-size: 0.68rem;
  font-style: italic;
  color: var(--ink-soft);
}

/* Wraps rather than overflowing: the row is eight categories long since
   Account joined it, which is past what a narrow viewport fits on one line.
   The row-gap is smaller than the column gap so a wrapped second line reads
   as a continuation of the first, not as a second nav. */
.settings-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  margin-bottom: 28px;
  border-bottom: 1px solid var(--hairline);
}

.settings-nav__link {
  padding: 0 0 10px;
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.settings-nav__link:hover {
  color: var(--ink);
}

.settings-nav__link.is-active {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

.settings-source-list {
  list-style: none;
  margin: 16px 0 24px;
  padding: 0;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.settings-source-list__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px;
  /* A floor, not a fixed height — every row already reaches this with a
     Hide/Delete button in its trailing slot (see .settings-source-list__actions).
     The one row with no buttons there at all ("Each" on /settings/inventory,
     which only ever shows a plain "(always available)" tag) would otherwise
     render visibly shorter than every row around it. */
  min-height: 55px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.settings-source-list__item--hidden {
  background: var(--paper-deep);
}

.settings-source-list__label {
  font-size: 0.88rem;
  color: var(--ink);
}

.settings-source-list__actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* Drag handle — document types only, currently the sole draggable list in
   the app. Grab cursor signals draggability the way the label text alone
   wouldn't. */
.settings-source-list__grip {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  width: 10px;
  color: var(--ink-soft);
  cursor: grab;
}

.settings-source-list__item--dragging {
  opacity: 0.4;
}

/* Connected-mailbox list on the Integrations page — each account carries
   more (meta line, granted-scope pills) than a source-option row, so it
   gets the full page width rather than the 480px cap other source lists
   use, and its own item layout below. */
.settings-source-list--wide {
  max-width: none;
}

.integration-account {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.integration-account__row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.integration-account__email {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--ink);
}

.integration-account__row .settings-source-list__actions {
  margin-left: auto;
}

.settings-source-add {
  max-width: 320px;
  margin-bottom: 0;
}

/* Settings → Account. Same reasoning as .compose-form__ai-status: transient
   feedback about one button press, not a standing condition of the page, so
   plain text rather than the amber .warning-note panel. */
.password-status {
  margin: 12px 0 0;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.password-status--error {
  color: var(--day-today);
}

/* Wider than .settings-source-add — these forms carry addresses and
   multi-line instructions, not a single short label. */
.settings-form {
  max-width: 420px;
  margin-bottom: 8px;
}

/* Short related fields sharing a line (city / province / postal code).
   Wraps rather than squeezing once the form gets narrow. */
.field-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.detail-form .field-row label {
  flex: 1 1 110px;
  min-width: 0;
}

.detail-form .field-row label:first-child {
  flex: 2 1 150px;
}

/* Sync intervals — fields of equal weight, wide enough that a caption
   doesn't wrap awkwardly.

   `white-space: nowrap` used to be paired with `flex-basis: 0` here, which
   holds only while the captions happen to fit: a third field made every
   label wider than the track it was given, and since they couldn't wrap
   they overlapped each other instead. The basis is now the caption's real
   minimum, so a row that runs out of room wraps to the next line — which is
   what `.field-row` was already set up to do. */
.settings-form--sync {
  max-width: 520px;
}

.settings-form--sync .field-row--sync label,
.settings-form--sync .field-row--sync label:first-child {
  flex: 1 1 190px;
  min-width: 0;
}

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

@media (max-width: 680px) {
  .view-switch {
    padding: 0 16px;
  }

  .view-switch__toggle {
    display: inline-flex;
    margin-left: auto;
  }

  .view-switch__links {
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    position: absolute;
    top: calc(100% + 4px);
    left: 16px;
    right: 16px;
    background: var(--paper);
    border: 1px solid var(--hairline);
    padding: 6px;
    z-index: 20;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  }

  .view-switch__links.is-open {
    display: flex;
  }

  .view-switch__links .view-switch__link {
    padding: 12px 10px;
    border-radius: var(--radius);
  }

  .view-switch__links .view-switch__link.is-active {
    background: var(--paper-deep);
  }

  .view-switch__logout {
    margin-left: 0;
    margin-top: 6px;
    align-self: stretch;
    text-align: center;
  }

  .ledger__header {
    grid-template-columns: 1fr;
    text-align: center;
  }
  .ledger__nav { justify-self: center; }
  .ledger__summary { justify-self: center; text-align: center; }

  .calendar, .calendar thead, .calendar tbody, .calendar tr, .calendar td {
    display: block;
    width: 100%;
  }
  .calendar thead { display: none; }
  .day {
    height: auto;
    min-height: 0;
    border-left: none;
    border-right: none;
  }
  .day--empty { display: none; }
  .day__number::before {
    content: attr(data-label) " ";
  }

  .timeline {
    overflow-x: auto;
  }
  .timeline__row {
    grid-template-columns: 120px 1fr;
    min-width: 700px;
  }
  .timeline__label {
    position: sticky;
    left: 0;
    background: var(--paper);
    /* .timeline__track is also position:relative (for its bars' absolute
       zebra-striping background), so without an explicit z-index the two
       positioned siblings paint in DOM order — track after label — and the
       scrolling bars/week-labels end up drawn over the sticky name column
       instead of passing behind it. */
    z-index: 2;
  }

  .invoice-doc__parties {
    grid-template-columns: 1fr;
  }
  .invoice-doc {
    padding: 20px;
  }
}

/* ---------------------------------------------------------------------
   Communications module (see communications/) — connected mailboxes,
   email threads, calendar events. Reuses the existing pill / settings-list
   / invoice-table vocabulary wherever it fits rather than inventing a
   parallel one; these rules only cover what genuinely has no equivalent.
   --------------------------------------------------------------------- */

/* Account states, on the same --pill-color scheme as order/invoice pills.
   Reusing the existing status hues on purpose: "connected" reading the
   same green as "delivered" is a feature, not a collision. */
.pill--connected { --pill-color: var(--status-delivered); }
.pill--paused { --pill-color: var(--ink-soft); }
.pill--error { --pill-color: var(--status-rush); }

/* Secondary line under a connected account: provider, last sync, errors. */
.integration-meta {
  display: block;
  margin-top: 4px;
  font-size: 0.7rem;
  color: var(--ink-soft);
}

.integration-meta--error {
  color: var(--status-rush);
}

/* What a mailbox actually granted — one chip per scope. */
.integration-scopes {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.integration-scope {
  padding: 2px 8px;
  font-size: 0.68rem;
  color: var(--ink-soft);
  border: 1px solid var(--hairline);
  border-radius: 999px;
}

/* A conversation nobody has opened yet. Filled rather than outlined like the
   other pills, because it's the one thing on the row that should catch the
   eye first. Purple, matching the badge that counts leads — the two are
   about the same inbox, and red now means only "something is broken". */
.pill--new {
  --pill-color: var(--status-pending);
  color: var(--paper);
  background: var(--status-pending);
}

/* Count badge on a nav link. The base is the lead badge: **grey**, because a
   lead is by definition undecided — nobody has established yet whether it's
   a client, a supplier or spam, and a colour that shouted would be claiming
   more than the app knows. The two modifiers below are the cases where
   something *is* known. Three states, three weights:
     grey   (.nav-badge)          — undecided, needs a person
     purple (.nav-badge--new)     — something happened, worth knowing
     red    (.nav-badge--alert)   — something is broken */
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 17px;
  height: 17px;
  margin-left: 5px;
  padding: 0 5px;
  font-size: 0.62rem;
  font-weight: 600;
  line-height: 1;
  color: #fff;
  background: var(--badge-neutral);
  border-radius: 999px;
  /* The count is decoration around a link that already says "Leads"; letting
     it be selected or dragged just gets in the way of clicking. */
  user-select: none;
}

/* The same badge carrying "!" instead of a count — an integration has stopped
   syncing. This one keeps the red: the two badges say different things, and
   the difference is worth a colour. Purple is "there's work waiting", red is
   "something is broken and nothing you do in the app will fix it". A single
   character doesn't need the horizontal padding a two-digit count does, so it
   drops to a true circle. */
.nav-badge--alert {
  padding: 0;
  width: 17px;
  background: var(--day-today);
}

/* Same red as .nav-badge--alert — "something needs attention" — but sized
   like the base .nav-badge (min-width, horizontal padding) rather than the
   fixed 17px circle, since this one carries a count (how many inventory
   items are at zero/negative stock), not a single "!" glyph. Not a fourth
   badge colour: still red, still "broken/needs attention", just a number
   instead of a symbol. */
.nav-badge--stock-alert {
  background: var(--day-today);
}

/* The softer, "running low" companion to --stock-alert: amber (the same
   gold/amber --status-ready the .warning-note border and the "ready" order
   status already use), sized like the base .nav-badge since it too carries a
   count. Sits beside the red badge when both apply — red = out of stock now,
   amber = restock soon. This is a fourth badge weight, added deliberately
   with the low-stock feature (see docs/design.md): amber = "worth attention
   soon", distinct from red = "broken/out". */
.nav-badge--low-stock {
  color: #fff;
  background: var(--status-ready);
}

/* A client the app created by itself from a sender rule. Purple, the app's
   "something is waiting for your attention" colour — and deliberately not
   the grey of the lead badge beside it on the same link: the lead is a
   question, this is an answer that arrived without anyone being asked. */
.nav-badge--new {
  background: var(--status-pending);
}

/* Unread mail from a client. Purple like the badge above for now, but its
   own class on purpose: the two say different things ("someone new is on
   your roster" vs "someone on it is waiting for a reply") and one of them
   will want to look different first.

   The `color` is repeated here rather than left to .nav-badge because this
   badge also renders inside a table cell and a nav link, both of which set
   their own text colour on descendants — a badge that came out black in
   the client table is exactly what that inheritance does. */
.nav-badge--mail {
  color: #fff;
  background: var(--status-pending);
}

/* A heading *inside* a settings section, for a section with two lists in it
   (the sender rules). Smaller and quieter than the section's own <h2>, so it
   reads as a division of that section rather than a new one — the 40px
   section gap that separates real sections would be wrong here. */
.settings-subheading {
  margin: 24px 0 4px;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* One "create a client" rule and its field mapping. A bordered block rather
   than another .settings-source-list row, because a rule owns a list of its
   own — without the outline it isn't clear which mappings belong to which
   sender once there are two rules. */
.rule-card {
  margin-bottom: 20px;
  padding: 14px 16px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.rule-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--hairline);
}

/* Spaced with margins, not padding inside the text, so the arrow doesn't
   collapse against a long label at narrow widths. */
.rule-card__arrow {
  margin: 0 8px;
  color: var(--ink-soft);
}

/* A status note sharing a .legend-row with the button it explains. The row is
   already a centred flex line, so the note only has to drop the vertical
   margins it carries when it's a standalone block. */
.legend-row__note {
  flex: 1 1 320px;
  margin: 0;
  max-width: 620px;
}

/* An unopened row in the thread list, marked with the same purple as its
   "New" pill so the two obviously refer to the same thing. The edge and the
   padding that keeps it clear of the text both live on .thread-list__item
   (transparent until a row is new) — carrying them here instead made the line
   sit tight against the subject and the triage buttons, and shifted every new
   row sideways. */
.thread-list__item--new {
  border-left-color: var(--status-pending);
}

/* The badge is a "look now" cue for someone at the screen; on paper it's
   noise attached to a nav that doesn't print either. */
@media print {
  .nav-badge {
    display: none;
  }
}

/* A synced calendar event on the month grid — its own --status-pending
   purple rather than one of the four order statuses, since an appointment
   isn't an order status (see "Design language" in CLAUDE.md). */
.chip--event {
  border-left: 4px solid var(--status-pending);
}

/* An event chip is a <button> — it opens that event's edit dialog. Stripped
   back to the chip it was: a real button here would put a control inside every
   calendar cell, which is the opposite of what a month grid should look like.
   Same click-to-edit pattern as the timeline's bars. */
button.chip--event {
  display: block;
  width: 100%;
  text-align: left;
  border-top: none;
  border-right: none;
  border-bottom: none;
  /* Family only, never the `font` shorthand: that would also reset the
     font-size .chip sets, and the button would render at body size — a chip
     several times the height of every other chip on the grid. */
  font-family: inherit;
  cursor: pointer;
}

button.chip--event:hover .chip__title {
  color: var(--status-pending);
}

/* Conversation list, on the client's Emails tab and the lead inbox. */
.thread-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.thread-list__item {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 0 12px 14px;
  border-bottom: 1px solid var(--hairline);
  /* Reserved on every row so marking one new (see .thread-list__item--new)
     only colours the edge — nothing moves, and the text keeps its gap. */
  border-left: 2px solid transparent;
}

.thread-list__item:last-child {
  border-bottom: none;
}

/* Subject and its "New" pill on one line. Needed because .thread-list__item
   is a column — without this the pill would sit on a row of its own. */
.thread-list__heading {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
}

.thread-list__subject {
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}

.thread-list__subject:hover {
  color: var(--status-progress);
}

.thread-list__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.thread-list__preview {
  font-size: 0.85rem;
  color: var(--ink-soft);
}

/* Triage buttons on a lead row. Each is its own <form> (the app has no JS
   layer for this), so they need laying out as a row rather than stacking as
   block elements. */
.thread-list__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}

.thread-list__actions .btn-secondary {
  padding: 4px 10px;
  font-size: 0.68rem;
}

/* One message inside a thread. Incoming and outgoing are distinguished by
   a border on opposite sides rather than by alignment — a chat-bubble
   layout would read as a different product than the rest of this app. */
.message {
  padding: 12px 16px;
  margin-bottom: 12px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}

.message--outgoing {
  border-left: 3px solid var(--status-progress);
}

.message--incoming {
  border-left: 3px solid var(--status-delivered);
}

.message__header {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}

/* Coloured to match the message's own left edge, so "You" and the client's
   name are each tied to the side of the conversation they came from without
   needing a second label to say so. */
.message__sender {
  font-weight: 600;
}

.message--incoming .message__sender {
  color: var(--status-delivered);
}

.message--outgoing .message__sender {
  color: var(--status-progress);
}

.message__meta {
  margin: 0;
  font-size: 0.7rem;
  color: var(--ink-soft);
}

/* pre-wrap keeps the sender's line breaks without needing |safe on text
   that came from outside — same reasoning as the invoice page's address
   block, which uses pre-line for the same reason. */
.message__body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.9rem;
  line-height: 1.55;
}

.message__attachments {
  margin: 12px 0 0;
  padding-left: 18px;
  font-size: 0.85rem;
}

/* The reply form overrides .detail-form's 360px cap: answering a full-width
   message in a box a third that wide reads as an afterthought, and the
   textarea should match the .message blocks above it. Only the message box
   wants the room, so the header fields are capped individually. */
.compose-form {
  max-width: none;
}

.compose-form__field {
  max-width: 360px;
}

.compose-form textarea {
  font-family: var(--font-body);
  width: 100%;
}

/* Documents picked to go out with the message, above the buttons. A plain
   .doc-list (so removing one is the app's usual trash icon, not a second
   convention), capped like .compose-form__field: a filename gains nothing
   from the width the textarea needs. */
.compose-form__attachments {
  max-width: 520px;
  margin: 12px 0 4px;
}

/* Send, then (on a reply, with an AI key saved) Suggest response, then the
   status line the suggestion writes into. Wraps rather than squeezing on a
   narrow screen, same as .field-row. */
.compose-form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}

/* Deliberately not a .warning-note: this is transient feedback about one
   button press, not a standing condition of the page, and giving it the
   amber panel would read as the form itself being in a bad state. Plain
   text in --ink-soft, red only when it's reporting a failure. */
.compose-form__ai-status {
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.compose-form__ai-status--error {
  color: var(--day-today);
}

/* Settings → AI. Exactly the .compose-form arrangement and for the same
   reason: .detail-form caps at 360px, which is right for a name or an
   amount and wrong for a paragraph of prompt instructions. The form runs
   full width so the prompt box does, and the key/model inputs — which
   gain nothing from the width — are capped back. */
.ai-form {
  max-width: none;
}

.ai-form__field {
  max-width: 360px;
}

.ai-form textarea {
  font-family: var(--font-body);
  width: 100%;
}

/* The rendering window. Wider than a form dialog because the point of it is
   looking at images side by side, and capped by viewport so a tall column of
   attempts scrolls inside the dialog rather than off it. */
.ai-render {
  max-width: min(900px, 92vw);
}

.ai-render .modal__body {
  max-height: 82vh;
  overflow-y: auto;
}

/* Same component as .compose-form__ai-status, and deliberately not a
   .warning-note: transient feedback about one button press, not a standing
   condition of the page. */
.ai-render__status {
  margin: 12px 0 0;
  font-size: 0.82rem;
  color: var(--ink-soft);
}

.ai-render__status--error {
  color: var(--day-today);
}

/* auto-fit rather than a fixed count: one attempt fills the width, three sit
   side by side, and nothing needs a media query. */
.ai-render__results {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  margin-top: 16px;
}

.ai-render__result {
  margin: 0;
}

.ai-render__result img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--hairline);
}

.ai-render__result figcaption {
  margin-top: 8px;
  font-size: 0.78rem;
  color: var(--ink-soft);
}

.ai-render__used-prompt {
  display: block;
  margin-bottom: 8px;
}

.ai-render__result-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.ai-render__saved {
  align-self: center;
  color: var(--ink-soft);
}

/* A heading *within* a section, below the section's own <h2>. */
.detail-subheading {
  margin: 24px 0 8px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------------------
   Print — used by the invoice page's "Print / save as PDF" button, so
   what comes out is the document alone, without the app around it.
   --------------------------------------------------------------------- */

@media print {
  .view-switch,
  .site-footer,
  .no-print {
    display: none !important;
  }

  .ledger {
    margin: 0;
    padding: 0;
    max-width: none;
  }

  .invoice-doc {
    border: none;
    padding: 0;
  }

  .invoice-doc__party a {
    color: var(--ink);
  }

  .table-scroll {
    overflow-x: visible;
  }
}