/* GENERATED — do not edit by hand.
 *
 * Assembled by cmd/cssbuild from sources.txt.
 * Edit a part under css/, then:
 *
 *   go generate ./cmd/desk/...
 *
 * The pre-commit hook rebuilds this file and refuses the commit if it was
 * stale, so what follows is always what the parts say.
 */

/* ---------------------------------------------------------------- 01-base.css */
/* Quinze jetons vivaient ici et aucun n'arrivait jusqu'à l'écran :
   design.css charge après cette feuille et son bloc « App-wide retint » les
   redéfinit tous à partir des jetons --db-*. Mesuré, pas supposé — voir
   CSS.md, « Comparer deux jeux de règles ». Ne restent que les deux que
   design.css laisse volontairement tranquilles, et il écrit pourquoi :
   « --off/--off-bg stay untouched: that pair means disabled/inactive, not a
   status, so it keeps its plain gray ». */
:root {
  --off-bg: #eceff1;
  --off: #5b6673;
}

* { box-sizing: border-box; }

body {
  font-family: var(--db-font-body, system-ui), -apple-system, "Segoe UI", sans-serif;
  margin: 0;
  color: var(--text);
  background: var(--bg);
}

h1 {
  font-family: var(--db-font-display, Georgia), serif;
  font-weight: 600;
  font-size: 1.6rem;
  margin: 0 0 0.25rem;
}

.property-header {
  background-size: cover;
  background-position: center;
  border-radius: 12px;
  padding: 1.5rem 1.75rem;
  margin-bottom: 1.5rem;
}
.property-header[style*="background-image"] {
  color: #fff;
}
.property-header[style*="background-image"] h1,
.property-header[style*="background-image"] .muted {
  color: #fff;
}
.property-header[style*="background-image"] a {
  color: #fff;
  text-decoration: underline;
}
h2 {
  font-family: var(--db-font-display, Georgia), serif;
  font-weight: 600;
  font-size: 1.15rem;
  margin: 0 0 0.75rem;
  color: var(--brand);
}

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

.muted { color: var(--muted); }

/* Layout */
.app-shell {
  display: flex;
  min-height: 100vh;
}

/* La barre latérale : ce qui reste ici après la refonte. design.css la
   redessine — largeur, fond, couleurs, rythme — et gagne, puisqu'il charge
   après. Ce qui suit est ce que design.css ne dit pas : la structure en
   colonne, les alignements, la casse des libellés de section. Dix-huit
   déclarations de couleur et d'espacement ont été retirées d'ici parce
   qu'elles n'atteignaient jamais l'écran ; les garder donnait à lire une
   barre latérale bleu-vert de 220px qui n'existe nulle part. */
.sidebar {
  color: #fff;
  display: flex;
  flex-direction: column;
}

.sidebar .brand {
  letter-spacing: 0.02em;
  display: flex;
  align-items: center;
}
.sidebar .brand img { flex: none; width: auto; }

.sidebar nav {
  display: flex;
  flex-direction: column;
}

.sidebar nav a {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.sidebar nav a i {
  width: 1.1em;
  text-align: center;
}

.sidebar nav a:hover { text-decoration: none; }

/* Grouped-nav section labels — split the flat sidebar link list into
   logical areas (Opérations / Finances / Propriétés / Propriétaires /
   Intégrations) without changing the link markup or hover behaviour. */
.sidebar nav .nav-section {
  padding: 0 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
}
.sidebar nav .nav-section:first-child { margin-top: 0; }

.content {
  flex: 1;
  padding: 2rem 2.5rem;
  max-width: 1100px;
  min-width: 0;
}

/* Mobile navigation: below 900px, the sidebar is replaced entirely by the
   bottom tab bar (.tab-bar, see design.css + nav.html) — no off-canvas
   drawer anymore. The old checkbox-hack toggle/backdrop classes are gone;
   nav.html no longer emits that markup. */
@media (max-width: 899px) {
  .app-shell { flex-direction: column; }
  .sidebar { display: none; }
  .content {
    padding: 1rem;
    max-width: 100%;
    /* Room for the fixed .tab-bar (see design.css) so content isn't hidden
       behind it. */
    padding-bottom: calc(1rem + 64px + env(safe-area-inset-bottom, 0px));
  }

  .field-grid { grid-template-columns: 1fr; }
  form input, form select, form textarea { min-width: 0; width: 100%; }
  .field input, .field select, .field textarea { max-width: 100%; }
}

/* Cards row.

   Two `.cards` overrides used to live above this rule — `1fr` below 900px and
   `repeat(2, 1fr)` between 481px and 899px — and neither ever applied: same
   specificity, earlier in the file, so this rule won at every width. They
   were removed rather than moved, because the auto-fit track below already
   produces the same column counts, and they misdirected a bug report about
   clipped amounts to a breakpoint that had nothing to do with it. */
.cards {
  display: grid;
  /* 200px, not 150: .card-value is nowrap monospace at 1.6rem, and an
     amount like "19 830,15 €" needs the room — below this the € was
     silently clipped off the end of the card. min(100%, …) keeps the
     track from overflowing a narrow phone. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 1rem;
  margin: 1.5rem 0;
}

/* Grids whose cards hold a euro amount rather than a count.

   200px is right for "3" or "16 / 20" but too narrow for money, and the
   shortfall is not confined to one breakpoint: auto-fit adds a column as soon
   as the container allows it, so the track drops back to ~200px at every
   column change and the amount clips again. That produced a sawtooth — fine
   at 600px, clipped at 740px, fine at 860px, clipped at 1040px — which is why
   raising this floor is the fix rather than patching a media query.

   The arithmetic: a .card spends 1.2rem of padding each side plus 1px of
   border, so a 232px track leaves ~192px of content. The value is monospace
   at 1.6rem, i.e. ~15.4px per character, so that holds a 12-character amount
   ("599 902,40 €") with a little to spare. Past 9 999 999,99 € — 14
   characters — raise this to 256px, or the € starts being clipped again. */
.cards-money {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 232px), 1fr));
}

.card {
  background: var(--panel-bg);
  border: 1px solid transparent;
  border-radius: var(--radius-lg, 16px);
  padding: 1rem 1.2rem;
  box-shadow: var(--db-shadow-sm, none);
}

.card-value {
  font-family: var(--db-font-mono, inherit);
  font-variant-numeric: tabular-nums;
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--brand);
  white-space: nowrap;
}

/* A card headed by a name rather than a figure — the property tiles on an
   owner's page. It cannot be .card-value: that class is nowrap on purpose, so
   an amount never breaks mid-number, and a property name held to one line
   runs straight off the card and drags the whole document sideways with it
   (/owners/4 at 390px scrolled to 405 because of exactly this). Names wrap.
   overflow-wrap covers the case with no space to break at. */
.card-name {
  font-family: var(--db-font-mono, inherit);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--brand);
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 0;
}

.bookings-table td:nth-child(2),
.bookings-table td:nth-child(3),
.bookings-table td:nth-child(4) {
  white-space: nowrap;
}

/* Monetary amount cells across every financial table (bookings, finances
   history, owners list, upcoming reservations, etc.) — always right-aligned
   and non-wrapping so figures stay scannable in a column. */
.amount, th.amount {
  text-align: right;
  white-space: nowrap;
}
td.amount {
  font-family: var(--db-font-mono, inherit);
  font-variant-numeric: tabular-nums;
}

/* Plain right-aligned counts that aren't money (capacity, bedroom count,
   days booked) — same alignment/no-wrap as .amount, but no monospace/ledger
   typeface: that's specifically for currency, and reusing it here made a
   simple property list read like a spreadsheet of figures. */
.num, th.num {
  text-align: right;
  white-space: nowrap;
}
td.num { font-variant-numeric: tabular-nums; }

/* Date/timestamp cells that must not wrap mid-value (cleaning turnover
   dates, communication timestamps, quote "Créé le", etc.) — same rationale
   as .amount above, just not right-aligned. */
.date-cell { white-space: nowrap; }

.card-label {
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 0.2rem;
}

.card-link {
  display: block;
  transition: border-color .15s ease, transform .15s ease;
}
.card-link:hover {
  text-decoration: none;
  border-color: var(--brand);
  transform: translateY(-1px);
}
.card-alert {
  border-color: var(--error);
  background: var(--error-bg);
}
.card-alert .card-value {
  color: var(--error);
}

/* Semantic card variants — meaning, not decoration */
.card-money {
  border-color: #eaceA0;
  background: var(--money-bg);
}
.card-money .card-value { color: var(--money); }

.card-people {
  border-color: #bfdccb;
  background: var(--people-bg);
}
.card-people .card-value { color: var(--people); }

.over-budget { color: var(--error); }

.panel-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
}
.panel-header h2 { margin: 0; }
.panel-header .inline-form { gap: 0.5rem; }

.inline-form {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.8rem;
  margin: 0;
}
.inline-form label {
  display: flex;
  flex-direction: column;
  font-size: 0.85rem;
  gap: 0.25rem;
  margin: 0;
}
.inline-form input, .inline-form select { min-width: 140px; }
.inline-form button { margin-top: 0; }

details {
  margin-top: 1rem;
  border-top: 1px solid var(--border);
  padding-top: 0.8rem;
}
details summary {
  cursor: pointer;
  color: var(--brand);
  font-weight: 600;
  margin-bottom: 0.6rem;
}

/* Panels */
.panel {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg, 16px);
  padding: 1.25rem 1.5rem;
  margin-bottom: 1.5rem;
  box-shadow: var(--db-shadow-sm, none);
}

.empty-state {
  color: var(--muted);
  font-style: italic;
}

/* /argent — one .panel card per owner in a responsive grid */
.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1rem;
  margin: 1.5rem 0;
}
.cards-grid .panel { margin-bottom: 0; }
.argent-owner-rows { display: flex; flex-direction: column; gap: 0.75rem; }
.argent-owner-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.argent-owner-row-label { color: var(--muted); }
.argent-owner-row-amount {
  font-family: var(--db-font-mono, inherit);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 1.1rem;
}
.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.85rem; }

/* Tables */
.row-thumb {
  display: block;
  width: 44px;
  height: 44px;
  border-radius: 6px;
  background-size: cover;
  background-position: center;
  background-color: var(--off-bg);
}

.publish-summary { margin: 0; padding-left: 1.1rem; font-size: 0.88rem; }
.publish-summary li { margin: 0.15rem 0; }
.table-wrap { overflow-x: auto; margin: 0.5rem 0; }
.table-wrap table { margin: 0; }
table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
th, td { border: 1px solid var(--border); padding: 0.5rem 0.7rem; text-align: left; font-size: 0.92rem; vertical-align: top; }
td .inline-form { flex-wrap: nowrap; }
td.review-text { max-width: 26rem; }
th {
  background: var(--brand-light);
  color: var(--brand);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
tr:nth-child(even) td { background: #fafcfd; }

/* Properties list — grouped by pricing tier, one panel per group */
.badge-count {
  background: var(--brand-light);
  color: var(--brand);
  font-variant-numeric: tabular-nums;
}
.property-table th { border-top: none; border-bottom-width: 2px; letter-spacing: 0.02em; text-transform: uppercase; font-size: 0.75rem; }
.property-table td { border-left: none; border-right: none; }
.property-table tr:last-child td { border-bottom: none; }
.property-table .cell-thumb { width: 3.2rem; padding-right: 0; }
.property-table .row-thumb { width: 56px; height: 56px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
.property-table .cell-name a { font-weight: 600; font-size: 1rem; color: var(--text, #1b2a33); }
.property-table tbody tr { transition: background-color 0.12s ease; }
.property-table tbody tr:hover td { background: var(--brand-light); }

@media (max-width: 640px) {
  /* Card layout: hide the header row and lay each <td> out as a labeled
     block row instead of a table cell, so property rows read as compact
     cards rather than forcing horizontal scroll on a phone. */
  .property-table, .property-table tbody, .property-table tr, .property-table td { display: block; width: 100%; }
  .property-table thead { display: none; }
  .property-table tr {
    border: 1px solid var(--border);
    border-radius: 8px;
    margin-bottom: 0.8rem;
    /* Right padding clears the absolutely-positioned thumbnail below. It
       used to be applied to .cell-name alone, so the name wrapped correctly
       but every OTHER row's value (capacity, bedrooms) slid underneath the
       image. Reserving the gutter on the row itself covers all of them,
       whatever the card ends up containing. */
    padding: 0.6rem 4.2rem 0.6rem 0.8rem;
    position: relative;
  }
  .property-table tr:hover td { background: none; }
  .property-table td { border: none; padding: 0.3rem 0; display: flex; justify-content: space-between; align-items: center; gap: 0.6rem; }
  .property-table td[data-label]:not([data-label=""])::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    flex: 0 0 auto;
  }
  .property-table .cell-thumb {
    position: absolute;
    top: 0.6rem;
    right: 0.8rem;
    width: auto;
    padding: 0;
  }
  .property-table .cell-name { padding-right: 0; }
  .property-table .cell-name::before { display: none; }
  .property-table .cell-name a { font-size: 1.05rem; }
  .property-table .row-thumb { width: 48px; height: 48px; }
}

/* Booking option picker (/properties/{id}/bookings/new) — same
   table-becomes-cards technique as .property-table, adapted for a
   checkbox+label header row and a quantity input instead of plain text. */
@media (max-width: 640px) {
  /* Grid, not flex-wrap: with flex-wrap, line-breaking decisions are made
     from each item's un-shrunk (max-content) width, so a long option name
     could get pushed to its own line instead of just wrapping its text —
     a real bug caught in testing. Grid with explicit column/span placement
     has no such ambiguity: checkbox+name always share row 1 (columns 1/2),
     each detail row (price, unit, quantity) explicitly spans both columns
     and grid auto-placement stacks them on their own row, regardless of
     text length. */
  .option-table, .option-table tbody { display: block; width: 100%; }
  .option-table thead { display: none; }
  .option-table tr {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 0.6rem;
    row-gap: 0.35rem;
    align-items: center;
    border: 1px solid var(--border);
    border-radius: 8px;
    margin-bottom: 0.8rem;
    padding: 0.7rem 0.9rem;
  }
  .option-table td { border: none; padding: 0; }
  .option-table .cell-check { grid-column: 1; }
  .option-table .cell-name { grid-column: 2; }
  .option-table .cell-check input[type="checkbox"] { width: 1.3rem; min-width: 0; height: 1.3rem; }
  .option-table .cell-name label { font-weight: 600; font-size: 1rem; }
  .option-table td:not(.cell-check):not(.cell-name) {
    grid-column: 1 / -1;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.6rem;
    border-top: 1px dashed var(--border);
    padding-top: 0.5rem;
  }
  .option-table td[data-label]:not([data-label=""])::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
  }
  .option-table td input[type="number"] { min-width: 0; max-width: 6rem; }
}

/* Badges */
.heatmap {
  display: grid;
  grid-template-columns: repeat(6, 1.9rem);
  gap: 4px;
}
.heatmap-weekgrid {
  grid-template-columns: repeat(7, 2rem);
  max-width: none;
  gap: 3px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px;
  width: max-content;
}
.heatmap-cell {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 2px;
  font-size: 0.68rem;
  color: var(--muted);
  background: var(--off-bg);
  cursor: default;
}
.heatmap-weekgrid .heatmap-cell { border-radius: 0; }
.heatmap-blank { background: var(--card, #fff); }
.heatmap-cal { width: max-content; }
.heatmap-cal-dow,
.heatmap-cal-row {
  display: grid;
  grid-template-columns: 4.5rem repeat(7, 2rem);
  gap: 3px;
  align-items: center;
}
.heatmap-cal-dow {
  font-size: 0.7rem;
  color: var(--muted);
  text-align: center;
  margin-bottom: 3px;
}
.heatmap-cal-row {
  margin-bottom: 3px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px;
}
.heatmap-cal-month {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--brand);
  text-transform: capitalize;
  background: var(--card, #fff);
  padding-right: 0.4rem;
  text-align: right;
}
.heatmap-cal-row .heatmap-cell { border-radius: 0; }
.heatmap-cell.level-1 { background: #cfe4e9; color: var(--brand); }
.heatmap-cell.level-2 { background: #8fc0ca; color: #fff; }
.heatmap-cell.level-3 { background: #4f97a6; color: #fff; }
.heatmap-cell.level-4 { background: var(--brand); color: #fff; }
.heatmap-legend {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.7rem;
  font-size: 0.78rem;
  color: var(--muted);
}
.heatmap-legend .heatmap-cell { width: 1rem; height: 1rem; aspect-ratio: unset; flex: 0 0 auto; }

/* Compact single-row nights-booked strip (/overview) — same .heatmap-cell
   coloring as the old calendar grid, no weekday/month chrome: this is a
   reference glance, not a calendar to read dates off of (hover for the
   date via the cell's title attribute). */
.heatmap-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
}
.heatmap-strip .heatmap-cell {
  width: 1.7rem;
  height: 1.7rem;
  aspect-ratio: unset;
  border-radius: 4px;
  flex: 0 0 auto;
}

/* /cleaning/calendar's .cc-* grid/agenda styles now live in design.css
   (Breton tokens, plus the mobile agenda layout) — see that file. */

/* Assign-cleaning dialog — a native <dialog>, not an anchored popover:
   anchoring reliably near whichever card/cell was clicked (accounting for
   viewport edges and the grid's own scroll container) adds real
   complexity for a benefit that's marginal here, so a centered modal stays
   the pragmatic choice, same everywhere it's opened from (grid cell,
   agenda card, size-card thumb). */
#assign-dialog {
  border: none;
  border-radius: var(--radius-lg, 14px);
  padding: 0;
  box-shadow: 0 12px 36px rgba(0,0,0,0.2);
  max-width: 400px;
  width: calc(100% - 2rem);
  overflow: hidden;
}
#assign-dialog::backdrop { background: rgba(0,0,0,0.4); }
#assign-dialog form { display: flex; flex-direction: column; max-height: 80vh; }

.assign-dialog-head {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 1.1rem 1.2rem;
  border-bottom: 1px solid var(--db-line-soft, var(--border));
  flex: none;
}
.assign-dialog-thumb { width: 44px; height: 44px; border-radius: 10px; flex: none; }
.assign-dialog-thumb[hidden] { display: none; }
.assign-dialog-head-text { flex: 1; min-width: 0; }
.assign-dialog-head-text h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  color: var(--db-ink, var(--text));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.assign-dialog-sub { margin: 0.15rem 0 0; font-size: 0.8rem; color: var(--db-ink-soft, var(--text-muted)); }
.assign-dialog-close {
  flex: none;
  border: none;
  background: transparent;
  color: var(--db-ink-faint, var(--text-muted));
  font-size: 1rem;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.assign-dialog-close:hover { background: var(--db-stone, var(--bg)); color: var(--db-ink, var(--text)); }

.assign-staff-list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.6rem;
  overflow-y: auto;
}
.assign-staff-row {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  border: 1px solid transparent;
  border-radius: 10px;
  background: none;
  font-family: var(--db-font-body);
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
  color: var(--db-ink, var(--text));
}
.assign-staff-row:hover { background: var(--db-tide-mist, var(--bg)); }
.assign-staff-row.is-current { background: var(--db-tide-mist, var(--bg)); border-color: var(--db-tide, var(--brand)); }
.assign-staff-row-icon {
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--db-tide-mist, var(--bg));
  color: var(--db-tide, var(--brand));
  font-weight: 700;
  font-size: 0.82rem;
  display: flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
}
.assign-staff-row-unassign .assign-staff-row-icon { background: var(--db-stone, var(--bg)); color: var(--db-ink-faint, var(--text-muted)); font-size: 0.78rem; }
.assign-staff-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.assign-staff-row-unassign .assign-staff-row-name { font-weight: 500; color: var(--db-ink-soft, var(--text-muted)); }
.assign-staff-row-role { flex: none; font-size: 0.72rem; color: var(--db-ink-faint, var(--text-muted)); }
.assign-staff-row-check { flex: none; width: 1rem; color: var(--db-tide, var(--brand)); visibility: hidden; }
.assign-staff-row.is-current .assign-staff-row-check { visibility: visible; }

/* [hidden] est une règle de la feuille du navigateur : n'importe quelle règle
   d'auteur qui donne un display à l'élément l'emporte, et l'élément « caché »
   se peint. Le bureau s'est fait avoir cinq fois — .tabpanel, .stay-chosen,
   .cat-meal-drop, #stay-search, .assign-dialog-thumb ont chacun leur rustine.
   Celle-ci vaut pour tous : deux sélecteurs simples, donc plus spécifique que
   la classe qui pose le display, et sans !important — qui voudra vraiment
   montrer un élément caché devra le dire aussi précisément. */
[hidden]:not([hidden="until-found"]) { display: none; }

.notice {
  display: block;
  padding: 0.6rem 1rem;
  border-radius: 8px;
  font-size: 0.88rem;
  margin-bottom: 1rem;
}
/* Long unbroken tokens in a notice (env var names, ids, URLs) otherwise
   run straight past the notice's right edge on a phone — no space to break
   at, and the default `overflow-wrap: normal` refuses to break mid-token. */
.notice code, .notice a { overflow-wrap: anywhere; }
.notice-ok { background: var(--ok-bg); color: var(--ok); }
.notice-error { background: var(--error-bg); color: var(--error); }
/* L'orange des états en attente : ni une confirmation, ni une erreur. Le
   bandeau « dates non acquises » et le badge « partiellement réglé » le
   demandaient tous les deux et aucun des deux n'existait — le badge
   s'affichait donc sans fond depuis qu'un séjour peut être réglé à
   plusieurs, c'est-à-dire dans l'état où ces séjours passent le plus clair
   de leur temps. */
.notice-warn { background: var(--pending-bg); color: var(--pending); }

.badge {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
}
.badge-ok { background: var(--ok-bg); color: var(--ok); }
.badge-off { background: var(--off-bg); color: var(--off); }
.badge-error { background: var(--error-bg); color: var(--error); }
.badge-warn { background: var(--pending-bg); color: var(--pending); }
/* A closed line that is neither pending nor holding money — a restored
   caution. Grey would have read as "still waiting for it". */
.badge-neutral { background: var(--db-tide-mist); color: var(--db-tide); }

/* Forms */
fieldset { border: 1px solid var(--border); border-radius: 10px; margin-bottom: 1.25rem; padding: 1.1rem 1.3rem 1.3rem; background: #fbfcfd; }
fieldset:last-of-type { margin-bottom: 0; }
legend { padding: 0 0.5rem; color: var(--brand); font-weight: 700; font-size: 0.95rem; }
form label { display: block; margin: 0.5rem 0; font-size: 0.92rem; }
form input, form select, form textarea {
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  min-width: 200px;
  font: inherit;
  background: #fff;
  transition: border-color .15s ease, box-shadow .15s ease;
}

/* Checkboxes and radios are not text fields: the 200px min-width above was
   stretching every checkbox to 200px, pushing its own label that far to the
   right and making a labelled setting read as two unrelated things. */
form input[type="checkbox"],
form input[type="radio"] {
  min-width: 0;
  width: auto;
  padding: 0;
  border-radius: 3px;
}
/* Consistent height across text/date/number inputs and <select> — browsers
   size these differently by default (native select chrome in particular),
   which is what made form rows look uneven. Checkboxes/radios are excluded
   entirely: they have their own small fixed sizing elsewhere and would
   otherwise inherit this height and lose their native look. */
form input:not([type="checkbox"]):not([type="radio"]),
form select,
form textarea {
  min-height: 2.6rem;
  line-height: 1.3;
  -webkit-appearance: none;
  appearance: none;
}
/* Native <select> chevron, since appearance:none above (needed to make
   selects match text inputs' height/border consistently across browsers)
   also removes the default one. */
form select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23667380'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.19l3.71-3.96a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 1.1em;
  padding-right: 2rem;
}
form input:focus, form select:focus, form textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-light);
}
form textarea { min-width: 100%; max-width: 100%; }

/* Field groups: a labelled control plus optional helper/hint text underneath */
.field { margin: 0.9rem 0; }
.field:first-child { margin-top: 0; }
.field label,
.field > .field-label {
  display: block;
  font-weight: 600;
  font-size: 0.88rem;
  margin-bottom: 0.3rem;
  color: var(--text);
}
.field input, .field select, .field textarea { width: 100%; max-width: 340px; }
.field input[type="checkbox"] { width: auto; max-width: none; }
.field-wide input, .field-wide select, .field-wide textarea { max-width: 100%; }
.field-hint { display: block; color: var(--muted); font-size: 0.8rem; margin-top: 0.3rem; }

.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 0.4rem 1.2rem;
}
.field-grid .field { margin: 0.5rem 0; }
/* Capped, not "none": auto-fill gives a lone field on its own row the full
   row width (1fr of a single column spanning the whole grid), ballooning
   it far wider than its siblings above just because of where it happened
   to wrap to — same visual cap as the plain (non-grid) .field class keeps
   every input a consistent, predictable width regardless of row position. */
.field-grid .field input, .field-grid .field select { width: 100%; max-width: 340px; }

/* Fixed 2-column layout (e.g. amount+status pairs) regardless of viewport
   width, instead of .field-grid's default auto-fill reflow. */
.field-grid-2col { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 480px) {
  .field-grid-2col { grid-template-columns: 1fr; }
}

.checkbox-field { display: flex; align-items: flex-start; gap: 0.5rem; font-weight: 500; }
.checkbox-field input { margin-top: 0.2rem; }

/* /settings/desk-operators — one checkbox per capability, wrapping grid */

/* Checkbox/tag grids (amenities, tags) */
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0.5rem 1rem;
}
.check-item {
  display: block;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #fff;
  font-size: 0.9rem;
  cursor: pointer;
}
.check-item input[type="checkbox"] { vertical-align: top; margin: 0.15rem 0.5rem 0 0; }
.check-label { display: inline; }
.check-item:has(input:checked) { border-color: var(--brand); background: var(--brand-light); }
/* Un réglage porté par la ligne d'une case à cocher : sur sa propre ligne, en
   retrait, pour qu'on lise d'abord si l'option est proposée et seulement
   ensuite à quelle condition. Le champ est étroit — c'est un nombre de nuits,
   pas une somme — et son placeholder porte la valeur du catalogue, ce qui fait
   que « vide » se lit comme « celle-là » plutôt que comme un oubli. */
.check-setting { display: block; margin: 0.35rem 0 0 0; font-size: 0.85rem; color: var(--muted, #5f6970); }
/* min-width: 0 contre les 200px que « form input » pose plus haut : sans lui
   un champ de deux chiffres occupe toute la ligne. */
.check-setting input[type="number"] {
  width: 4.5rem; min-width: 0; padding: 0.15rem 0.35rem; font-size: 0.85rem;
}
/* La condition de durée sur une ligne d'option, et la ligne quand elle n'est
   pas atteinte. Grisée plutôt que retirée : disparaître ferait chercher une
   option qu'on sait exister, rester visible avec sa condition explique. */
.option-min-note { font-size: 0.8rem; color: var(--muted, #5f6970); white-space: nowrap; }
tr.is-unavailable { opacity: 0.55; }
tr.is-unavailable .option-min-note { color: var(--db-critical, #a33); }

/* Photo gallery grids (property Images page + /media library) — mirrors
   .check-grid's card sizing/spacing, sized for thumbnails instead of text. */
.img-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 1rem;
}
.img-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #fff;
  overflow: hidden;
}
.img-card img { display: block; width: 100%; height: 130px; object-fit: cover; }
.img-card video { display: block; width: 100%; height: 130px; object-fit: cover; background: #1f2430; }
.img-card-video-badge { position: relative; }
.img-card-video-badge::after { content: "\f03d"; font-family: "Font Awesome 6 Free"; font-weight: 900; position: absolute; top: 0.4rem; right: 0.5rem; color: #fff; background: rgba(0,0,0,0.55); border-radius: 0.25rem; padding: 0.15rem 0.35rem; font-size: 0.75rem; pointer-events: none; }
.img-card-body { padding: 0.5rem 0.6rem; font-size: 0.82rem; }
/* Les deux choix l'un sous l'autre, et non côte à côte : « Photo de vignette
   (liste de recherche) » et « Photo de couverture (fiche gîte) » sont des
   phrases, pas des mots, et deux phrases sur une ligne dans une carte de
   200px débordaient sous la carte voisine. On ne le voyait pas tant que les
   vignettes ne s'affichaient pas. */
.img-card-flags { display: flex; flex-direction: column; gap: 0.3rem; margin: 0.3rem 0; }
.img-card-flags form { display: block; }
.img-card-flags label { display: flex !important; align-items: flex-start; gap: 0.35rem; cursor: pointer; margin: 0 !important; line-height: 1.35; }
/* Le bouton radio ne rétrécit pas et reste aligné sur la première ligne du
   libellé, qui peut en faire deux. */
.img-card-flags input[type="radio"] { flex: 0 0 auto; margin-top: 0.2rem; }
.img-card-flags input[type="radio"] { min-width: 0; width: auto; padding: 0; border: none; }
.img-card-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-top: 0.3rem; }
.img-card-actions form { margin: 0; }
.img-card-actions button, .poster-form label { margin-top: 0; padding: 0.35rem 0.7rem; font-size: 0.78rem; }
/* The video-poster action is a <label> driving a hidden file input — that
   way choosing a cover image needs no intermediate page — so it gets none
   of the button rules above and has to be dressed as one by hand. */
.poster-form label {
  display: inline-block;
  background: #fff;
  color: var(--brand);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  font-weight: 600;
  cursor: pointer;
}
.poster-form label:hover { background: var(--brand-light); }

/* Booking property picker (/bookings/new) — clickable thumbnail cards,
   grouped by pricing tier like /properties, instead of a dropdown. */
.picker-card {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: border-color 0.12s ease, transform 0.12s ease;
}
.picker-card:hover {
  text-decoration: none;
  border-color: var(--brand);
  transform: translateY(-2px);
}
.picker-card-noimg {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 130px;
  background: var(--off-bg);
  color: var(--muted);
  font-size: 1.6rem;
}

/* inline-flex, not the browser default: an <a> carrying any of these classes
   is an inline box, whose vertical padding does not contribute to line
   height, so its padding box escaped the line and overlapped whatever sat
   above it (on the booking Paiement tab the Rapprocher link cut 5px into the
   button above it). <button> was never affected — it is inline-block
   already — which is why the bug only ever showed on the anchor-styled ones.
   The list is separate from the rule below because the variants are used
   without .btn (class="btn-small btn-secondary"), and only display needs
   fixing for them: everything else about their look is already right. */
button, .btn, .btn-small, .btn-secondary, .btn-danger, .btn-outline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
}

button, .btn {
  margin-top: 0.9rem;
  padding: 0.6rem 1.3rem;
  background: var(--brand);
  color: #fff;
  border: none;
  border-radius: var(--radius-md, 10px);
  cursor: pointer;
  font-size: 0.92rem;
  font-weight: 600;
  transition: background .15s ease, transform .1s ease;
}
button:hover, .btn:hover { background: #163b48; }
button:active, .btn:active { transform: translateY(1px); }

.btn-secondary {
  background: #fff;
  color: var(--brand);
  border: 1px solid var(--border);
}
.btn-secondary:hover { background: var(--brand-light); }

/* Unavailable, not merely quiet. A control that cannot act yet has to keep
   its shape — same box, same baseline, sitting in the row of buttons it
   belongs to — because turning it into a bare line of text is what makes it
   look like a typo next to the real ones. Faded and not-allowed says "later",
   which is true; hiding it would lose the fact that a document is expected at
   all. Pair it with a title that says WHY, since a greyed control that does
   not explain itself is just a dead end. */
button:disabled, .btn:disabled, .btn-small:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  filter: grayscale(1);
}
button:disabled:hover, .btn:disabled:hover, .btn-small:disabled:hover {
  background: inherit;
}

.btn-danger {
  background: var(--error-bg);
  color: var(--error);
  border: 1px solid #f3c8c4;
}
.btn-danger:hover { background: #f7d6d3; }

.btn-small { margin-top: 0; padding: 0.35rem 0.75rem; font-size: 0.82rem; }

/* .form-actions vivait ici ET plus bas, avec les quatre mêmes propriétés :
   la version basse gagnait, celle-ci se lisait pour rien. Une seule reste. */

section.panel ul, section.panel ol { padding-left: 1.3rem; }
section.panel li { margin-bottom: 0.4rem; }

/* Customer communication thread (mirrors the guest portal message list) */
.messages { display: flex; flex-direction: column; gap: 0.6rem; margin: 0.75rem 0; }
.message { border-radius: 8px; padding: 0.6rem 0.9rem; max-width: 70%; }
.message-guest { background: var(--off-bg); align-self: flex-start; }
.message-concierge { background: var(--brand-light); align-self: flex-end; }
.message-owner { background: var(--db-sand-mist, var(--off-bg)); align-self: flex-start; }
.message-meta { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.78rem; color: var(--muted); margin-bottom: 0.2rem; }
.message-body { white-space: pre-wrap; }

/* Airbnb-style date-range popover (daterange.js) — enhancement layer over
   the plain <input type="date"> pair, which stays the real form data. Visual
   language ported from the public site's own hand-rolled picker
   (hugo-site/assets/css/style.css's .ab-pill/.ab-cal-* section) — rounded
   pill trigger, circular day buttons, filled range background — using this
   app's own tokens (--brand etc.) instead of the site's --accent/--card. */
/* Used two ways: wrapping a nested .field-grid (quote_new.html/booking_new.html
   — that grid supplies the row layout, .daterange itself just sits in normal
   block flow within its own <section>) or, on filter forms, as a direct
   child of .inline-form (quotes_list.html/customers_list.html etc.) — there
   .daterange needs its own row layout so its two <label>s (and the trigger
   button daterange.js inserts between them) sit inline like every other
   .inline-form field instead of stacking. */
.inline-form .daterange { display: flex; flex-wrap: wrap; gap: 0.8rem; align-items: end; }
/* Once daterange.js successfully enhances a container it adds this class
   and the trigger becomes the only visible/interactive control for the
   whole range — hide the two native <input type=date> (still real form
   data, just not shown) and the second label entirely (its own text plus
   input are redundant once the trigger shows "start → end"); the first
   label's text stays as a small heading for the trigger next to it. Both
   inputs stay fully visible/usable with JS disabled, since this is a class
   added by the script itself, not a blanket CSS rule. */
.daterange-js input[type="date"] { display: none; }
.daterange-js label:has([data-daterange-end]) { display: none; }
/* Le déclencheur est un CHAMP, pas un bouton flottant : il remplace deux
   <input type=date> et se tient dans la même colonne, à la même hauteur, avec
   la même bordure. La pilule ombrée et arrondie qu'il était flottait au-dessus
   du formulaire sans lui appartenir — et, portant deux dates ISO, elle passait
   à la ligne. */
.daterange-trigger {
  margin: 0.35rem 0 0;
  display: inline-flex;
  align-items: center;
  /* Les mêmes mesures qu'un champ du formulaire — hauteur minimale, marge
     intérieure, rayon — et non celles d'un bouton. C'était l'intention écrite
     juste au-dessus, sans être tenue : plus haut, plus arrondi, en gras et
     centré, il se lisait comme une action à côté des listes voisines au lieu
     de se lire comme la commande qui porte les dates. */
  justify-content: flex-start;
  gap: 0.5rem;
  min-height: 2.6rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel-bg, #fff);
  color: var(--text);
  font: inherit;
  font-weight: 400;
  cursor: pointer;
  white-space: nowrap;
  text-align: left;
}
.daterange-trigger i { color: var(--brand); }
.daterange-trigger:hover { background: var(--brand-light); }
.daterange-popover {
  position: absolute;
  z-index: 40;
  background: var(--panel-bg, #fff);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 12px 36px rgba(20, 30, 25, 0.16);
  padding: 1.1rem;
  min-width: min(560px, 90vw);
  max-width: calc(100vw - 1.5rem);
  overflow-x: auto;
}
.daterange-nav { display: flex; justify-content: space-between; margin-bottom: 0.4rem; }
.daterange-nav-btn {
  width: 1.8rem; height: 1.8rem; border-radius: 50%;
  border: 1px solid var(--border); background: var(--panel-bg, #fff);
  color: var(--brand); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.9rem;
}
.daterange-nav-btn:hover { background: var(--brand-light); }
/* La géométrie du calendrier public, dans la palette du desk.
   Trois choses différaient et se voyaient : des colonnes de largeur fixe
   (1.9rem) au lieu de 1fr, donc une grille qui ne remplissait pas le popover ;
   un aspect-ratio:1 sur des cellules en display:flex, qui écartait les
   rangées ; et un écart de 3px au lieu de 2. Le reste — deux mois, en-tête
   centré, jours en pastilles — était déjà le même. */
.daterange-months { display: flex; gap: 2rem; }
.daterange-month { flex: 1 1 50%; }
.daterange-month-heading {
  font-weight: 700; color: var(--text); text-transform: capitalize;
  font-size: 0.9rem; margin-bottom: 0.6rem; text-align: center;
}
.daterange-dow, .daterange-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
}
.daterange-dow { text-align: center; margin-bottom: 0.3rem; }
.daterange-dow span { font-size: 0.72rem; color: var(--muted); text-align: center; }
.daterange-day {
  border: none; background: none; font: inherit; font-size: 0.85rem;
  padding: 0.4rem 0; border-radius: 50%;
  cursor: pointer; color: var(--text);
}
.daterange-day:hover:not(:disabled):not(.is-selected) { background: var(--brand-light); }
.daterange-day.is-blank { visibility: hidden; }
.daterange-day.is-past { color: var(--muted); opacity: 0.4; cursor: not-allowed; }
.daterange-day.is-selected { background: var(--brand); color: #fff; }
/* Les nuits déjà prises, et les séjours qui les traversent. Marquées, pas
   refusées : le passé reste la seule chose que ce calendrier interdise, et
   un devis sur une semaine occupée se discute au moment de l'envoyer — voir
   handleQuoteCreate. L'ambre les distingue du gris du passé, qui dit « pas
   même la peine », et de la sélection, qui dit oui. */
.daterange-day.is-booked,
.daterange-day.is-conflict { color: var(--pending, #b5540a); background: var(--pending-mist, #fbe9d7); }
.daterange-day.is-conflict.is-selected,
.daterange-day.is-booked.is-selected { background: var(--pending, #b5540a); color: #fff; }
.daterange-day.is-in-range { background: var(--brand-light); border-radius: 0; }

/* Confirm row — "Effacer" resets the in-progress pick, "Valider" commits it
   into the real inputs and closes the popover. Day clicks alone never do
   either (see daterange.js's header comment) — this is the fix for the
   "extra click silently changes the range" bug the mobile-first picker on
   the public site hit first. */
.daterange-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.8rem;
  margin-top: 0.8rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--border);
}
.daterange-clear {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  padding: 0.4rem 0.2rem;
  margin-top: 0;
}
.daterange-clear:hover { color: var(--text); }
.daterange-done {
  margin-top: 0;
  padding: 0.55rem 1.3rem;
  font-size: 0.85rem;
  border-radius: 999px;
}

/* Le calendrier sur un téléphone.

   Deux mois l'un sous l'autre, c'est deux fois la hauteur — et sur un écran
   de 667 px le popover dépassait par le bas : « Valider » se trouvait hors de
   l'écran, et le calendrier ne pouvait pas être refermé autrement qu'en
   touchant à côté, ce qui perd la sélection.

   Deux réponses, et aucune ne touche au grand écran. D'abord un plafond de
   hauteur là où la barre d'onglets existe (moins de 900 px) : le popover
   s'arrête au-dessus d'elle et défile en dedans, et sa rangée de boutons
   reste collée en bas de ce défilement. Ensuite, sous 640 px, un serrage de
   toutes les mesures — c'est lui qui fait que les deux mois tiennent
   réellement, le défilement n'étant plus que le filet de sécurité. */
@media (max-width: 899px) {
  .daterange-popover {
    max-height: calc(100vh - 64px - 3rem - env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .daterange-actions {
    position: sticky;
    bottom: 0;
    background: var(--panel-bg, #fff);
    margin-bottom: 0;
  }
}

/* Sur un téléphone, un mois à la fois.

   Deux mois avec des cases qu'un doigt peut viser font 600 px de haut : ils
   ne tiennent pas sur un écran de 667, et les serrer assez pour qu'ils
   tiennent donne des cases de 20 px de haut — plus petites que ce qu'on peut
   toucher sans se tromper de jour. Ce qui doit céder, c'est le nombre de
   mois, pas la taille des jours : les flèches sont déjà là et font déjà le
   travail, et une sélection qui enjambe deux mois se fait en deux gestes.

   Le second mois est caché, pas retiré : le rendu reste le même partout, et
   c'est la largeur de l'écran qui décide de ce qu'on en montre. Les cases y
   gagnent au passage — plus grandes ici que sur un grand écran, ce qui est le
   bon sens sur un doigt. */
@media (max-width: 640px) {
  .daterange-popover { padding: 0.9rem; }
  .daterange-months { flex-direction: column; gap: 0.75rem; }
  .daterange-month + .daterange-month { display: none; }
  .daterange-month-heading { font-size: 0.95rem; margin-bottom: 0.5rem; }
  .daterange-dow span { font-size: 0.75rem; }
  .daterange-day { font-size: 0.95rem; padding: 0.5rem 0; }
}

/* HTML mirror of internal/pdf's document layout (invoice_review.html,
   invoice_generate.html's preview panel) — a plain "paper" card so the
   on-screen review looks like the PDF it produces, not another app page. */
.pdf-preview {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1.5rem;
  max-width: 700px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.pdf-preview-header { display: flex; justify-content: space-between; margin-bottom: 1rem; }
.pdf-preview-seller-name { font-size: 1.3rem; font-weight: 700; color: var(--brand); }
.pdf-preview-muted { color: var(--muted); font-size: 0.82rem; }
.pdf-preview-title { font-size: 1.1rem; font-weight: 700; margin: 0.5rem 0 1rem; }
.pdf-preview-parties { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-bottom: 1rem; }
.pdf-preview-party, .pdf-preview-property { flex: 1 1 220px; }
.pdf-preview-party-label { font-weight: 600; font-size: 0.78rem; text-transform: uppercase; color: var(--muted); margin-bottom: 0.2rem; }
/* Photo beside the property text, not stacked above it: stacked, it left the
   opposite party block facing a column of white space the height of the
   image. */
.pdf-preview-property.has-photo { display: flex; align-items: flex-start; gap: 0.7rem; }
.pdf-preview-property-photo {
  flex: none;
  width: 96px;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 6px;
  background: var(--off-bg);
}
@media (max-width: 480px) {
  .pdf-preview-property-photo { width: 72px; }
}
.pdf-preview-table { margin: 0.5rem 0; }
.pdf-preview-table th { background: none; border: none; border-bottom: 2px solid var(--text); color: var(--text); font-size: 0.8rem; }
.pdf-preview-table td { border: none; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
.pdf-preview-table tr:nth-child(even) td { background: #f6f7f8; }
.pdf-preview-row-muted td { color: var(--muted); }
.pdf-preview-vat { margin-top: 1rem; max-width: 320px; margin-left: auto; }
.pdf-preview-vat-row { display: flex; justify-content: space-between; padding: 0.25rem 0; font-size: 0.9rem; }
.pdf-preview-vat-note { text-align: right; font-size: 0.72rem; margin: 0.1rem 0; }
.pdf-preview-vat-total { font-weight: 700; font-size: 1.05rem; border-top: 1px solid var(--border); margin-top: 0.3rem; padding-top: 0.5rem; }
.pdf-preview-footer { margin-top: 1.2rem; font-size: 0.72rem; color: var(--muted); }

/* Per-conversation inbox (/communication) — each thread a collapsible
   panel using the existing chat-bubble markup (see thread_partial.html). */
/* Les cinq déclarations qui étaient ici sont toutes reposées par la version
   basse — y compris margin-bottom, que son `margin: 0` raccourci annule sans
   qu'un test de doublon nom-à-nom puisse le voir. Bloc entièrement mort. */
.comm-thread summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0;
}
/* font-weight est reposé par la version basse ; le reste non. */
.comm-thread-label { color: var(--text); flex: 1 1 auto; }
.comm-thread .messages { margin-top: 0.8rem; }

/* Tag cloud (/tags) — size scales with usage count (.tag-cloud-size-1..5,
   see tagListRow.SizeClass in tags_list.go). */
.tag-cloud {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem 1rem;
}
.tag-cloud-item {
  color: var(--brand);
  font-weight: 600;
  white-space: nowrap;
}
.tag-cloud-count {
  color: var(--muted);
  font-weight: 400;
  font-size: 0.7em;
  margin-left: 0.25rem;
}
.tag-cloud-size-1 { font-size: 0.85rem; opacity: 0.75; }
.tag-cloud-size-2 { font-size: 1.05rem; opacity: 0.85; }
.tag-cloud-size-3 { font-size: 1.3rem; }
.tag-cloud-size-4 { font-size: 1.65rem; }
.tag-cloud-size-5 { font-size: 2.1rem; }

/* Drag-and-drop upload zone (dropzone.js) — desktop drag target wrapping a
   real file input, which stays the actual upload mechanism (no backend
   change). Mobile relies on the input's own capture=environment attribute
   for a native camera/gallery picker instead of any of this. */
.dropzone {
  border: 2px dashed var(--border);
  border-radius: 10px;
  padding: 1.5rem;
  text-align: center;
  transition: border-color .15s ease, background .15s ease;
  position: relative;
}
.dropzone-active { border-color: var(--brand); background: var(--brand-light); }
.dropzone-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  color: var(--muted);
}
.dropzone-label i { font-size: 1.8rem; color: var(--brand); }
.dropzone-text { font-size: 0.85rem; }
.dropzone input[type="file"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
  width: 100%;
  height: 100%;
}

/* /media property picker — reuses .img-card/.img-grid but as a clickable
   property card instead of a photo card. */
.media-picker-card { color: inherit; text-decoration: none; display: block; transition: border-color .12s ease, transform .12s ease; }
.media-picker-card:hover { text-decoration: none; border-color: var(--brand); transform: translateY(-2px); }
.media-picker-thumb { display: block; width: 100%; height: 130px; background-size: cover; background-position: center; background-color: var(--off-bg); }

/* An input paired with an inline action button (SIRET lookup — see
   siret-lookup.js). Replaces a repeated inline style="display:flex" and
   keeps the button from stretching to the input's height in a grid cell. */
.input-action { display: flex; gap: 0.5rem; align-items: stretch; }
.input-action input { flex: 1 1 auto; min-width: 0; }
.input-action button { flex: 0 0 auto; white-space: nowrap; }

/* Inline occupancy bar (owners listing) — a number alone makes 12% and 82%
   look alike at a glance; the bar is what makes the column scannable. */
.meter {
  display: inline-block; vertical-align: middle;
  width: 64px; height: 6px; border-radius: 3px;
  background: var(--border, #e3e6e8); overflow: hidden;
}
.meter-fill { display: block; height: 100%; background: var(--brand, #2f6f5e); }
.meter-label { margin-left: 0.4rem; font-variant-numeric: tabular-nums; }

/* Integration status: an icon rather than a repeated word. */
.conn { font-size: 1rem; }
.conn-ok      { color: var(--ok, #2f8f5b); }
.conn-pending { color: var(--pending, #b8860b); }
.conn-off     { color: var(--muted, #8c959c); }
.conn-error   { color: var(--error, #c0392b); }

/* Monetary amounts (see internal/money and the "eur" template helper).
   nowrap keeps the € with its number and stops a thousands separator
   becoming a line break; tabular-nums gives every digit the same advance so
   a right-aligned column lines up on the decimal comma. */
.money { white-space: nowrap; font-variant-numeric: tabular-nums; }
td.amount, th.amount, .amount { text-align: right; }
td.amount .money, td.amount { font-variant-numeric: tabular-nums; }

/* Booktabs-style document table (invoice detail) — the typographic
   convention from LaTeX's booktabs: horizontal rules only, never vertical
   ones, a heavier rule top and bottom, and generous row padding. Vertical
   rules add ink without adding information; the eye already groups columns
   by alignment. */
table.booktabs { width: 100%; border-collapse: collapse; }
/* The global fills — the zebra stripe on tr:nth-child(even) and the tinted
   th band — are the opposite of what this table is. Booktabs separates rows
   with rules and whitespace and never with ink, which is also what the PDF
   prints; a striped, banded version reads as a data grid pasted into a
   document rather than as the document itself. */
table.booktabs th,
table.booktabs tr:nth-child(even) td { background: transparent; }
table.booktabs th,
table.booktabs td { padding: 0.55rem 0.75rem; border: 0; vertical-align: top; }
table.booktabs thead th {
  border-top: 1.5px solid var(--text, #11181c);
  border-bottom: 0.75px solid var(--text, #11181c);
  font-weight: 600; font-size: 0.82rem; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--muted, #5f6970);
}
table.booktabs tbody tr + tr td { border-top: 1px solid var(--border, #eceff1); }
table.booktabs tfoot .booktabs-rule-above td { border-top: 0.75px solid var(--text, #11181c); }
table.booktabs tfoot td { padding-top: 0.4rem; padding-bottom: 0.4rem; }
table.booktabs tfoot .booktabs-total td {
  border-top: 1px solid var(--border, #eceff1);
  border-bottom: 1.5px solid var(--text, #11181c);
  font-weight: 700; font-size: 1.05rem;
}
.booktabs-label { display: block; }
.booktabs-detail { display: block; font-size: 0.85rem; color: var(--muted, #5f6970); }
.nowrap { white-space: nowrap; }

/* --- Booktabs on a phone: the receipt line --------------------------------
   A four- or five-column document table cannot fit 390px. At that width the
   last column — the one figure that matters — fell off the right edge and
   dragged the whole page into horizontal scroll, and IBM Plex Mono on every
   .amount cell (see td.amount above) made each column wider than it had any
   need to be.

   Of the three standard responsive-table patterns — horizontal overflow,
   priority columns, card view — only the card view escapes the scroll trap
   without hiding data. But the generic "LABEL: value" card that .table-cards
   renders (see internal/webui/static/design.css) turns an invoice into a
   form, losing exactly the document feel this page exists to convey.

   So: a card view specialised for a document. A line item becomes two lines,
   the way a paper receipt prints it — the designation on its own line, then
   its derivation ("7 nuit(s) × 127,65 €") in small muted type on the left
   with the line total, bold and tabular, hard against the right edge. The
   arithmetic reads as arithmetic instead of as three unlabelled columns, so
   the hidden <thead> costs nothing. Nothing is dropped and nothing scrolls.

   Markup contract — every <td> in tbody declares its role with data-cell:
     title  the designation; own line, full width, reads as the card heading
     meta   a supporting figure; joins the derivation line, separated from
            the preceding meta cell by a middle dot — or by data-sep when
            another operator says it better ("×" between quantity and unit
            price, "· TVA" before a rate)
     total  the row's amount; pushed to the right edge and emphasised
   A cell with no data-cell stays in normal flow. The <tfoot> needs no
   attributes: every totals row is (label, amount) by construction.

   Breakpoint matches .table-cards so a page mixing the two flips at once. */
@media (max-width: 640px) {
  /* SCOPED TO :has(td[data-cell]) — every rule below assumes the markup
     contract documented above, and a .booktabs table that does not carry it
     is not merely unstyled but unreadable: the cells become flex items with
     no roles and run together into one line. Making the transformation
     opt-in means a table that has not been converted keeps its ordinary
     rendering instead of silently breaking, which is the failure mode a
     shared component must not have.

     This is defensive, not a fix for anything observed. /taxe-de-sejour was
     blamed on it and was not: that page was blank on a phone because its
     section still carried .table-as-list, which hides the table outright.
     No .booktabs table without data-cell exists today. */
  table.booktabs:has(td[data-cell]),
  table.booktabs:has(td[data-cell]) tbody,
  table.booktabs:has(td[data-cell]) tfoot,
  table.booktabs:has(td[data-cell]) tr,
  table.booktabs:has(td[data-cell]) td { display: block; }
  table.booktabs:has(td[data-cell]) thead { display: none; }
  /* thead carried the heavy opening rule; with it hidden the rule moves to
     the table, so the document still opens on a firm line. */
  table.booktabs:has(td[data-cell]) { border-top: 1.5px solid var(--text, #11181c); }
  table.booktabs:has(td[data-cell]) td { padding: 0; }

  table.booktabs:has(td[data-cell]) tbody tr {
    display: flex;
    /* wrap so a long derivation pushes the total onto its own line rather
       than squeezing it — margin-left:auto keeps it right-aligned there. */
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.4rem;
    padding: 0.65rem 0;
  }
  /* The row divider moves from the cells to the row: as flex items the cells
     no longer sit in a shared line box, so per-cell top borders would draw
     one rule per cell instead of one per row. */
  table.booktabs:has(td[data-cell]) tbody tr + tr { border-top: 1px solid var(--border, #eceff1); }
  table.booktabs:has(td[data-cell]) tbody tr + tr td { border-top: 0; }

  table.booktabs:has(td[data-cell]) td[data-cell="title"] { flex: 1 0 100%; margin-bottom: 0.25rem; }

  table.booktabs:has(td[data-cell]) td[data-cell="meta"] {
    flex: 0 1 auto;
    /* Mono buys vertical alignment in a column of figures; inline in a
       sentence it buys nothing and costs width. Back to the body font. */
    font-family: var(--db-font-body, inherit);
    font-size: 0.85rem;
    color: var(--muted, #5f6970);
    text-align: left;
    white-space: nowrap;
  }
  table.booktabs:has(td[data-cell]) td[data-cell="meta"] + td[data-cell="meta"]::before {
    content: "·";
    margin-right: 0.4rem;
  }
  /* Repeats the sibling combinator on purpose: the rule above carries two
     type selectors, so a plain td[data-cell][data-sep] loses the cascade to
     it and the middle dot would override every custom operator. */
  table.booktabs:has(td[data-cell]) td[data-cell="meta"] + td[data-cell="meta"][data-sep]::before,
  table.booktabs:has(td[data-cell]) td[data-cell="meta"][data-sep]::before { content: attr(data-sep); }

  table.booktabs:has(td[data-cell]) td[data-cell="total"] {
    flex: 0 0 auto;
    margin-left: auto;
    text-align: right;
    font-weight: 600;
  }

  /* A group heading ("Options") owns its line. */
  table.booktabs:has(td[data-cell]) tr.booktabs-section { padding-top: 1rem; }
  table.booktabs:has(td[data-cell]) tr.booktabs-section td { flex: 1 0 100%; padding-top: 0; }

  /* Totals: label left, figure right — the only two things a totals row has,
     so no data-cell is needed to tell them apart. */
  table.booktabs:has(td[data-cell]) tfoot tr {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0 1rem;
    padding: 0.28rem 0;
  }
  table.booktabs:has(td[data-cell]) tfoot td { padding: 0; }
  table.booktabs:has(td[data-cell]) tfoot td:first-child { text-align: left; }
  /* Same reason as tbody: the rules belong to the row now, not the cells. */
  table.booktabs:has(td[data-cell]) tfoot tr.booktabs-rule-above {
    border-top: 0.75px solid var(--text, #11181c);
    margin-top: 0.3rem;
    padding-top: 0.55rem;
  }
  table.booktabs:has(td[data-cell]) tfoot tr.booktabs-total {
    border-top: 1px solid var(--border, #eceff1);
    border-bottom: 1.5px solid var(--text, #11181c);
  }
  table.booktabs:has(td[data-cell]) tfoot tr.booktabs-rule-above td,
  table.booktabs:has(td[data-cell]) tfoot tr.booktabs-total td { border: 0; }
  /* A one-cell note has nothing to sit opposite, and right-aligned prose
     that wraps reads as ragged — give it the width and let it read left. */
  table.booktabs:has(td[data-cell]) tfoot .doc-vat-note {
    flex: 1 0 100%;
    text-align: left;
    /* It sits in an .amount cell purely by column position, and inherits
       nowrap + Plex Mono from there — neither of which suits a sentence. */
    white-space: normal;
    font-family: var(--db-font-body, inherit);
  }
}

/* Document header: title block on the left, actions on the right. */
/* Title left, actions right, on one line when there is room and on two when
   there is not — with the actions pinned to the right edge either way. The
   space-between alone was not enough: once the header wrapped, the action
   block became the only item on its line and drifted into the middle of the
   page, level with nothing and belonging to nothing. */
.doc-head { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; align-items: flex-start; justify-content: space-between; }
.doc-head > :first-child { flex: 1 1 22rem; min-width: 0; }
/* EN LIGNE, PAS EN COLONNE.
   Empilés, deux boutons de largeurs différentes s'alignaient à droite et sur
   rien d'autre : deux bords gauches en escalier au-dessus d'un document dont
   tout le reste est aligné. Sur une ligne ils forment une rangée, et la
   hauteur du bandeau cesse de dépendre du nombre d'actions de la page.
   flex-wrap parce que catering_order et invoice_detail en ont trois. */
.doc-actions {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
}

/* The status belongs beside the title, not buried in a run of metadata after
   a middot: it is the first thing anyone wants from a document, and reading
   it should not require parsing a sentence. */
.doc-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.doc-title .badge { font-size: 0.78rem; font-weight: 600; }

/* The button carried .btn's 0.9rem top margin into a flex row aligned to the
   top of the title, so it floated: level with nothing, aligned to nothing.
   The row owns its own spacing now. */
.doc-actions .btn, .doc-actions button { margin-top: 0; }

/* Why the control cannot act, next to the control — visible, not a tooltip.
   Capped so it wraps as a short paragraph under the button instead of
   stretching the header. */
/* A rarely-wanted control, folded away under the service it belongs to. Not
   hidden — a one-shot repair someone will look for exactly once has to be
   findable — but not competing with the panel's actual subject either. */
.panel-details { margin-top: 1.25rem; border-top: 1px solid var(--border); padding-top: 0.75rem; }
.panel-details > summary { cursor: pointer; font-size: 0.88rem; color: var(--muted); }
.panel-details > summary:hover { color: var(--brand); }

.doc-actions-note {
  margin: 0;
  max-width: 22rem;
  text-align: right;
  font-size: 0.82rem;
  color: var(--muted);
}

/* Owner detail: the metadata strip under the title — the same figures as the
   listing row that links here, so the page opens by answering "who is this
   and is anything wrong" before any table. */
.doc-meta {
  display: flex; flex-wrap: wrap; gap: 1.75rem;
  padding: 0.9rem 0 0.25rem;
}
.doc-meta-item { display: flex; flex-direction: column; gap: 0.3rem; }
.doc-meta-label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted, #5f6970); }
.doc-meta-value { font-weight: 600; }
/* A second line inside one item — a date range under its period name — kept
   small so the strip stays a row of answers rather than a paragraph. */
.doc-meta-sub { font-size: 0.78rem; color: var(--muted, #5f6970); }
.notify-form { margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }

/* Booking list — applies the enterprise data-table guidance: row dividers
   rather than zebra striping (stripes collide with hover/selected states
   once a row is interactive), a sticky header so column meaning survives
   scrolling, dates left-aligned as identifiers, amounts right-aligned and
   tabular. */
/* .table-sticky ne s'écrit jamais seul : les trois tableaux qui le portent
   portent aussi .table-modern, dont design.css fixe le fond et le z-index de
   l'en-tête et qui gagne à égalité de spécificité. Le fond et le z-index: 2
   qui figuraient ici n'atteignaient donc jamais l'écran ; les garder aurait
   changé le rendu le jour où cette feuille passe après design.css. Ce qui
   reste est ce que design.css ne dit pas : coller l'en-tête, et le souligner
   pour qu'il se détache du contenu qui défile dessous. */
.table-sticky thead th {
  position: sticky; top: 0;
  box-shadow: inset 0 -1px 0 var(--border, #e3e6e8);
}
.table-sticky tbody tr:nth-child(even) { background: transparent; }
.table-sticky tbody tr + tr td { border-top: 1px solid var(--border, #eceff1); }
.table-sticky tbody tr:hover { background: var(--hover-bg, #f5f7f8); }
/* A past booking is context, not noise: dimmed a little, never greyed out
   to the point of being unreadable. */
.table-sticky tbody tr.row-past td { color: var(--muted, #5f6970); }
.table-sticky tbody tr.row-past .cell-primary a { color: inherit; }
/* Même chose pour le pied : le filet vient de .table-modern. */
.table-sticky tfoot td {
  padding-top: 0.6rem; font-variant-numeric: tabular-nums;
}

/* Toolbar above a table: filter segments on the left, search on the right. */
.table-toolbar {
  display: flex; flex-wrap: wrap; gap: 1rem;
  align-items: center; justify-content: space-between; margin-bottom: 1rem;
}
.seg { display: inline-flex; border: 1px solid var(--border, #e3e6e8); border-radius: 999px; overflow: hidden; }
.seg-item {
  padding: 0.4rem 0.9rem; font-size: 0.9rem; text-decoration: none;
  color: var(--muted, #5f6970); background: transparent;
}
.seg-item + .seg-item { border-left: 1px solid var(--border, #e3e6e8); }
.seg-item.is-active { background: var(--brand, #2f6f5e); color: #fff; font-weight: 600; }
.seg-count { opacity: 0.7; margin-left: 0.35rem; font-variant-numeric: tabular-nums; }
.table-search { display: flex; gap: 0.4rem; align-items: center; }
.table-search input[type="search"] { min-width: 14rem; }

/* Sous un tableau paginé : où l'on en est, et comment aller ailleurs. Des
   pages plutôt qu'un défilement sans fin — sur un écran de contrôle, savoir
   combien il reste fait partie de la décision, et revenir à la page 7 vaut
   mieux qu'un défilement fluide. */
.table-pager {
  display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem;
  align-items: center; justify-content: space-between;
  margin-top: 1rem; font-size: 0.9rem;
}
.table-pager-count { color: var(--muted, #5f6970); font-variant-numeric: tabular-nums; }
.table-pager-pages { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.15rem; }
.table-pager-pages > * {
  min-width: 2rem; padding: 0.3rem 0.5rem; border-radius: 6px;
  text-align: center; text-decoration: none; font-variant-numeric: tabular-nums;
}
.table-pager-pages a { color: var(--brand, #2f6f5e); }
.table-pager-pages a:hover { background: var(--hover-bg, #f5f7f8); }
.table-pager-pages .is-current { background: var(--brand, #2f6f5e); color: #fff; font-weight: 600; }
/* Un contrôle indisponible reste visible, à sa place, en gris : le retirer
   ferait bouger les autres d'une page à l'autre. */
.table-pager-pages .is-off { color: var(--muted-light, #a5adb3); }
.table-pager-pages .is-gap { color: var(--muted, #5f6970); min-width: 1rem; padding: 0.3rem 0.2rem; }

/* Ce qui restreint la vue se lit au-dessus du tableau, avec de quoi en sortir. */
.table-filter-note { margin: -0.25rem 0 0.75rem; }

/* Notes internes — le formulaire, et l'historique de ce qui a été enregistré.
   Une note se relit plus souvent qu'elle ne s'écrit : le textarea prend la
   largeur du panneau et respire, plutôt que d'imiter un champ de saisie
   d'une ligne. */
.note-form textarea {
  width: 100%; min-height: 7rem; resize: vertical;
  font: inherit; line-height: 1.5;
}
.note-form .field { display: block; margin-bottom: 0.35rem; }

/* En lecture — l'état par défaut, parce qu'une note se relit bien plus
   souvent qu'elle ne s'écrit. Le texte est du texte : ses retours à la ligne
   comptent, et il ne prétend pas être un champ de saisie. */
.note-read .note-body {
  margin: 0 0 0.5rem; white-space: pre-wrap; line-height: 1.55;
  font-size: 1.02rem;
}
.note-read .empty-state { margin: 0 0 0.5rem; }

.note-history { display: flex; flex-direction: column; gap: 1.25rem; }
.note-version { padding-left: 0.9rem; border-left: 2px solid var(--border, #dde3e8); }
/* La version courante se distingue, mais discrètement : les autres ne sont
   pas des erreurs, ce sont ce qui a été écrit avant. */
.note-version.is-current { border-left-color: var(--brand, #1f4e5f); }
.note-version-head {
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline;
  font-size: 0.9rem; margin-bottom: 0.3rem;
}
.note-version-when { font-variant-numeric: tabular-nums; font-weight: 600; }
/* Une note est du texte tapé à la main : ses retours à la ligne comptent. */
.note-version-body { margin: 0; white-space: pre-wrap; }

/* Une carte de synthèse qui porte une mauvaise nouvelle : elle se distingue
   des autres sans crier, et ne devient rouge que quand le nombre n'est pas
   zéro — un compteur de conflits à zéro est une bonne nouvelle, pas une
   alerte. */
.doc-meta-alert { border-left: 3px solid var(--error, #b3261e); padding-left: 0.75rem; }
.doc-meta-alert .doc-meta-value { color: var(--error, #b3261e); }

/* La ligne d'élision d'un échantillon : ce n'est pas une donnée, elle ne doit
   pas se lire comme une ligne du fichier. */
.row-elision td {
  text-align: center; font-style: italic;
  color: var(--muted, #5f6970); background: var(--hover-bg, #f5f7f8);
}

/* Screen-reader-only label — a search input and an actions column still
   need names even when the design shows only an icon. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* A row action must never wrap: a two-line button in a one-line row reads
   as broken layout, and the row itself is already clickable, so the label
   only has to confirm where the click goes. */
.cell-actions { white-space: nowrap; }
.cell-actions .btn-small { white-space: nowrap; }
/* Guest names may wrap, but not mid-column-collapse — give the primary
   column room before the browser starts breaking two-word names. */
.table-sticky .cell-primary { min-width: 9rem; }

/* Quote sheet — an HTML rendering of the document itself, not a form about
   it: parties side by side, then the same booktabs table the PDF prints. */
/* Pas de largeur maximale ici.
   Elle ne s'appliquait qu'à cette feuille : sur un écran large le document
   s'arrêtait soixante points avant les cartes qui le suivent — 960 contre
   1020 — et la page se lisait comme si la feuille avait rétréci. Une limite
   de largeur est une décision de page, pas de panneau ; tant qu'il n'y a pas
   de conteneur pour la porter, mieux vaut que tout ait la même largeur que
   d'en avoir un seul qui diffère. */
.doc-parties {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 1.5rem; margin-bottom: 1.75rem;
}
.doc-party-label {
  margin: 0 0 0.35rem; font-size: 0.78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted, #5f6970);
}
.doc-party-name { margin: 0 0 0.15rem; font-weight: 600; }
.doc-parties p { margin: 0 0 0.1rem; }
/* A section heading inside the table, for the options group. */
table.booktabs tr.booktabs-section td {
  padding-top: 1rem; font-size: 0.78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted, #5f6970);
  border-top: 1px solid var(--border, #eceff1);
}
/* An option the customer did not take is shown, not hidden: it is an upsell
   record. Muted so it never reads as part of the total. */
table.booktabs tr.row-unselected td { color: var(--muted, #8c959c); }
/* On mobile the total cell is emphasised (see the receipt-line block above);
   an option that was never taken must not be — a bold figure on a declined
   line reads as money owed. */
table.booktabs tr.row-unselected td[data-cell="total"] { font-weight: 500; }
.doc-vat-note { font-size: 0.8rem; padding-top: 0 !important; }
.doc-footnote { margin-top: 0.75rem; font-size: 0.85rem; }

/* Payment tracker — replaces up to three stacked status pills per row.
   Three competing coloured pills is badge overload: nothing stands out and
   the row heights go ragged. This shows the whole journey (acompte → solde
   → caution) as one compact control, with the state that needs attention
   named once beside it.

   Each step carries an icon as well as a colour: colour alone fails in
   greyscale and for colour-blind readers. */
.paysteps { display: inline-flex; gap: 0.25rem; vertical-align: middle; }
.paystep {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.15rem 0.45rem; border-radius: 999px;
  font-size: 0.75rem; line-height: 1.4; white-space: nowrap;
  border: 1px solid transparent;
}
.paystep-label { font-weight: 500; }
.paystep-paid     { background: var(--ok-bg, #e6f4ec); color: var(--ok, #1e6f47); }
.paystep-pending  { background: var(--pending-bg, #fdf3e0); color: var(--pending, #8a6100); }
.paystep-failed   { background: var(--error-bg, #fdeceb); color: var(--error, #b42318); }
.paystep-released { background: var(--panel-alt, #f0f2f3); color: var(--muted, #5f6970); }
.paysteps-note {
  display: block; margin-top: 0.3rem;
  font-size: 0.78rem; color: var(--muted, #5f6970);
}
/* Only the state that requires action gets emphasis — a row where
   everything is settled should read as quiet. */
.paysteps-note.needs-action { color: var(--pending, #8a6100); font-weight: 600; }

/* Below a wide viewport the three step labels cost more width than they
   earn, and the row starts overflowing. The steps collapse to icons; the
   meaning is not lost, because the note underneath names the stage in words
   ("Acompte en attente" / "Soldé") — the icons are the quick visual, the
   note is the accessible carrier. */
@media (max-width: 1500px) {
  .paystep-label { display: none; }
  .paystep { padding: 0.2rem 0.35rem; }
}

/* A property name is the row's anchor: give it room rather than letting it
   break into three lines and drag the row height with it. */
.table-sticky .cell-primary a { text-wrap: balance; }
.table-sticky td.cell-primary { min-width: 12rem; }

/* Thumbnail inline with the property name, rather than owning a column with
   an empty header. */
.cell-with-thumb { display: inline-flex; align-items: center; gap: 0.5rem; }
.icon-link { color: var(--muted, #5f6970); padding: 0 0.25rem; }
.icon-link:hover { color: var(--brand, #2f6f5e); }

/* Booking cards (customer page).
   A table earns its chrome when you compare many rows; repeat customers are
   rare, so most customers have one or two bookings and a header row plus a
   single data row is mostly furniture. Cards suit a small set of
   self-contained records where a photo carries meaning — and the extra room
   pays for itself: each payment stage can now show its AMOUNT, which the
   table had no space for. */
.booking-cards { display: grid; gap: 1rem; }
.booking-card {
  display: grid; grid-template-columns: 8.5rem 1fr;
  background: var(--panel-bg, #fff);
  border: 1px solid var(--border, #e3e6e8);
  border-radius: var(--radius-lg, 16px);
  overflow: hidden;
  box-shadow: var(--db-shadow-sm, none);
  /* A card is a block at the same level as .panel, so it carries the same
     bottom margin — otherwise a standalone card (the booking page, where it
     is the header rather than one of a list) sits flush against the section
     below it. */
  margin-bottom: 1.5rem;
}
/* Inside the list the grid gap already does the spacing; keeping the margin
   as well would double it. */
.booking-cards > .booking-card { margin-bottom: 0; }
/* Pas de bande verticale sur une carte qui attend quelque chose. Elle disait
   « urgence » avec une couleur seule, et --pending (#b5540a) n'est qu'à 24°
   de teinte de --db-critical (#ac2f26) pour une clarté presque identique :
   sur quatre pixels, sans rien à côté pour la situer, elle se lisait rouge —
   donc erreur, pas attente.
   La carte le dit déjà mieux, et avec des mots : la pastille de statut
   (« Arrivée aujourd'hui · acompte en attente ») et la note en pied
   (.booking-card-note.needs-action) portent la même teinte sur du texte, où
   la forme et la phrase font le travail et la couleur ne fait qu'appuyer. */
.booking-card.is-past { opacity: 0.85; }
.booking-card-media {
  background-size: cover; background-position: center; min-height: 9rem; display: block;
}
.booking-card-body { padding: 1.1rem 1.25rem; display: grid; gap: 0.9rem; }
.booking-card-head {
  display: flex; flex-wrap: wrap; gap: 0.75rem;
  align-items: flex-start; justify-content: space-between;
}
.booking-card-title { margin: 0 0 0.2rem; font-size: 1.05rem; }
.booking-card-dates { margin: 0; color: var(--muted, #5f6970); font-size: 0.92rem; }
.booking-card-dates i { margin-right: 0.35rem; }
.booking-card-badges { display: flex; gap: 0.35rem; flex-wrap: wrap; }

/* The figures strip: the stay amount, then one cell per payment stage. A
   definition list because that is what it is — a label and its value. */
.booking-card-figures {
  display: grid; gap: 0.75rem; margin: 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr));
  padding: 0.75rem 0; border-top: 1px solid var(--border, #eceff1);
  border-bottom: 1px solid var(--border, #eceff1);
}
.booking-card-figures > div { display: grid; gap: 0.2rem; }
.booking-card-figures dt {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--muted, #5f6970);
}
.booking-card-figures dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.fig-state { display: block; font-size: 0.78rem; font-weight: 500; }
.fig-paid     .fig-state { color: var(--ok, #1e6f47); }
.fig-pending  .fig-state { color: var(--pending, #8a6100); }
.fig-failed   .fig-state { color: var(--error, #b42318); }
.fig-released .fig-state { color: var(--muted, #5f6970); }

.booking-card-foot {
  display: flex; flex-wrap: wrap; gap: 0.75rem;
  align-items: center; justify-content: space-between;
}
.booking-card-note { font-size: 0.9rem; color: var(--muted, #5f6970); }
.booking-card-note.needs-action { color: var(--pending, #8a6100); font-weight: 600; }
.booking-card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.count-chip {
  font-size: 0.8rem; font-weight: 600; padding: 0.1rem 0.5rem;
  border-radius: 999px; background: var(--panel-alt, #eef1f2); color: var(--muted, #5f6970);
  vertical-align: middle;
}

/* On a phone the photo becomes a banner rather than a narrow strip. */
@media (max-width: 640px) {
  .booking-card { grid-template-columns: 1fr; }
  .booking-card-media { min-height: 7rem; }
}

/* Tab panels. The tab strip itself is design.css's shared .tabs component
   (it used to be a .tablist of .seg-items here, which made a tab set and a
   table's filter bar indistinguishable); only the panels stay app-side.
   They are plain sections until tabs.js runs: with JavaScript off the page
   reads as one continuous document, which is the right fallback for a
   record — nothing is hidden that cannot be shown. */
.tabs { margin-top: 1.25rem; }
.tabpanel { display: block; }
.tabpanel[hidden] { display: none; }

/* Label/value pairs, e.g. the coordonnées panel. */
.detail-list { display: grid; gap: 0.85rem; margin: 0 0 1rem; }
.detail-list > div { display: grid; gap: 0.15rem; }
.detail-list dt {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--muted, #5f6970);
}
.detail-list dd { margin: 0; font-weight: 500; }

/* Read-only state inside a settings form. A settings page mixes two kinds of
   content — things you can change, and things that merely report. Styling
   them alike is what makes a form read as documentation: the eye cannot find
   the controls. This block is visibly not a form: inset, no input styling,
   its own heading. */
.status-block {
  background: var(--panel-alt, #f4f6f7);
  border: 1px solid var(--border, #e3e6e8);
  border-radius: var(--radius-md, 12px);
  padding: 1rem 1.15rem;
  margin-bottom: 1.5rem;
}
.status-block-head {
  display: flex; flex-wrap: wrap; gap: 0.6rem;
  align-items: center; justify-content: space-between; margin-bottom: 0.9rem;
}
.status-block-title {
  font-size: 0.78rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--muted, #5f6970);
}
.status-block-figures {
  display: grid; gap: 0.85rem; margin: 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
}
.status-block-figures > div { display: grid; gap: 0.15rem; }
.status-block-figures dt { font-size: 0.8rem; color: var(--muted, #5f6970); }
.status-block-figures dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.status-block-note {
  margin: 0.9rem 0 0; font-size: 0.85rem; color: var(--muted, #5f6970);
}
.status-block-note i { margin-right: 0.3rem; }

/* A boolean setting, laid out as a control rather than a stray checkbox: the
   box and its label on one line, the explanation indented beneath so the
   sentence never competes with the thing you click. */
.switch-field {
  display: grid; grid-template-columns: auto 1fr; gap: 0.65rem;
  align-items: start; padding: 0.85rem 0;
}
.switch-field input[type="checkbox"] { margin-top: 0.25rem; width: 1.05rem; height: 1.05rem; }
.switch-field-text { display: grid; gap: 0.25rem; }
.switch-field-label { font-weight: 600; }
.switch-field-help { font-size: 0.85rem; color: var(--muted, #5f6970); }

/* Sub-groups inside a settings tab. A tab that mixes two unrelated concerns
   (when to ask for money / whether a quote goes out unattended) reads as a
   pile of controls; a heading per group tells the reader which help text
   belongs to which setting. */
.settings-group + .settings-group {
  margin-top: 1.5rem; padding-top: 1.25rem;
  border-top: 1px solid var(--border, #eceff1);
}
.settings-group-title {
  margin: 0 0 0.75rem; font-size: 0.78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted, #5f6970);
}

/* Per-field help, attached under its own input rather than floating between
   controls where it could belong to either. */
.field-help {
  display: block; margin-top: 0.3rem;
  font-size: 0.82rem; font-weight: 400; color: var(--muted, #5f6970);
}

/* A unit belongs beside the number, not inside the label: keeping labels to
   one line is what makes inputs in a grid line up, since a label that wraps
   pushes its own field down out of alignment with its neighbour. */
/* Ce bloc posait un .input-suffix en flex ; la refonte plus bas le repasse en
   position: relative / display: block, ce qui rend align-items et gap sans
   effet, et repose largeur, taille et couleur. Ne survit que ce que la
   version basse ne dit pas : ne pas couper l'unité en deux lignes. */
.input-suffix-unit { white-space: nowrap; }

/* Fields in a grid align on their tops so a longer help text below one of
   them cannot drag its neighbour's input out of line. */
.field-grid { align-items: start; }
/* Settings fields carry help text under the input, so they need more room
   than the 200px default before the sentence starts wrapping every few
   words. */
.settings-group .field-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); }

/* Un intertitre dans une grille de champs : il prend toute la largeur, sans
   quoi il se glisserait dans une colonne comme s'il était un champ de plus. */
.field-grid .pl-group {
  grid-column: 1 / -1; margin: 0.6rem 0 0;
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted);
}
.field-grid .pl-group:first-child { margin-top: 0; }

/* Les actions d'une commande traiteur : une rangée de boutons de même
   métrique, l'attendu en premier et en plein. Voir catering_status.go. */
.cat-actions { display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; margin-top: 12px; }
.cat-actions form { margin: 0; display: flex; }
.cat-actions .btn { margin: 0; }

/* Le corps d'un message, dans son bac à sable. Une hauteur généreuse pour ne
   pas lire par la fente d'une boîte aux lettres, et une bordure discrète
   pour que la frontière entre la page et le message se voie. */
.email-body {
  width: 100%; height: 32rem; border: 1px solid var(--border);
  border-radius: 8px; background: #fff;
}
.email-body-text {
  white-space: pre-wrap; word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9rem; background: var(--panel-bg); padding: 1rem;
  border-radius: 8px; border: 1px solid var(--border);
}

/* Une ligne de tableau qui mène quelque part.
   Le lien reste sur le texte — clavier, clic-milieu, « ouvrir dans un nouvel
   onglet » — et la ligne y ajoute une cible de sa largeur. Le curseur le dit,
   sans quoi rien ne distingue une ligne cliquable d'une autre. */
.row-link { cursor: pointer; }
.row-link:hover { background: var(--hover-bg, #f5f7f8); }

/* « Réglé à plusieurs », à côté de l'état du paiement. Discret : c'est une
   précision sur le mot d'à côté, pas un second état. */
.bk-split {
  display: inline-flex; align-items: center; gap: .25rem;
  margin-left: .4rem; padding: .05rem .4rem; border-radius: 999px;
  font-size: .78rem; font-weight: 600; white-space: nowrap;
  background: var(--db-surface-alt, #eef3f0); color: var(--db-ink-soft, #5a6b62);
}

/* ---------------------------------------------------------------- 02-options.css */
/* ============================================================================
   /settings/options — a catalogue of options, each one previously rendered
   as a fully open editor (every field, both forms, and the same paragraph
   of archive-explainer text repeated once per row) all at once. With even
   a handful of options that reads as a wall of open forms, not a list you
   can scan. Below: a collapsed row per option (name/price/scope/usage at a
   glance, click to edit) and the create form moved from the bottom of the
   page to a collapsed disclosure at the top.
   ============================================================================ */
.option-new {
  border: 1px dashed var(--brand);
  border-radius: var(--radius-lg, 12px);
  background: var(--brand-light);
  margin-bottom: 1.5rem;
  overflow: hidden;
}
.option-new summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1.1rem;
  cursor: pointer;
  font-weight: 700;
  color: var(--brand);
  list-style: none;
}
.option-new summary::-webkit-details-marker { display: none; }
.option-new[open] summary { border-bottom: 1px dashed var(--brand); }
.option-new-body { padding: 1.1rem; background: var(--panel-bg); }

/* .option-row — liste de fiches repliables. Née pour le catalogue d'options,
   qui est passé au tableau (voir plus bas) ; elle reste le composant des
   périodes de taxe de séjour sur la fiche d'un gîte, où il n'y a rien à
   comparer d'une ligne à l'autre — on ouvre celle qu'on veut marquer versée. */
.option-list { display: flex; flex-direction: column; gap: 0.6rem; }
.option-row {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-bg);
  overflow: hidden;
}
.option-row summary {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  padding: 0.8rem 1rem;
  cursor: pointer;
  list-style: none;
}
.option-row summary::-webkit-details-marker { display: none; }
.option-row summary:hover { background: var(--brand-light); }
.option-row[open] summary { border-bottom: 1px solid var(--border); }
.option-row-name {
  flex: 1 1 12rem;
  min-width: 0;
  font-weight: 700;
  font-size: 0.95rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.option-row-price {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: 0.88rem;
  min-width: 9ch;
}
.option-row-scope,
.option-row-used {
  flex: none;
  font-size: 0.8rem;
  color: var(--muted, #5f6970);
  min-width: 8ch;
}
.option-row-chevron {
  flex: none;
  margin-left: auto;
  font-size: 0.8rem;
  color: var(--muted, #5f6970);
  transition: transform 0.15s ease;
}
.option-row[open] .option-row-chevron { transform: rotate(90deg); }
.option-row-body { padding: 1.1rem 1rem; }
@media (max-width: 640px) {
  .option-row-scope, .option-row-used { min-width: 0; }
}

/* ---- /settings/options, le catalogue ------------------------------------- */
/* Une liste de <details> est devenue un tableau : le reversement, le prix et
   la facturation se comparent d'une option à l'autre, ce qu'une pile de
   lignes repliées ne permet pas — chaque valeur y flottait à une abscisse
   différente selon la longueur du nom. */
.opt-table { table-layout: fixed; width: 100%; min-width: 34rem; }
.opt-table .c-name  { width: 46%; }
.opt-table .c-price { width: 14%; }
.opt-table .c-unit  { width: 18%; }
.opt-table .c-split { width: 22%; }
/* Le reversement décide de qui touche l'argent : sans pastille, mais pas sans
   signal — « 100 % SUBA » est l'exception et se marque, « Partagée » est la
   règle et se lit normalement. */
.opt-split-suba { font-weight: 700; }
.option-split-help {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  margin: 0.9rem 0 0;
  font-size: 0.82rem;
  line-height: 1.45;
}
.option-split-help i { margin-top: 0.15rem; color: var(--brand); }
.option-archive-form { margin-top: 0.9rem; }
@media (max-width: 640px) {
  .opt-table { table-layout: auto; min-width: 0; }
}

/* Device setup fields on /integrations#telephonie. These are three values to
   be copied onto a phone, never edited, so they stack full-width with their
   labels above: side by side they truncate to uselessness, and a truncated
   URL that someone copies anyway is a support call. */
.device-fields { display: grid; gap: 0.15rem; }
.device-fields .field-label {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--muted, #5f6970); margin-top: 0.4rem;
}
.device-fields input[type="text"] {
  width: 100%; max-width: 100%; margin: 0;
  font-family: var(--db-font-mono, monospace); font-size: 0.78rem;
}

/* Handset setup on /integrations#telephonie. The QR codes lead and the text
   fields follow, because scanning is the path that works and typing a
   32-character token onto a phone is the path that quietly fails. */
.qr-setup {
  display: flex; flex-wrap: wrap; gap: 1.25rem; align-items: flex-start;
  margin: 0.75rem 0 0.25rem;
}
.qr-card { margin: 0; text-align: center; max-width: 170px; }
.qr-card img {
  display: block; width: 150px; height: 150px;
  /* A white plate under the code: the page background is cream, and a
     scanner wants maximum contrast at the quiet zone. */
  background: #fff; padding: 6px; border-radius: 6px;
  border: 1px solid var(--border, #e3e6e8);
}
.qr-card figcaption {
  margin-top: 0.45rem; font-size: 0.78rem; line-height: 1.35;
  color: var(--muted, #5f6970);
}
.qr-setup .device-fields { flex: 1 1 20rem; min-width: 0; }

/* .link-btn — a <button> that has to look like a link, for the secondary or
   destructive half of a pair of actions: "Supprimer" beside a save, "Retirer
   de l'astreinte" beside "Régénérer". It is a button because it posts a
   form, not because it deserves a button's weight.
   The class was in use in three templates with no rule behind it, so it fell
   through to the brand-filled default and made every one of those the
   loudest control on its card — the exact inverse of the intent. */
.link-btn {
  margin: 0;
  padding: 0.3rem 0.2rem;
  background: none;
  border: 0;
  color: var(--muted, #5f6970);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.link-btn:hover { background: none; color: var(--error, #b3261e); }
.link-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Les options d'un gîte, en tableau.

   Elles étaient en grille de cartes, ce qui tenait tant qu'une ligne portait
   une case et un nom. Depuis que le prix s'y règle, chacune porte cinq choses,
   et à quatre cartes par rangée les noms se cassaient sur cinq lignes, les
   hauteurs ne s'accordaient plus et la note de droite sortait de l'écran.

   Le tableau est .table-modern .table-cards, celui du reste du bureau.
   design.css énonce la règle : le tableau encadré à zébrures sert aux grilles
   denses qu'on lit case par case, celui-ci aux listes qu'on lit ligne par
   ligne. C'est ce qu'est cette page. Il ne reste ici que ce qui lui est propre.
   ============================================================================ */

/* La colonne « Proposée » : la largeur de son titre, pas plus, pour que le nom
   de l'option commence tôt.

   Une largeur explicite et non le « width: 1% » qui sert d'habitude à faire
   rétrécir une colonne : mesurée, elle tombait à 9,7 px malgré nowrap — le
   pourcentage l'emportait sur la largeur minimale du contenu — et le titre,
   qui déborde sans être coupé, se peignait sous l'en-tête voisin, opaque parce
   que collant. La colonne existait, son titre était invisible. */
.opt-table .opt-col-offered { width: 7rem; white-space: nowrap; }

/* Les quatre colonnes courtes sont dimensionnées, la colonne des noms prend le
   reste. Laissées libres, elles se partageaient la largeur à parts égales : un
   grand vide après « par séjour » pendant que « Courses livrées avant
   l'arrivée » se cassait en deux lignes. C'est le nom qui a besoin de place,
   les trois autres tiennent en quelques caractères. */
.opt-table th:nth-child(3), .opt-table td:nth-child(3) { width: 11rem; }

/* « Facturée » alignée à droite : la colonne touche celle du prix du
   catalogue, et une unité collée au montant qu'elle qualifie se lit d'un
   trait — « par séjour  150,00 € ». C'est l'exception qui vaut la règle
   « le texte à gauche, les nombres à droite » : ce n'est pas de la prose,
   c'est le qualificatif d'un nombre. */
.opt-table .opt-billed, .opt-table th:nth-child(3) { text-align: right; }
/* Deux lignes plutôt qu'une colonne large : voir unitLabelLines. Serrées,
   parce que ce sont deux morceaux d'une même mention. */
.opt-table .opt-billed { line-height: 1.25; }
.opt-table th:nth-child(4), .opt-table td:nth-child(4) { width: 11rem; }
.opt-table th:nth-child(5), .opt-table td:nth-child(5) { width: 12rem; }

/* TOUTE LA CELLULE EST LA CIBLE.

   Une case native fait 13 px de côté, soit la moitié des 24 px que la WCAG
   demande d'une cible (2.5.8), et elle est seule au milieu d'une colonne de
   7 rem : on vise, et on rate. L'étiquette recouvre la cellule, ce qui donne
   une cible de la taille de la ligne sans une ligne de JavaScript — clavier,
   double-clic et sélection au glisser continuent de se comporter comme le
   navigateur l'a prévu.

   L'étiquette ne porte pas de texte : le nom accessible de la case reste celui
   que lui donne l'étiquette du nom de l'option, dans la cellule voisine. */
.opt-table td.opt-col-offered { position: relative; }
.opt-check {
  position: absolute; inset: 0; margin: 0;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 0.35rem; padding: 0.85rem 0.9rem;
  cursor: pointer; border-radius: 6px;
}
.opt-check:hover { background: var(--brand-light); }

/* La case elle-même : plus grande que les 13 px par défaut, et à la couleur de
   l'application plutôt qu'au bleu du système — accent-color colore l'état coché
   sans remplacer le contrôle natif, donc sans lui retirer ce qu'il sait faire
   (contraste forcé, lecteurs d'écran, état indéterminé). */
.opt-table td.opt-col-offered input[type="checkbox"] {
  width: 1.15rem; height: 1.15rem; min-width: 0; margin: 0;
  accent-color: var(--brand); cursor: pointer;
}
/* L'anneau de focus est posé sur la case et non sur l'étiquette : c'est la
   case qui reçoit le focus clavier. */
.opt-table td.opt-col-offered input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--brand); outline-offset: 2px;
}

/* TOUTE LA LIGNE SUR UNE SEULE LIGNE DE BASE.

   Les cellules sont alignées en haut par défaut, ce qui aligne le HAUT des
   boîtes et non le bas des lettres. Le prix du catalogue est du texte nu, le
   prix du gîte est un champ : mesuré, la bordure et la marge intérieure du
   champ posaient son texte 9,3 px plus bas que le montant d'à côté. Deux
   nombres à comparer, décalés l'un par rapport à l'autre — et c'est précisément
   la comparaison qui est l'objet de cette page.

   vertical-align: baseline aligne les cellules sur la ligne de base de leur
   PREMIÈRE ligne, ce qui vaut aussi pour un nom qui passe sur deux lignes.
   Toute la ligne se pose donc sur la même ligne de base, le champ compris.

   La case à cocher garde son alignement haut : ce n'est pas du texte, elle n'a
   pas de ligne de base à partager, et la caler sur celle des autres la
   décentrerait de sa propre ligne. */
.opt-table tbody td:not(.opt-col-offered) { vertical-align: baseline; }

/* Les deux colonnes de prix côte à côte : c'est la question de la page —
   lesquels de ce gîte s'écartent du catalogue — et l'écart doit se voir sans
   calcul. Chiffres tabulaires pour que les unités s'alignent en colonne. */
.opt-table .opt-catalogue { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Le champ de prix : assez large pour « catalogue » en filigrane et son €.
   Sans min-width: 0 il hériterait des 200px que « form input » pose. */
.opt-price-field { display: inline-block; width: 9.5rem; }
/* La condition de durée, sous le mode de facturation et dans sa cellule :
   « par séjour / à partir de 8 nuits » est une seule phrase. Deuxième ligne,
   plus petite, alignée à droite comme ce qu'elle prolonge. */
.opt-nights {
  display: flex; align-items: baseline; justify-content: flex-end;
  gap: 0.35rem; margin-top: 0.35rem; font-size: 0.8rem; white-space: nowrap;
}
.opt-nights-field { display: inline-block; width: 6.5rem; }
.opt-nights-field input {
  min-width: 0; width: 100%; text-align: right;
  padding: 0.15rem 3rem 0.15rem 0.4rem; font-size: 0.8rem;
}
.opt-nights-field .input-suffix-unit { font-size: 0.75rem; }
.opt-price-field input { min-width: 0; text-align: right; }
/* Replié en fiche, un champ de saisie n'a pas à s'étirer sur la carte. */
.table-cards td .opt-price-field input[type="number"] { max-width: none; }

/* « form label » pose display: block, ce qui envoyait l'étiquette à la ligne
   et donnait des hauteurs inégales. Le nom et son étiquette sont une seule
   phrase : ils tiennent sur une ligne. */
.opt-table .cell-primary label { display: inline; margin: 0; font-weight: inherit; cursor: pointer; }

/* « tous les gîtes » : une légende sous la case, et non une parenthèse au fil
   du nom.

   Elle dit pourquoi la case est cochée — l'option est au catalogue de tous les
   gîtes — donc elle qualifie la disponibilité et pas le titre. Au fil du nom
   elle allongeait une colonne sur deux et se lisait comme un morceau du nom ;
   sous la case, elle se saute à la lecture et la colonne des noms retrouve sa
   largeur. */
.opt-tag {
  display: inline-block; white-space: nowrap;
  font-size: 0.68rem; font-weight: 400; line-height: 1.2; color: var(--muted);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 0.05rem 0.4rem;
}

/* Ce que cette page sert à voir : les prix qui s'écartent du catalogue. Le
   fond seul ne suffirait pas — il ne survit ni à l'impression ni au
   daltonisme — d'où la barre à gauche, qui est une différence de forme. */
.opt-table tbody tr.is-overridden > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
.opt-table tbody tr.is-overridden .opt-price-field input { font-weight: 600; color: var(--brand); }

/* Une option que ce gîte ne propose pas : son prix ne s'applique à rien, et le
   champ est désactivé plutôt que d'attendre une saisie qui sera ignorée. La
   ligne s'efface un peu, sans disparaître — on doit pouvoir la recocher. */
.opt-table tbody tr.is-not-offered td.cell-primary,
.opt-table tbody tr.is-not-offered td.muted,
.opt-table tbody tr.is-not-offered td.opt-catalogue { opacity: 0.55; }

@media (max-width: 640px) {
  /* Repliée en fiche, la cellule n'est plus une case dans une colonne : c'est
     une ligne « PROPOSÉE …… ☑ » du tableau de bord de la carte. L'étiquette
     doit donc revenir dans le flux — absolue, elle se posait par-dessus les
     lignes suivantes et recouvrait leurs intitulés.
     Elle garde la cible large en occupant la place restante de la ligne. */
  /* Les largeurs de colonnes sont celles du tableau : en fiche il n'y a plus
     de colonnes, et une cellule de 8 rem y coupait « à l'unité » sous son
     intitulé. Elles sont plus spécifiques que le « width: 100% » de
     .table-cards, donc il faut les défaire ici. */
  .opt-table th:nth-child(3), .opt-table td:nth-child(3),
  .opt-table th:nth-child(4), .opt-table td:nth-child(4),
  .opt-table th:nth-child(5), .opt-table td:nth-child(5),
  .opt-table .opt-col-offered { width: auto; }
  /* Et l'unité de facturation se relit à gauche, comme le reste de la fiche :
     l'alignement à droite ne servait qu'à la coller au prix du catalogue,
     colonne qui n'existe plus ici. */
  .opt-table .opt-billed { text-align: left; }
  /* En fiche, la ligne se lit « PROPOSÉE …… ☑ tous les gîtes » : la légende
     revient à côté de la case, sur une seule ligne, poussée à droite comme
     les autres valeurs. */
  .opt-check {
    position: static; inset: auto;
    flex: 1; flex-direction: row; align-items: center; justify-content: flex-end;
    gap: 0.45rem; padding: 0.1rem 0;
  }
  /* Le champ tient sur la ligne de son intitulé, poussé à droite comme les
     autres valeurs de la fiche — c'est justify-content: space-between de
     .table-cards qui l'y met. En width: 100 % il réclamait toute la largeur,
     passait à la ligne suivante et s'y posait à gauche, seul de son espèce.
     margin-left: auto le garde à droite si un écran très étroit le fait
     malgré tout passer à la ligne. */
  .opt-price-field { width: 11rem; max-width: 100%; margin-left: auto; }
  /* En fiche, la ligne « FACTURÉE » porte les deux : le mode, puis la
     condition en dessous, toutes deux à droite. */
  .opt-billed { display: block; text-align: right; }
  .opt-nights { justify-content: flex-end; }
  .opt-nights-field { width: 7rem; }
}

/* ---------------------------------------------------------------- 03-desk-core.css */
/* ===========================================================================
 * Ci-dessus : ce qui venait d'app.css. Ci-dessous : desk.css d'origine.
 * La frontière n'a pas de sens thématique, seulement un sens de cascade —
 * ce qui suit gagne à spécificité égale.
 * ======================================================================== */

/* desk.css — components only the concierge dashboard renders: the Accueil
   dashboard widgets (.db-*), the cleaning calendar grid and agenda, the rail's
   collapsible navigation with its search field and count badges, the property
   and settings tab strips, the /guide and /team pages.

   Split out of the shared design.css (now internal/webui/static/design.css)
   so the owner, guest and staff portals stop downloading ~900 lines of CSS
   for pages they do not have. Loaded after design.css, so it can rely on the
   tokens defined there. */

/* ------------------------------------------------------------ db-hero */
.db-hero {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 2rem;
  margin-bottom: 2rem;
  font-family: var(--db-font-body);
}
.db-hero-eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--db-sand-deep);
  margin-bottom: 0.5rem;
}
.db-hero h1 {
  font-family: var(--db-font-display);
  font-size: 2.05rem;
  font-weight: 600;
  color: var(--db-ink);
  letter-spacing: -0.01em;
  margin: 0;
  text-wrap: balance;
}
.db-hero-meta { font-size: 0.88rem; color: var(--db-ink-soft); margin-top: 0.4rem; }
.db-hero-date { text-align: right; font-size: 0.82rem; color: var(--db-ink-faint); white-space: nowrap; }
.db-hero-date strong {
  display: block;
  font-family: var(--db-font-display);
  font-size: 1.05rem;
  color: var(--db-ink);
  font-weight: 600;
  text-transform: capitalize;
  margin-top: 0.15rem;
}

/* ------------------------------------------------------------- db-kpis */
.db-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1px;
  background: var(--db-line);
  border: 1px solid var(--db-line);
  border-radius: 14px;
  overflow: hidden;
  margin-bottom: 2.25rem;
  box-shadow: var(--db-shadow-sm);
  font-family: var(--db-font-body);
}
.db-kpi {
  background: var(--db-surface);
  padding: 1.2rem 1.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-decoration: none;
  color: inherit;
  transition: background 0.12s ease;
}
.db-kpi:hover { background: var(--db-surface-2); }
.db-kpi-label { font-size: 0.74rem; color: var(--db-ink-soft); font-weight: 500; }
.db-kpi-value {
  font-family: var(--db-font-display);
  font-size: 1.75rem;
  font-weight: 600;
  color: var(--db-ink);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.db-kpi-value small { font-family: var(--db-font-mono); font-size: 0.95rem; font-weight: 500; color: var(--db-ink-faint); }
.db-kpi-trend { font-family: var(--db-font-mono); font-size: 0.74rem; font-weight: 500; color: var(--db-ink-faint); }
.db-kpi-trend.warn { color: var(--db-pending); }
.db-kpi-trend.up { color: var(--db-ok); }
.db-kpi.money .db-kpi-value { color: var(--db-sand-deep); }

/* -------------------------------------------------------- section head */
.db-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 1rem;
  font-family: var(--db-font-body);
}
.db-section-head h2 {
  font-family: var(--db-font-display);
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--db-ink);
  margin: 0;
}
.db-section-head .db-count { font-size: 0.82rem; color: var(--db-ink-faint); }
.db-section-head .db-count strong { color: var(--db-critical); font-weight: 600; }

/* ---------------------------------------------------------- db-priority */
.db-priority {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--db-line);
  border-radius: 12px;
  overflow: hidden;
  background: var(--db-surface);
  box-shadow: var(--db-shadow-sm);
  margin-bottom: 2.25rem;
}
.db-priority-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.9rem 1.2rem 0.9rem 1rem;
  text-decoration: none;
  color: inherit;
  border-left: 3px solid transparent;
  border-bottom: 1px solid var(--db-line-soft);
  font-family: var(--db-font-body);
  transition: background 0.12s ease;
}
.db-priority-row:last-child { border-bottom: none; }
.db-priority-row:hover { background: var(--db-surface-2); }
.db-priority-row.sev-critical { border-left-color: var(--db-critical); }
.db-priority-row.sev-pending { border-left-color: var(--db-pending); }
.db-row-icon {
  width: 34px; height: 34px;
  border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  flex: none;
  font-size: 0.95rem;
}
.sev-critical .db-row-icon { background: var(--db-critical-mist); color: var(--db-critical); }
.sev-pending .db-row-icon { background: var(--db-pending-mist); color: var(--db-pending); }
.db-row-text { flex: 1; min-width: 0; }
.db-row-title {
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--db-ink);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.db-row-sub { font-size: 0.8rem; color: var(--db-ink-soft); margin-top: 0.1rem; }
.db-row-count {
  font-family: var(--db-font-mono);
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--db-ink);
  min-width: 2.5ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.db-row-chev { color: var(--db-ink-faint); flex: none; font-size: 0.85rem; }

/* .db-priority-plain — a quieter variant of .db-priority for lists whose
   rows carry real action controls (forms, buttons, selects), not just a
   label + chevron: the border/shadow used everywhere else reads as too
   heavy once every row also has its own bordered inputs. Lighter outer
   border, no shadow, more breathing room between rows. */
.db-priority-plain {
  box-shadow: none;
  border-color: var(--db-line-soft);
}
.db-priority-plain .db-priority-row { padding-top: 1.1rem; padding-bottom: 1.1rem; }

/* .db-priority-row-actions — /cleaning's "Maintenance à traiter" rows add
   real action controls (assign form, quote/bill forms) as a third column
   after the icon and text, unlike dashboard.html's rows (icon + text +
   count/chevron only, no independent action). A 3-column grid on desktop
   — icon | text (flexible) | actions (right-aligned, own track so it
   never squeezes the title) — collapsing to a single column on narrow
   screens, actions moving below the text and spanning full width so a
   multi-field form never has to shrink to fit a sliver of space. Scoped
   to this modifier class so dashboard's simpler single-line rows keep
   their current flex layout untouched. No sev-critical/sev-pending
   accent bar here either (this class is applied instead of, not
   alongside, those) — severity is already carried by the status pills in
   the title, a colored left bar on every row read as noise once nearly
   every row needed one. */
.db-priority-row-actions {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) minmax(0, 340px);
  column-gap: 1rem;
  row-gap: 0.6rem;
  align-items: start;
}
.db-priority-row-actions .db-row-icon { grid-column: 1; grid-row: 1; }
.db-priority-row-actions .db-row-text { grid-column: 2; grid-row: 1; }
.db-priority-row-actions .maint-actions {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: center;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
}
@media (max-width: 640px) {
  .db-priority-row-actions { grid-template-columns: 34px minmax(0, 1fr); }
  .db-priority-row-actions .maint-actions {
    grid-column: 1 / -1;
    grid-row: 2;
    justify-self: stretch;
    justify-content: flex-start;
    margin-top: 0.2rem;
  }
  .db-priority-row-actions .maint-actions > * { max-width: 100%; }
}

/* Cohesive input/button language for the maintenance actions column — the
   same visual vocabulary as any other form field in this app (see
   app.css's `form input, form select` base rule: 1px var(--border),
   6px radius, #fff background), just sized a touch more compactly for a
   row-level control. Shared by the quote/bill forms and by
   .assign-control below so every control here reads as one family. */
.maint-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.5rem;
  margin: 0;
}
.maint-input {
  height: 2.4rem;
  padding: 0 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.85rem;
  font-family: var(--db-font-body);
  background: #fff;
  color: var(--db-ink);
}
.maint-input:focus { outline: 2px solid var(--db-tide-mist-2); outline-offset: 1px; }
.maint-input-amount { width: 6.5rem; }
.maint-input-note { width: 10rem; }
.maint-btn {
  height: 2.4rem;
  padding: 0 0.9rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--db-surface-2);
  color: var(--db-tide);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  font-family: var(--db-font-body);
}
.maint-btn:hover { background: var(--db-tide-mist); border-color: var(--db-tide); }
.maint-btn-primary { background: var(--db-tide); color: #fff; border-color: var(--db-tide); }
.maint-btn-primary:hover { background: var(--db-tide-mid); border-color: var(--db-tide-mid); }

/* .assign-select — a single dropdown IS the control: choosing a name
   submits immediately (onchange="this.form.submit()", same established
   pattern as rates_recap.html's period select and property_images.html's
   cover-photo radios — plain HTML form semantics, no page-specific JS
   file needed). One click, not two; no split button pretending to be a
   sentence. A small uppercase caption above states what the control does
   ("Assigner"), and the select's own value states the current state
   ("Non assigné" or the assignee's name) — state lives in the value, not
   in a placeholder. .is-assigned tints the field so an assigned row reads
   at a glance across the whole list, without needing to read the text. */
.assign-select-form {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin: 0;
}
.assign-select-label {
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--db-ink-faint);
}
.assign-select {
  height: 2.4rem;
  max-width: 12rem;
  padding: 0 1.8rem 0 0.7rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background-color: #fff;
  color: var(--db-ink);
  font-size: 0.85rem;
  font-weight: 500;
  font-family: var(--db-font-body);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23667380'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.19l3.71-3.96a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 1em;
}
.assign-select:hover { border-color: var(--db-tide); }
.assign-select:focus { outline: 2px solid var(--db-tide-mist-2); outline-offset: 1px; }
.assign-select.is-assigned {
  border-color: var(--db-ok);
  background-color: var(--db-ok-mist);
  color: var(--db-ok);
  font-weight: 600;
}

/* -------------------------------------------------------- cleaning-day */
/* /cleaning's "Ménages à affecter" section: one heading per deadline day,
   each followed by a row of size-cards (capacity-band buckets), each
   holding clickable property thumbnails that open the shared assign
   dialog (cleaningAssignDialog partial). Each day's heading+cards is its
   own group, sized to its own content — .cleaning-day-list then wraps
   these groups left to right, so e.g. a day with a single small band
   sits next to the next day's group instead of claiming a full-width row
   on its own with mostly empty space. Groups only break to a new line
   when they don't fit, same reasoning as .size-card itself. */
.cleaning-day-list {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.6rem 2rem;
  margin-top: 0.4rem;
}
.cleaning-day-group { flex: 0 0 auto; }
.cleaning-day-heading {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--db-ink);
  margin: 0 0 0.6rem;
}
.size-card-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
}
.size-card {
  /* Sized to its own content (icon-count header + however many thumbnails
     it holds), not stretched to fill the row — a band with 2 turnovers
     shouldn't take the same width as one with 8, and a wide panel should
     fit several bands per row instead of forcing one per line. Capped so
     a band with many turnovers wraps its thumbnails internally rather
     than growing the card to an unreasonable width. */
  flex: 0 0 auto;
  min-width: 160px;
  max-width: 320px;
  border: 1px solid var(--db-line);
  border-radius: 12px;
  background: var(--db-surface);
  box-shadow: var(--db-shadow-sm);
  padding: 0.8rem 0.9rem;
}
.size-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.size-card-label {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--db-ink-soft);
}
.size-card-count {
  font-family: var(--db-font-mono);
  font-weight: 700;
  font-size: 0.85rem;
  color: var(--db-ink);
  background: var(--db-surface-2);
  border-radius: 999px;
  padding: 0.05rem 0.55rem;
}
.size-card-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.size-card-thumb {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  border: 2px solid transparent;
  background-color: var(--off-bg);
  background-size: cover;
  background-position: center;
  cursor: pointer;
  padding: 0;
  flex: none;
  transition: transform 0.1s ease, border-color 0.1s ease;
}
.size-card-thumb:hover { transform: scale(1.06); }
.size-card-thumb.is-urgent { border-color: var(--db-critical); }
.size-card-thumb-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 0.6rem;
  font-weight: 600;
  color: var(--db-ink-soft);
  text-align: center;
  line-height: 1.1;
  padding: 0.15rem;
  overflow: hidden;
}

/* ------------------------------------------------------------ db-empty */
.db-empty {
  padding: 1.5rem 1.3rem;
  text-align: center;
  color: var(--db-ink-soft);
  font-size: 0.9rem;
  font-family: var(--db-font-body);
}
.db-empty i { color: var(--db-ok); margin-right: 0.4rem; }

/* --------------------------------------------------------- db-actions */
.db-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0.7rem;
  font-family: var(--db-font-body);
}
.db-action {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--db-line);
  border-radius: 10px;
  background: var(--db-surface);
  text-decoration: none;
  color: var(--db-ink);
  font-size: 0.86rem;
  font-weight: 500;
  transition: border-color 0.12s ease, transform 0.12s ease;
}
.db-action:hover { border-color: var(--db-tide-mid); transform: translateY(-1px); }
.db-action i { color: var(--db-tide-mid); flex: none; width: 1.1em; text-align: center; }
.db-action-sub { display: block; font-size: 0.76rem; font-weight: 400; color: var(--db-ink-soft); margin-top: 0.1rem; }

/* ---------------------------------------------------------- db-split */
.db-split { display: grid; grid-template-columns: 1.35fr 1fr; gap: 1.75rem; align-items: start; }
@media (max-width: 1100px) { .db-split { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------- db-panel */
.db-panel {
  border: 1px solid var(--db-line);
  border-radius: 12px;
  background: var(--db-surface);
  padding: 1.35rem 1.45rem;
  box-shadow: var(--db-shadow-sm);
  font-family: var(--db-font-body);
}
.db-panel h3 { font-size: 0.95rem; font-weight: 600; margin: 0 0 0.2rem; color: var(--db-ink); }
.db-panel .db-panel-sub { font-size: 0.78rem; color: var(--db-ink-soft); margin-bottom: 1rem; }
/* Fixed 14-column grid, not flex-wrap: a flex row that runs out of space
   wraps leftover cells onto a mostly-empty new line and stretches them
   (flex:1) to fill it — a real bug, not just a squeeze. Grid shrinks every
   column together instead. */
.db-strip { display: grid; grid-template-columns: repeat(14, 1fr); gap: 3px; margin-bottom: 0.9rem; }
.db-strip .db-day {
  height: 34px;
  border-radius: 5px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--db-font-mono);
  font-size: 0.68rem; font-weight: 600;
  color: var(--db-tide-deep);
  background: var(--db-stone-deep);
}
.db-strip .l0 { background: var(--db-stone-deep); color: var(--db-ink-faint); }
.db-strip .l1 { background: var(--db-tide-mist-2); }
.db-strip .l2 { background: #9cc6cc; color: #0e2b35; }
.db-strip .l3 { background: var(--db-tide-mid); color: #fff; }
.db-strip .l4 { background: var(--db-tide-deep); color: #fff; }
.db-legend { display: flex; gap: 1rem; font-size: 0.72rem; color: var(--db-ink-faint); margin-top: 0.75rem; flex-wrap: wrap; }
.db-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.db-legend i { width: 9px; height: 9px; border-radius: 3px; display: inline-block; }
.db-arrivals { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.75rem; }
.db-arrival-row {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 0.82rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--db-line-soft);
}
.db-arrival-row:last-child { border-bottom: none; padding-bottom: 0; }
.db-arrival-name { font-weight: 600; color: var(--db-ink); }
.db-arrival-prop { color: var(--db-ink-soft); font-size: 0.78rem; }
.db-arrival-date { font-family: var(--db-font-mono); font-size: 0.78rem; color: var(--db-ink-soft); }

/* ---------------------------------------------------------------- 04-guide.css */
/* ============================================================================
   /guide — user documentation section (see backend/cmd/app/guide.go). Reuses
   .cards-grid/.panel/.card-link for the topic/page index tiles; the only new
   rules are the Mermaid diagram container (scrolls on mobile instead of
   overflowing, same treatment .filter-pills/.table-wrap already use) and a
   small subtitle style for topic/page descriptions.
   ============================================================================ */
.guide-desc { color: var(--muted); margin-top: -0.5rem; }
.guide-diagram {
  overflow-x: auto;
  margin: 1rem 0;
  padding: 0.5rem 0;
  -webkit-overflow-scrolling: touch;
}
.guide-diagram .mermaid { min-width: 320px; }
.guide-diagram svg { cursor: zoom-in; }
.guide-diagram-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 2rem;
  background: rgba(15, 43, 53, 0.85);
  overflow: hidden;
}
.guide-diagram-overlay-stage {
  flex: 1;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;
}
.guide-diagram-overlay-stage.is-dragging { cursor: grabbing; }
.guide-diagram-overlay-stage svg {
  max-width: 95vw;
  max-height: 88vh;
  width: auto;
  height: auto;
  background: var(--db-surface);
  border-radius: var(--radius-md, 10px);
  padding: 1.5rem;
  /* Dragging fires frequent transform updates on .guide-diagram-overlay-stage
     itself — pointer-events stay on the SVG so mousedown still starts a drag,
     this just stops the browser from treating the SVG as a draggable image
     and showing its own native drag ghost. */
  -webkit-user-drag: none;
  user-select: none;
}
.guide-diagram-overlay-toolbar {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  display: flex;
  gap: 0.5rem;
  z-index: 1001;
}
.guide-diagram-overlay-btn {
  width: 2.5rem;
  height: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-size: 1rem;
  cursor: pointer;
}
.guide-diagram-overlay-btn:hover { background: rgba(255, 255, 255, 0.24); }
.guide-diagram-overlay-close { font-size: 1.15rem; }
/* Card-as-mini-table: a "header row" (square icon cell + wide title cell)
   with the description as a second, full-width "merged row" underneath,
   separated by a rule — reads as a tiny 2-row table rather than a stacked
   list. Grid instead of flex specifically so the description can span both
   header columns via grid-column: 1 / -1. */
.guide-card {
  display: grid;
  grid-template-columns: 44px 1fr;
  align-items: center;
  column-gap: 0.85rem;
}
.guide-card > .guide-card-icon { grid-column: 1; grid-row: 1; }
.guide-card > h2 { grid-column: 2; grid-row: 1; margin: 0; }
.guide-card > .guide-desc {
  grid-column: 1 / -1;
  grid-row: 2;
  margin: 0;
  margin-top: 0.85rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}
.guide-card-icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: var(--brand-light);
  color: var(--brand);
  font-size: 1.05rem;
}
.guide-list-icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 9px;
  background: var(--brand-light);
  color: var(--brand);
  font-size: 0.95rem;
}
.guide-keywords {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.guide-keyword {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  background: var(--db-stone-deep, #e7e3d6);
  color: var(--muted);
  font-size: 0.75rem;
  white-space: nowrap;
}
/* Fixed layout so the description column doesn't stretch to fill all
   available width on wide desktop screens — icon/title/keywords keep their
   natural size, description gets a capped share instead of the lion's
   share of a 1200px+ viewport. */
.guide-infra-table { table-layout: fixed; width: 100%; }
.guide-infra-table th:nth-child(1), .guide-infra-table td:nth-child(1) { width: 52px; }
.guide-infra-table th:nth-child(2), .guide-infra-table td:nth-child(2) { width: 20%; }
.guide-infra-table th:nth-child(3), .guide-infra-table td:nth-child(3) { width: 32%; }
.guide-infra-table th:nth-child(4), .guide-infra-table td:nth-child(4) { width: auto; }
.guide-diagram-overlay-hint {
  color: #fff;
  font-size: var(--text-sm, 0.85rem);
  opacity: 0.85;
  margin: 0;
}

/* Screenshot gallery — real captures (desktop/mobile) of each espace,
   grouped per space via .guide-screenshot-group. Grid instead of a fixed
   side-by-side pair since a mobile capture is much narrower/taller than a
   desktop one and shouldn't be stretched to match its width. */
.guide-screenshot-group {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.25rem;
  margin: 1rem 0 1.75rem;
}
.guide-screenshot {
  margin: 0;
  flex: none;
}
.guide-screenshot img {
  display: block;
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  box-shadow: 0 2px 10px rgba(15, 43, 53, 0.08);
}
.guide-screenshot.is-desktop img { width: 480px; }
.guide-screenshot.is-mobile img { width: 200px; }
.guide-screenshot figcaption {
  margin-top: 0.4rem;
  font-size: var(--text-sm, 0.8rem);
  color: var(--muted);
  text-align: center;
}

/* ---------------------------------------------------------------- 05-team.css */
/* ============================================================================
   /team — internal staff + outside prestataires, one card per person,
   responsive grid (see backend/cmd/desk/team.go, templates/team.html).
   ============================================================================ */
.team-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
  margin: 1rem 0 1.75rem;
}
.team-card {
  background: var(--db-surface);
  border: 1px solid var(--db-line);
  border-radius: var(--radius-lg, 16px);
  padding: 1.1rem 1.2rem;
  box-shadow: var(--db-shadow-sm);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-decoration: none;
  color: inherit;
  transition: background-color 0.15s ease;
}
.team-card:hover {
  background: var(--db-tide-mist);
  text-decoration: none;
}
.team-card.is-inactive { opacity: 0.55; }
.team-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
}
.team-card-name {
  font-family: var(--db-font-display);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--db-ink);
}
.team-card-specialty { font-size: 0.8rem; color: var(--db-ink-soft); margin-top: 0.1rem; }
.team-role-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
  background: var(--db-tide-mist);
  color: var(--db-tide);
}
.team-role-pill.role-cleaning { background: var(--db-ok-mist); color: var(--db-ok); }
.team-role-pill.role-prestataire { background: var(--db-sand-mist); color: var(--db-sand-deep); }
.team-role-pill.role-directeur { background: var(--db-tide-deep); color: #fff; }
.team-card-contact {
  font-size: 0.82rem;
  color: var(--db-ink-soft);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.team-card-contact a { color: inherit; }
.team-card-hours {
  font-size: 0.8rem;
  color: var(--db-ink-soft);
  font-family: var(--db-font-mono);
  font-variant-numeric: tabular-nums;
}
.team-card-hours strong { color: var(--db-ink); }
.team-card-hours.over-budget strong { color: var(--db-critical); }
.team-card-foot {
  margin-top: auto;
  padding-top: 0.6rem;
  border-top: 1px solid var(--db-line-soft);
}
.team-card-foot span { font-size: 0.82rem; font-weight: 600; color: var(--db-tide); }

/* ============================================================================
   Property hub — "Propriétaire(s)" card, replacing what used to be a
   header-heavy table for what's usually a single row (see
   templates/property_hub.html). Same visual language as .team-card.
   ============================================================================ */
.owner-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1rem;
  margin: 1rem 0 1.25rem;
}
.owner-card {
  background: var(--db-surface);
  border: 1px solid var(--db-line);
  border-radius: var(--radius-lg, 16px);
  padding: 1.1rem 1.2rem;
  box-shadow: var(--db-shadow-sm);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  text-decoration: none;
  color: inherit;
  transition: background-color 0.15s ease;
}
.owner-card:hover {
  background: var(--db-tide-mist);
  text-decoration: none;
}
.owner-card-head {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
.owner-card-avatar {
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  background: var(--db-tide-mist);
  color: var(--db-tide);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--db-font-display);
  font-weight: 600;
  font-size: 1rem;
}
.owner-card-name {
  font-family: var(--db-font-display);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--db-ink);
}
.owner-card-type { font-size: 0.78rem; color: var(--db-ink-soft); margin-top: 0.1rem; }
.owner-card-contact {
  font-size: 0.82rem;
  color: var(--db-ink-soft);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.owner-card-contact a { color: inherit; }
.owner-card-contact i { width: 1.1em; text-align: center; }
.owner-card-foot {
  margin-top: auto;
  padding-top: 0.6rem;
  border-top: 1px solid var(--db-line-soft);
}
.owner-card-foot span { font-size: 0.82rem; font-weight: 600; color: var(--db-tide); }

/* ---------------------------------------------------------------- 06-operators.css */
/* ============================================================================
   /settings/desk-operators — roster grid (mirrors .team-grid/.team-card
   above almost exactly, same visual language, different footer content)
   plus the per-operator page's role picker and capability grid.
   ============================================================================ */
.operator-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
  margin: 1rem 0 1.75rem;
}
.operator-card {
  background: var(--db-surface);
  border: 1px solid var(--db-line);
  border-radius: var(--radius-lg, 16px);
  padding: 1.1rem 1.2rem;
  box-shadow: var(--db-shadow-sm);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  text-decoration: none;
  color: inherit;
  transition: background-color 0.15s ease, transform 0.15s ease;
}
.operator-card:hover {
  background: var(--db-tide-mist);
  text-decoration: none;
  transform: translateY(-2px);
}
.operator-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
}
.operator-card-name {
  font-family: var(--db-font-display);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--db-ink);
}
.operator-card-email { font-size: 0.78rem; color: var(--db-ink-soft); margin-top: 0.1rem; }
.team-role-pill.role-admin { background: var(--db-sand-mist); color: var(--db-sand-deep); }
.team-role-pill.role-assistant { background: var(--db-tide-mist); color: var(--db-tide); }
.operator-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--db-line-soft);
  font-size: 0.8rem;
  color: var(--db-ink-soft);
}
.operator-card-access { display: inline-flex; align-items: center; gap: 0.35rem; }
.operator-card-access-all { color: var(--db-ok); font-weight: 600; }

/* Role picker — three big selectable tiles standing in for a <select>,
   since the choice materially changes the page below it (the capability
   grid appears/disappears) and deserves to read as a real decision, not a
   dropdown item. Native radios, visually hidden but still tabbable/
   spacebar-toggleable — no ARIA reinvention needed. */
.role-picker { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.7rem; margin-top: 0.4rem; }
.role-option { position: relative; cursor: pointer; }
.role-option input {
  position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none;
}
.role-option-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  padding: 0.9rem 1rem;
  border: 1.5px solid var(--db-line);
  border-radius: var(--radius-md, 10px);
  background: var(--db-surface);
  transition: border-color 0.12s ease, background 0.12s ease;
}
.role-option-body i { color: var(--db-ink-faint); font-size: 0.95rem; margin-bottom: 0.2rem; }
.role-option-body strong { font-family: var(--db-font-display); font-size: 0.98rem; color: var(--db-ink); }
.role-option-body small { color: var(--db-ink-soft); font-size: 0.78rem; }
.role-option:hover .role-option-body { border-color: var(--db-tide-mid); }
.role-option input:checked + .role-option-body {
  border-color: var(--db-tide);
  background: var(--db-tide-mist);
  box-shadow: 0 0 0 1px var(--db-tide) inset;
}
.role-option input:checked + .role-option-body i { color: var(--db-tide); }
.role-option input:focus-visible + .role-option-body { outline: 2px solid var(--db-tide); outline-offset: 2px; }

/* Capability grid — same "card standing in for a checkbox" idea as the
   role picker, one per gated section, icon matching that section's own
   rail icon (see cmd/desk/desk_operators.go's allCapabilities). */
.capability-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.7rem; margin-top: 1rem; }
.capability-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.9rem 1rem;
  border: 1.5px solid var(--db-line);
  border-radius: var(--radius-md, 10px);
  background: var(--db-surface);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.capability-card input {
  position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none;
}
.capability-card-icon {
  flex: none;
  width: 2.1rem; height: 2.1rem;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--db-tide-mist);
  color: var(--db-tide);
  font-size: 0.9rem;
}
.capability-card-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.capability-card-label { font-weight: 600; font-size: 0.92rem; color: var(--db-ink); }
.capability-card-desc { font-size: 0.76rem; color: var(--db-ink-soft); line-height: 1.3; }
.capability-card-check {
  flex: none;
  margin-left: auto;
  width: 1.3rem; height: 1.3rem;
  border-radius: 50%;
  border: 1.5px solid var(--db-line);
  display: flex; align-items: center; justify-content: center;
  color: transparent;
  font-size: 0.68rem;
  transition: all 0.12s ease;
}
.capability-card:hover { border-color: var(--db-tide-mid); }
.capability-card input:checked ~ .capability-card-check {
  background: var(--db-tide);
  border-color: var(--db-tide);
  color: #fff;
}
.capability-card:has(input:checked) {
  border-color: var(--db-tide);
  background: var(--db-tide-mist);
}
.capability-card input:focus-visible ~ .capability-card-check { outline: 2px solid var(--db-tide); outline-offset: 2px; }
.capability-card input:disabled ~ * { opacity: 0.5; }
.capability-card:has(input:disabled) { cursor: default; }

/* ============================================================================
   /settings/technical-accounts — the masked-secret cell, eye toggle and
   copy button. See technical-accounts.js.
   ============================================================================ */

/* Masqué par défaut : la valeur réelle n'entre dans le DOM qu'après un appel
   à /reveal, jamais posée là par le serveur au premier rendu. Une fois
   révélée, elle passe en police à chasse fixe pour que des caractères
   ambigus (l/1, O/0) se distinguent à l'œil. */
.secret-cell {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
}
.secret-text { font-variant-numeric: tabular-nums; letter-spacing: 0.05em; }
.secret-text.is-revealed {
  font-family: var(--font-mono, monospace);
  letter-spacing: 0;
  user-select: all;
}
.secret-status { font-size: 0.8rem; color: var(--muted); min-width: 3.5rem; }

/* ---------------------------------------------------------------- 07-cleaning.css */
/* ============================================================================
   /cleaning/calendar — "Planning des ménages". Desktop keeps a reference
   grid (every property × every day); mobile drops the grid entirely for a
   day-first agenda of just the actionable turnovers (see cleaning.go's
   buildCalendarAgenda doc comment for why — a property×day matrix simply
   doesn't fit a phone, and a horizontally-scrolling table with no
   affordance just hides most of the data). Both share the legend/dot
   vocabulary below so "what does this color mean" only has to be learned
   once.
   ============================================================================ */
.cc-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  margin-bottom: 0.4rem;
}
.cc-window-picker { display: flex; gap: 0.4rem; }

.cc-legend {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.2rem;
  font-size: 0.82rem;
  color: var(--db-ink-soft);
  margin: 0.4rem 0 1.2rem;
  font-family: var(--db-font-body);
}
.cc-swatch {
  display: inline-block;
  width: 0.95rem;
  height: 0.95rem;
  border-radius: 4px;
  vertical-align: middle;
  margin-right: 0.35rem;
}
.cc-swatch-free { background: var(--db-surface); border: 1px solid var(--cc-line); }
/* « Réservé » était à 1,26:1 de « Libre » : c'est le fait principal de cette
   grille, et il était invisible. Assombri à 1,84:1 — pas jusqu'aux 3:1 d'une
   règle générale, parce que la grille est majoritairement réservée et qu'un
   remplissage mi-gris partout écraserait les marqueurs, qui sont le travail.
   L'occupation est le contexte ; l'affectation est la tâche. */
.cc-swatch-booked {
  background-color: var(--cc-booked-bg);
  background-image: repeating-linear-gradient(
    45deg,
    var(--cc-booked) 0, var(--cc-booked) 1.5px,
    transparent 1.5px, transparent 6px);
  border: 1px solid var(--cc-line);
}

/* LA FORME AUTANT QUE LA COULEUR.
   Les deux marqueurs étaient un vert et un rouge de luminance quasi identique
   — 1,24:1 l'un contre l'autre. Deux teintes, une seule clarté : le même point
   pour qui ne distingue pas le rouge du vert, le même point en noir et blanc,
   le même point sur une capture d'écran. Une coche et un disque plein se
   distinguent sans couleur du tout.
   Et l'ambre plutôt que le rouge : une liste de ménages à affecter est un plan
   de travail, pas une avarie. */
.cc-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.05rem; height: 1.05rem; border-radius: 50%;
  vertical-align: middle; margin-right: 0.35rem;
  font-size: 0.72rem; font-weight: 700; line-height: 1;
}
.cc-mark-assigned { background: transparent; color: var(--cc-assigned); box-shadow: inset 0 0 0 1.5px var(--cc-assigned); }
.cc-mark-needed { background: var(--cc-needed); box-shadow: inset 0 0 0 1px var(--cc-needed-ink); }

/* Les couleurs de ce planning, nommées ici plutôt que reprises du thème :
   ce sont des états d'un tableau, mesurés les uns contre les autres, et non
   les sémantiques « ok / critique » du reste de l'application. */
:root {
  --cc-booked:      #b8a880;  /* la teinte des hachures */
  --cc-booked-bg:   #f3eee1;  /* le fond sur lequel elles courent */
  --cc-line:        #8f866c;
  --cc-assigned:    #1a6b45;
  --cc-needed:      #e8a33d;  /* 3,01:1 contre le vert, et lisible en gris */
  --cc-needed-ink:  #a5701d;
  --cc-needed-mist: #fbf0dc;
  /* Le lavis du créneau : assez clair pour rester du « libre » et ne pas
     concurrencer le marqueur qu'il porte, assez présent pour se voir. */
  --cc-gap-wash: #fdf7ea;
  --cc-gap-wash-hover: #f8edd6;
}

/* -------------------------------------------------------------- grid --- */
.cc-grid-wrap { max-height: 70vh; overflow: auto; }
.cc-grid { border-collapse: collapse; width: 100%; font-family: var(--db-font-mono); }
/* No vertical gridlines at all — with only two fill states (free/booked)
   plus the merged-bar technique above, a hairline around every one of the
   ~250 cells was itself the dominant visual texture ("a spreadsheet"),
   independent of what color those cells were. A row-separator is the only
   line this grid needs; the date axis reads from the sticky header, not
   from vertical rules. */
.cc-grid th, .cc-grid td {
  border: none;
  /* Le filet était à 1,06:1 du remplissage « réservé » : les cases
     n'avaient plus de bord. */
  border-bottom: 1px solid var(--cc-line);
  padding: 0;
  text-align: center;
  font-size: 0.76rem;
}
.cc-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--db-surface-2);
  color: var(--db-ink-soft);
  font-family: var(--db-font-body);
  font-weight: 600;
  font-size: 0.72rem;
  padding: 0.35rem 0.3rem;
  white-space: nowrap;
  border-bottom: 1px solid var(--db-line);
}
.cc-grid th.today { background: var(--db-tide); color: #fff; }
.cc-band-row td {
  text-align: left;
  background: var(--db-stone);
  color: var(--db-ink-soft);
  font-family: var(--db-font-body);
  font-weight: 700;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--db-line);
}
.cc-propcell {
  text-align: left !important;
  padding: 0.35rem 0.6rem !important;
  white-space: nowrap;
  background: var(--db-surface);
  position: sticky;
  left: 0;
  z-index: 1;
}
.cc-propcell a {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--db-ink);
  font-family: var(--db-font-body);
  font-weight: 600;
  font-size: 0.85rem;
}
.cc-propcell .row-thumb { width: 24px; height: 24px; flex: none; }
.cc-cell { width: 2.3rem; height: 1.7rem; }
/* LE CRÉNEAU, ET NON LE SEUL JOUR DU DÉPART.
   Entre un départ et l'arrivée suivante il y a parfois une semaine, et la
   grille ne montrait que le premier jour : on lisait « ménage le 27 » sans
   voir qu'on avait jusqu'au 2. Les jours du créneau portent maintenant un
   lavis, avec un liseré aux deux bouts pour en marquer les bornes. C'est la
   réponse visuelle à « de combien de temps je dispose », qui est la question
   que se pose quelqu'un qui répartit le travail. */
.cc-cell-gap { cursor: pointer; }
/* Le lavis ne va que sur les jours réellement libres. Le jour de l'arrivée
   appartient au créneau — on peut faire le ménage le matin même, et il reste
   donc cliquable — mais il est réservé, et le peindre comme du libre le
   dirait faux. Ce sont les liserés qui marquent les bornes. */
.cc-cell-gap.free { background: var(--cc-gap-wash); }
.cc-cell-gap.free:hover { background: var(--cc-gap-wash-hover); }
/* En retard : le départ est déjà passé et le ménage n'a pas eu lieu. Le
   marqueur est ramené au premier jour affiché, et porte un anneau — une forme,
   pas une nuance de plus dans la même famille de jaunes. */
.cc-late .cc-mark,
.cc-mark-late { box-shadow: 0 0 0 2px var(--db-surface), 0 0 0 2px var(--db-alert, #b3261e) inset, 0 0 0 4px var(--db-alert, #b3261e); }
.cc-gap-first { box-shadow: inset 2px 0 0 var(--cc-needed-ink); }
.cc-gap-last { box-shadow: inset -2px 0 0 var(--cc-needed-ink); }
.cc-gap-first.cc-gap-last { box-shadow: inset 2px 0 0 var(--cc-needed-ink), inset -2px 0 0 var(--cc-needed-ink); }

/* "Libre" is the default/majority state and stays uncolored (just the plain
   surface) — coloring both free AND booked created a wall-to-wall
   checkerboard that competed with the turnover markers for attention.
   "Réservé" gets one quiet neutral fill; a run of consecutive booked
   nights for the same stay is merged server-side (see mergeBookedRuns,
   cleaning.go) into a single spanning cell — a continuous bar per stay,
   the same technique every hotel PMS tape chart (WebRezPro, RoomKeyPMS,
   Cloudbeds) uses instead of shading every night separately. */
/* UNE TRAME PLUTÔT QU'UN APLAT.
   Manière des vieilles cartes en noir et blanc : on distingue les zones par
   leur texture, pas par leur teinte. Une hachure se voit sur un écran mal
   réglé, à l'impression, en niveaux de gris, et par-dessus elle laisse le
   marqueur ressortir — un aplat assez foncé pour être vu concurrençait ce
   qu'il porte. Le pas est de 6 px : assez large pour se lire dans une case de
   quelques pixels, assez serré pour faire matière et non rayure. */
.cc-cell.booked {
  background-color: var(--cc-booked-bg);
  background-image: repeating-linear-gradient(
    45deg,
    var(--cc-booked) 0, var(--cc-booked) 1.5px,
    transparent 1.5px, transparent 6px);
}
/* Surtout PAS display: flex ici. Un <td> en flex cesse d'être une cellule de
   tableau : il ne s'étire plus à la hauteur de sa ligne, et le marqueur se
   retrouvait cinq pixels au-dessus du centre, sa cellule débordant sur le
   filet de la ligne du dessus. Mesuré : cellule de 17 px dans une ligne de 36.
   Une cellule de tableau se centre avec text-align et vertical-align. */
.cc-cell-turnover {
  position: relative;
  text-align: center;
  vertical-align: middle;
}
.cc-cell-turnover .cc-mark { margin: 0; }
.cc-cell-turnover.cc-assigned .cc-mark {
  background: transparent; color: var(--cc-assigned);
  box-shadow: inset 0 0 0 1.5px var(--cc-assigned);
}
.cc-cell-turnover.cc-needs-cleaning .cc-mark {
  background: var(--cc-needed); box-shadow: inset 0 0 0 1px var(--cc-needed-ink);
}
/* The one deliberately saturated exception: an unassigned turnover is the
   single actionable state on this whole grid, so it's the only cell that
   gets a color wash — a soft tint, not a hard box-shadow border (that
   read as a stack of alarm boxes once there were a dozen of them). */
/* Le jour qui demande une affectation : trame croisée et plus serrée que celle
   de « réservé ». Deux textures qui se distinguent l'une de l'autre sans
   couleur, comme deux figurés sur une carte. */
.cc-cell-turnover.cc-needs-cleaning {
  background-color: var(--cc-needed-mist);
  background-image:
    repeating-linear-gradient(45deg,  var(--cc-needed-ink) 0, var(--cc-needed-ink) 1px, transparent 1px, transparent 5px),
    repeating-linear-gradient(-45deg, var(--cc-needed-ink) 0, var(--cc-needed-ink) 1px, transparent 1px, transparent 5px);
  background-blend-mode: normal;
}

/* ------------------------------------------------------------ agenda --- */
/* The primary view at every width now (see cleaning_calendar.html) — a
   reflowing grid of day-cards, only ever holding days that actually have
   something to show (empty days are dropped server-side). On a wide
   screen this fills the available width with several day-cards side by
   side instead of one property×day matrix; on mobile it collapses to a
   single column, same cards, same markup. */
/* UNE COLONNE, À TOUTES LES LARGEURS.
   La grille de cartes rangeait les journées de gauche à droite puis à la
   ligne : sur un écran large on lisait lundi, mardi, mercredi, puis on
   revenait à gauche pour jeudi. Un agenda se lit de haut en bas — c'est une
   suite de jours, pas un tableau de bord. Une seule colonne, plus large,
   donne aussi la place d'écrire la date en toutes lettres. */
.cc-agenda {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.8rem;
  align-items: start;
  max-width: 46rem;
}
.cc-agenda-day {
  background: var(--db-surface);
  border: 1px solid var(--db-line-soft);
  border-radius: var(--radius-lg, 12px);
  padding: 0.9rem 1rem;
}
.cc-agenda-day.is-today {
  border-color: var(--db-tide);
  box-shadow: 0 0 0 1px var(--db-tide);
}
.cc-agenda-date {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--db-ink);
  margin: 0 0 0.7rem;
}
.cc-agenda-day.is-today .cc-agenda-date { color: var(--db-tide); }
.cc-agenda-empty { color: var(--db-ink-faint); font-size: 0.82rem; margin: 0; }
.cc-agenda-list { display: flex; flex-direction: column; gap: 0.5rem; }
.cc-agenda-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--db-line);
  border-radius: 10px;
  background: var(--db-surface);
  font-family: var(--db-font-body);
  text-align: left;
  cursor: pointer;
}
.cc-agenda-item.is-needed { border-color: var(--db-critical); background: var(--cc-needed-mist); }
.cc-agenda-item .row-thumb { width: 36px; height: 36px; flex: none; border-radius: 8px; }
.cc-agenda-item-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.cc-agenda-item-name { font-weight: 600; font-size: 0.88rem; color: var(--db-ink); }
.cc-agenda-item-status { font-size: 0.76rem; color: var(--db-ink-soft); }
.cc-agenda-item .cc-mark { margin: 0; flex: none; }

/* --------------------------------------------------- grid disclosure --- */
/* The full property×day occupancy matrix, now secondary — collapsed by
   default so it never competes with the agenda above; still one click
   away for the rarer "is this property free on date X" lookup. */
.cc-grid-disclosure { margin-top: 1.4rem; }
.cc-grid-disclosure summary {
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--db-tide);
  padding: 0.5rem 0;
  list-style: none;
}
.cc-grid-disclosure summary::-webkit-details-marker { display: none; }
.cc-grid-disclosure summary::before {
  content: "▸ ";
  display: inline-block;
  transition: transform 0.15s ease;
}
.cc-grid-disclosure[open] summary::before { transform: rotate(90deg); }
.cc-grid-disclosure[open] summary { margin-bottom: 0.6rem; }

@media (max-width: 640px) {
  .cc-agenda { grid-template-columns: 1fr; }
  .cc-legend { flex-direction: column; align-items: flex-start; gap: 0.4rem; }
}

/* ------------------------------------------------------------ traiteur ---
   /intendance/traiteur — un tableau, et non des cartes.

   La première version reprenait la grille de cartes du planning ci-dessus.
   Elle marche là-bas parce qu'une rotation tient en deux mots ; ici une ligne
   porte une carte, des allergies et un téléphone, donc les cartes n'avaient
   plus la même hauteur, les deux colonnes se désalignaient, et une journée ne
   se lisait plus d'un trait. Un tableau remet les mêmes faits l'un sous
   l'autre : le nombre de couverts se compare d'une ligne à la suivante, ce qui
   est ce qu'on fait avant d'aller faire les courses. */
.cat-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.cat-table th, .cat-table td {
  padding: 0.5rem 0.6rem; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--db-line-soft);
}
.cat-table thead th {
  font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--db-ink-soft); font-weight: 600; white-space: nowrap;
}
.cat-table .amount { text-align: right; font-variant-numeric: tabular-nums; }

/* La journée est une ligne du même tableau, et non un bloc à part : c'est ce
   qui garde les colonnes alignées d'un bout à l'autre de la page. */
.cat-daysep th, .cat-daysep td {
  background: var(--db-sand-mist, #f4f1ea);
  font-weight: 700; color: var(--db-ink);
  border-top: 1px solid var(--db-line); border-bottom: 1px solid var(--db-line);
  padding-top: 0.55rem; padding-bottom: 0.55rem;
}
/* « th » est en majuscules dans toute la feuille, ce qui convient à un
   en-tête de colonne mais pas à une date : « MERCREDI 26 AOÛT » se lit moins
   bien que « Mercredi 26 août », et en français le mois est en minuscules.
   La majuscule initiale est déjà posée en Go. */
.cat-daysep th { text-transform: none; letter-spacing: 0; font-size: 0.85rem; }
.cat-daysep.is-today th, .cat-daysep.is-today td { background: var(--db-tide-mist); color: var(--db-tide-deep); }
.cat-day-covers { font-family: var(--db-font-mono); font-size: 0.78rem; white-space: nowrap; }

.cat-table tr.is-pending td { background: var(--db-pending-mist); }
.cat-guests { font-family: var(--db-font-mono); font-weight: 600; color: var(--db-tide); }
.cat-badge {
  font-size: 0.7rem; font-weight: 600; color: var(--db-sand-deep); white-space: nowrap;
  border: 1px solid var(--db-sand); border-radius: 999px; padding: 0.05rem 0.45rem; margin-left: 0.3rem;
}
.cat-table a { color: var(--db-tide); text-decoration: none; }
.cat-table a:hover { text-decoration: underline; }
.cat-table td[data-label="Contact"] a { font-variant-numeric: tabular-nums; white-space: nowrap; }
.cat-menu { display: block; color: var(--db-ink-soft); }
.cat-notes {
  display: block; margin-top: 0.2rem; color: var(--db-critical);
  background: var(--db-critical-mist); border-radius: 6px; padding: 0.2rem 0.4rem;
}

/* Sur mobile, chaque ligne redevient une fiche — le tableau ne rentre pas, et
   sept colonnes en accordéon horizontal ne se lisent pas. */
@media (max-width: 720px) {
  .cat-table, .cat-table tbody, .cat-table tr, .cat-table td { display: block; width: 100%; }
  .cat-table thead { display: none; }
  .cat-table tr { border: 1px solid var(--db-line); border-radius: 10px; margin: 0 0 0.6rem; padding: 0.4rem 0.5rem; }
  .cat-table td { border: 0; padding: 0.15rem 0; }
  .cat-table td::before {
    content: attr(data-label) " · "; font-size: 0.75rem; color: var(--db-ink-faint);
  }
  .cat-table td:empty { display: none; }
  .cat-table .amount { text-align: left; }
  .cat-daysep { border: 0; border-radius: 0; margin: 0.8rem 0 0.4rem; padding: 0; }
  .cat-daysep th { display: block; width: 100%; border-radius: 8px; }
  .cat-daysep td { display: none; }
}

/* Le choix du séjour : un champ de recherche et une liste, plutôt qu'un
   <select> de quarante entrées qu'on parcourt en lisant. On sait de qui on
   parle — un nom, un gîte, une date — et le champ filtre sur les trois. */
.stay-picker { margin: 0 0 1rem; }
.stay-list {
  list-style: none; margin: 0.4rem 0 0; padding: 0;
  max-height: 17rem; overflow-y: auto;
  border: 1px solid var(--db-line); border-radius: 10px;
}
.stay-list li + li { border-top: 1px solid var(--db-line-soft); }
.stay-list li[hidden] { display: none; }
.stay-row {
  display: grid; align-items: baseline; gap: 0.2rem 0.7rem; margin: 0;
  grid-template-columns: auto 1fr auto auto auto;
  padding: 0.5rem 0.7rem; cursor: pointer; font-size: 0.88rem;
}
.stay-row:hover { background: var(--brand-light); }
.stay-row input[type="radio"] { min-width: 0; margin: 0; accent-color: var(--brand); }
.stay-row:has(input:checked) { background: var(--brand-light); font-weight: 600; }
.stay-guest { color: var(--db-ink); }
.stay-where, .stay-when, .stay-count { color: var(--db-ink-soft); font-weight: 400; white-space: nowrap; }
.stay-when { font-variant-numeric: tabular-nums; }
.stay-empty { margin: 0.5rem 0 0; font-size: 0.85rem; }

/* Le séjour retenu, une fois retenu. La liste se replie dessus : cinquante-sept
   lignes dont une en gras, ce n'est pas une réponse à « qu'est-ce que j'ai
   choisi ». */
.stay-chosen {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  border: 1px solid var(--db-tide); border-radius: 10px;
  background: var(--db-tide-mist); padding: 0.7rem 0.9rem;
}
.stay-chosen[hidden] { display: none; }
.stay-chosen-facts { display: flex; align-items: baseline; gap: 0.9rem; flex-wrap: wrap; }
.stay-chosen-guest { color: var(--db-tide-deep); }
.stay-chosen-where, .stay-chosen-when, .stay-chosen-count {
  color: var(--db-ink-soft); font-size: 0.88rem;
}
.stay-chosen-when { font-variant-numeric: tabular-nums; }
#stay-search[hidden] { display: none; }

@media (max-width: 640px) {
  /* Le nom sur sa ligne, le reste dessous : cinq colonnes ne tiennent pas. */
  .stay-row { grid-template-columns: auto 1fr; }
  .stay-where, .stay-when, .stay-count { grid-column: 2; }
}

/* La colonne du bouton « retirer » : la largeur du bouton, pas plus. */
.cat-form-table .cat-line-drop { width: 3rem; text-align: right; }
.cat-form-table .cat-line-drop button { min-width: 0; }

/* Les champs de la grille tarifaire : un montant et un pourcentage, pas les
   deux cents pixels que « form input » pose par défaut. */
.cat-rate-field { display: inline-block; width: 7rem; }
.cat-rate-field input { min-width: 0; width: 100%; text-align: right; }

/* UN REPAS EST UNE FICHE.

   Sept colonnes de champs ne tiennent pas dans un panneau de mille pixels, et
   un tableau sert à comparer des lignes — ici on n'en compare aucune, on en
   saisit une puis une autre. En fiche, chaque champ a la largeur de ce qu'il
   reçoit et son intitulé juste au-dessus. */
.cat-meal {
  border: 1px solid var(--db-line); border-radius: var(--radius-md, 10px);
  background: var(--db-surface); padding: 0.9rem 1.1rem 1.1rem;
  margin: 0 0 0.9rem; position: relative;
}
.cat-meal-title {
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--db-ink-soft); padding: 0 0.35rem;
}
.cat-meal-grid {
  display: grid; gap: 0.5rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.cat-meal-grid .field { margin: 0; }
.cat-meal-grid input, .cat-meal-grid select { width: 100%; min-width: 0; }
/* La carte prend toute la largeur : trente-cinq plats ne tiennent pas dans un
   douzième de panneau. */
.cat-meal-menu { grid-column: 1 / -1; }
.cat-meal-drop {
  position: absolute; top: 0.7rem; right: 0.9rem;
  min-width: 0;
}
/* [hidden] vient de la feuille du navigateur et perd contre la règle qui donne
   display: flex aux boutons. Mesuré : hidden valait bien true, et le bouton
   s'affichait quand même. Troisième fois dans cette feuille — chaque fois
   qu'un élément stylé en flex doit pouvoir disparaître, il faut le dire. */
.cat-meal-drop[hidden] { display: none; }

/* La carte convenue occupe toute la largeur de la fiche : « .field input »
   plafonne à 340 px, ce qui laissait l'intitulé et le champ côte à côte dans
   une cellule de 933. Trente-cinq plats ne tiennent pas dans un tiers. */
.cat-meal-menu input { max-width: none; }
.cat-suppl-field { display: block; }
.cat-suppl-field input { min-width: 0; width: 100%; text-align: right; }

/* Les deux axes d'une commande, en pastilles. Chacune dit un fait ; leur
   couleur ne classe pas par gravité mais par « est-ce fini ». */
.cat-state {
  display: inline-block; white-space: nowrap; font-size: 0.75rem; font-weight: 600;
  border-radius: 999px; padding: 0.1rem 0.5rem;
  border: 1px solid var(--db-line); color: var(--db-ink-soft);
}
.cat-state-draft { border-style: dashed; }
.cat-state-sent, .cat-state-billed { border-color: var(--db-tide); color: var(--db-tide-deep); }
.cat-state-accepted, .cat-state-paid {
  border-color: var(--db-ok); color: var(--db-ok); background: var(--db-ok-mist);
}
.cat-state-declined, .cat-state-expired, .cat-state-cancelled { color: var(--db-ink-faint); }

/* Le délai d'achat est passé sans réponse : la seule ligne qui demande une
   action aujourd'hui. Une barre, et non un fond seul — elle survit à
   l'impression et au daltonisme. */
.table-modern tbody tr.is-overdue > td:first-child { box-shadow: inset 3px 0 0 var(--db-critical); }

/* L'en-tête d'une commande, lue comme une facture : à qui elle s'adresse à
   gauche, ce qu'elle est à droite — référence, numéro, états. C'est la
   disposition d'une facture, et c'est ce qu'on cherche des yeux. */
.cat-inv-head {
  display: grid; gap: 1rem 2rem; grid-template-columns: 1fr;
  margin: -0.2rem -0.2rem 1.2rem; padding: 0.9rem 1rem;
  /* Son propre fond : l'en-tête n'est pas une ligne de plus du panneau, c'est
     à qui la facture s'adresse — et il se clique. */
  background: var(--db-tide-mist); border-radius: var(--radius-md, 10px);
}
.cat-inv-head p { margin: 0 0 0.25rem; }

/* Tout le bloc de gauche est le lien vers le séjour. */
.cat-inv-stay {
  display: grid; gap: 0.15rem; align-content: start;
  color: inherit; text-decoration: none; border-radius: 8px;
}
.cat-inv-stay:hover { text-decoration: none; }
.cat-inv-guest { font-family: var(--db-font-display, inherit); font-size: 1.15rem; font-weight: 700; color: var(--db-tide-deep); }
.cat-inv-where, .cat-inv-mail { color: var(--db-ink-soft); font-size: 0.9rem; }
.cat-inv-go { color: var(--brand); font-size: 0.88rem; font-weight: 600; margin-top: 0.2rem; }
.cat-inv-stay:hover .cat-inv-go { text-decoration: underline; }
.cat-inv-stay:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
.cat-inv-meta .summary { margin: 0.5rem 0 0; }
.cat-inv-meta .summary dt { color: var(--db-ink-soft); font-size: 0.8rem; }
.cat-inv-meta .summary dd { margin: 0 0 0.35rem; font-variant-numeric: tabular-nums; }

@media (min-width: 60rem) {
  .cat-inv-head { grid-template-columns: 1fr auto; align-items: start; }
  /* Les faits de la facture alignés à droite, comme sur le document. */
  .cat-inv-meta { text-align: right; }
  .cat-inv-meta .summary { display: grid; grid-template-columns: auto auto; gap: 0.1rem 0.8rem; justify-content: end; }
  .cat-inv-meta .summary dt { text-align: right; }
  .cat-inv-meta .summary dd { text-align: right; }
}

/* Le pied de la facture : les totaux se lisent d'un bloc, séparés des lignes. */
.cat-inv-table tfoot td { font-weight: 600; }
.cat-inv-table tfoot tr:last-child td { font-size: 1.05rem; }
/* L'échéance d'un ménage, sur la ligne d'agenda. La plainte d'origine :
   « on ne voit pas la date de la prochaine réservation et donc le nombre de
   jours qu'on a pour gérer le ménage ». Discrète — c'est une contrainte, pas
   un statut — mais présente sur chaque ligne. */
.cc-agenda-item-deadline {
  display: block;
  font-size: 0.78rem;
  color: var(--db-ink-faint, var(--muted));
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------- grille mobile ---
   Sur un téléphone la colonne des noms prenait 250 px des 360 disponibles et,
   collée à gauche, ne les rendait jamais : il restait une colonne de jour et
   demie, et la grille ne servait à rien. Elle défile maintenant avec le reste,
   les noms courts (properties.short_name) tiennent en un mot, et les colonnes
   se resserrent — quatorze jours passent sur un écran de poche.
   La grille ne liste par ailleurs plus que les gîtes qui ont un ménage sur la
   période (voir rowHasTurnover) : c'est ce qui fait le plus pour la densité. */
@media (max-width: 700px) {
  .cc-grid-wrap { max-height: none; }
  .cc-propcell {
    position: static;
    padding: 0.3rem 0.4rem !important;
    max-width: 7rem;
  }
  .cc-propcell a { font-size: 0.78rem; }
  .cc-propcell a span:not(.row-thumb) { overflow: hidden; text-overflow: ellipsis; }
  .cc-propcell .row-thumb { display: none; }
  .cc-cell { width: 1.5rem; height: 1.9rem; }
  .cc-grid thead th { font-size: 0.62rem; padding: 0.3rem 0.15rem; }
}

/* ---------------------------------------------- choix du jour du ménage ---
   Des pastilles et non un <select> : elles sont deux à sept, on veut les voir
   toutes d'un coup, et la boîte de dialogue affecte en un clic — le jour se
   choisit avant, sans ouvrir puis refermer une liste. */
.assign-days { padding: 0.6rem 0.9rem 0.2rem; }
.assign-days-label {
  margin: 0 0 0.4rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--db-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.assign-days-list { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.assign-day-input { position: absolute; opacity: 0; pointer-events: none; }
.assign-day {
  display: inline-block;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--db-line);
  border-radius: 999px;
  background: var(--db-surface);
  color: var(--db-ink-soft);
  font-size: 0.8rem;
  cursor: pointer;
  text-transform: capitalize;
}
.assign-day:hover { border-color: var(--db-tide); }
.assign-day-input:checked + .assign-day {
  background: var(--db-tide);
  border-color: var(--db-tide);
  color: #fff;
  font-weight: 600;
}
/* Le focus clavier doit se voir : l'input qui le porte est masqué. */
.assign-day-input:focus-visible + .assign-day { outline: 2px solid var(--db-tide); outline-offset: 2px; }

/* ---------------------------------------------------------------- 08-sidebar.css */
/* ============================================================================
   Sidebar, second generation — six collapsible sections instead of twenty-two
   flat links, a "+ Nouveau" button holding the creation verbs that used to sit
   among them, a search field, and count badges. See cmd/desk/nav.go for why
   the places/actions split is the load-bearing idea; these are just its
   visual consequences. The rail itself (.sidebar) keeps its existing tide-deep
   treatment from the block above.
   ============================================================================ */

/* app.css styles `details` globally as a page-level disclosure: a hairline
   above, 1rem of space, a brand-colored summary. Both nav disclosures below
   are chrome, not content, so they opt out of all three. */
.sidebar details { margin: 0; border-top: none; padding-top: 0; }
.sidebar details > summary { margin-bottom: 0; color: inherit; }

/* --- "+ Nouveau" ---------------------------------------------------------- */
/* <details>, not a JS dropdown: it opens on click, closes on click, works
   with the keyboard, and needs no script. */
.nav-new { position: relative; }
.nav-new > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.6rem 0.9rem;
  border-radius: 8px;
  background: var(--db-sand);
  color: var(--db-tide-deep);
  font-family: var(--db-font-body);
  font-size: 0.9rem;
  font-weight: 700;
}
.nav-new > summary::-webkit-details-marker { display: none; }
.nav-new > summary:hover { background: #c79350; }
.nav-new-menu {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-top: 0.35rem;
  padding: 0.3rem;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.07);
}
.nav-new-menu a {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.45rem 0.6rem;
  border-radius: 6px;
  color: #c3d8db;
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
}
.nav-new-menu a:hover { background: rgba(255, 255, 255, 0.09); color: #fff; text-decoration: none; }

/* --- search --------------------------------------------------------------- */
.nav-search { position: relative; margin: 0; }
.nav-search > i {
  position: absolute;
  left: 0.7rem;
  top: 50%;
  transform: translateY(-50%);
  color: #6f95a0;
  font-size: 0.8rem;
  pointer-events: none;
}
.nav-search input[type="search"] {
  width: 100%;
  padding: 0.5rem 0.7rem 0.5rem 2rem;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
  font-family: var(--db-font-body);
  font-size: 0.85rem;
}
.nav-search input[type="search"]::placeholder { color: #7e9ea7; }
.nav-search input[type="search"]:focus {
  outline: none;
  border-color: var(--db-sand);
  background: rgba(255, 255, 255, 0.1);
}
.nav-search-results {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 0.35rem);
  z-index: 200;
  max-height: 60vh;
  overflow-y: auto;
  padding: 0.3rem;
  border: 1px solid var(--db-line);
  border-radius: 10px;
  background: var(--db-surface);
  box-shadow: var(--db-shadow-md);
}
.nav-search-group {
  padding: 0.45rem 0.6rem 0.2rem;
  font-family: var(--db-font-body);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--db-ink-faint);
}
.nav-search-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  border-radius: 6px;
  color: var(--db-ink);
  font-family: var(--db-font-body);
  text-decoration: none;
}
.nav-search-item:hover,
.nav-search-item.is-active { background: var(--db-tide-mist); text-decoration: none; }
.nav-search-item > i { flex: none; width: 1.1em; text-align: center; color: var(--db-tide-mid); font-size: 0.85rem; }
.nav-search-item-text { min-width: 0; display: flex; flex-direction: column; }
.nav-search-item-label { font-size: 0.88rem; font-weight: 600; }
.nav-search-item-sub {
  font-size: 0.76rem;
  color: var(--db-ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nav-search-empty { margin: 0; padding: 0.6rem; font-size: 0.85rem; color: var(--db-ink-soft); }

/* On /plus and /search the field sits on the page's stone background, not in
   the dark rail, so it needs the light-surface treatment instead. */
.nav-search-page { margin-bottom: 1.25rem; }
.nav-search-page input[type="search"] {
  border-color: var(--db-line);
  background: var(--db-surface);
  color: var(--db-ink);
}
.nav-search-page input[type="search"]::placeholder { color: var(--db-ink-faint); }
.nav-search-page > i { color: var(--db-ink-faint); }
.search-page-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.75rem; }
.search-page-form .field { flex: 1 1 22rem; margin: 0; }

/* --- collapsible sections ------------------------------------------------- */
/* The 1.4rem gap set earlier in this file was tuned for the flat rail, where
   it separated five labelled groups of plain links. With six <details> that
   each already read as a distinct block, it just pushed the last two sections
   below the fold. */
.sidebar nav { gap: 0.15rem; }
.sidebar { gap: 1.1rem; }
.nav-group > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  color: #c3d8db;
  font-family: var(--db-font-body);
  font-size: 0.9rem;
  font-weight: 600;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
.nav-group > summary > i:first-child { opacity: 0.85; width: 1.1em; text-align: center; }
.nav-group-title { flex: 1; }
/* Disclosure chevron, drawn after the title so the badge sits at the far
   right where it is scanned, not sandwiched between text and arrow. */
.nav-group > summary::after {
  content: "\f078"; /* fa-chevron-down */
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  font-size: 0.6rem;
  opacity: 0.5;
  transition: transform 0.15s ease;
}
.nav-group[open] > summary::after { transform: rotate(180deg); }
.nav-group[open] > summary { color: #fff; }
.nav-group-links { display: flex; flex-direction: column; gap: 0.1rem; padding: 0.15rem 0 0.4rem 0.5rem; }
.nav-group-links a {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.4rem 0.7rem;
  border-radius: 6px;
  border-left: 2px solid transparent;
  color: #a8c4c9;
  font-size: 0.84rem;
  font-weight: 500;
  text-decoration: none;
}
.nav-group-links a > span:not(.nav-badge) { flex: 1; }
.nav-group-links a > i { opacity: 0.8; width: 1.05em; text-align: center; font-size: 0.8rem; }
.nav-group-links a:hover { background: rgba(255, 255, 255, 0.06); color: #fff; text-decoration: none; }
.nav-group-links a.active {
  background: rgba(255, 255, 255, 0.09);
  color: #fff;
  border-left-color: var(--db-sand);
}

/* --- count badges --------------------------------------------------------- */
/* Sand, not red: these are "there is work here", not "something is broken".
   A section with nothing pending renders no badge at all — that silence is
   the whole mechanism, since it is what lets six headings be scanned in one
   pass instead of read. */
.nav-badge {
  flex: none;
  min-width: 1.35rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--db-sand);
  color: var(--db-tide-deep);
  font-family: var(--db-font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1.35rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* The property tab strip that lived here is gone: it is design.css's shared
   .tabs component now, so a gîte's sub-pages, a customer record's panels and
   the settings pages all use one control instead of three lookalikes. See
   that file's "Tabs" header for the reasoning, including why it no longer
   scrolls horizontally. */


/* A section flattened to a single link (see nav.html): same metrics as a
   .nav-group summary so the rail's rows stay on one rhythm. */
.nav-flat {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  color: #c3d8db;
  font-family: var(--db-font-body);
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
}
.nav-flat > span:not(.nav-badge) { flex: 1; }
.nav-flat > i:first-child { opacity: 0.85; width: 1.1em; text-align: center; }
.nav-flat:hover { background: rgba(255, 255, 255, 0.06); color: #fff; text-decoration: none; }
.nav-flat.active { background: rgba(255, 255, 255, 0.09); color: #fff; }

/* ---------------------------------------------------------------- 09-rates.css */
/* ============================================================================
   /rates/recap — the bulk rate grid. One row per gîte, one column per day,
   drag-select a rectangle and type a price to apply it to all of it.

   Moved here from an inline <style> in rates_recap.html: it is ~40 rules on a
   page served on every visit, and inline it could be neither cached nor
   gzipped alongside the rest.

   The design problem this solves: fourteen columns of the SAME number is
   what a healthy rate grid mostly looks like, and reading it told you
   nothing — you had to compare fourteen values to discover that nothing
   changed. So the grid now encodes the two things that matter and lets the
   digits recede:

     · a per-row heat band (.l0–.l4), scaled to that gîte's own min/max, so
       the seasonal shape of a row is visible as a gradient without reading;
     · a boundary line (.is-change) wherever the price differs from the day
       before, so period edges are the thing your eye lands on.

   A row whose price never moves therefore renders completely flat and quiet,
   which is the honest picture.
   ============================================================================ */
.rr-toolbar { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.rr-nav { display: flex; gap: 0.4rem; align-items: center; }
.rr-nav a.btn-small { margin-top: 0; }
.rr-range {
  color: var(--muted);
  font-family: var(--db-font-mono);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}
.rr-days-select {
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  font-size: 0.85rem;
  background: var(--panel-bg);
}
.rr-note { font-size: 0.82rem; color: var(--muted); margin: 0.25rem 0 1.25rem; max-width: 62ch; }
.rr-empty-groups { margin-top: 1.25rem; }
.rr-empty-groups i { color: var(--db-ink-faint); margin-right: 0.35rem; }

.rr-panel { padding-bottom: 0.75rem; }
/* L'en-tête colle en haut du panneau : sur dix-sept lignes, les flèches et
   « Aujourd'hui » sortaient du champ au moment précis où on lit la grille et
   où l'on décide de changer de semaine. Il se pose au-dessus de l'en-tête du
   tableau, qui colle lui aussi — d'où le z-index plus haut. */
.rr-group-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.6rem;
  margin-bottom: 0.6rem;
  position: sticky; top: 0; z-index: 5;
  background: var(--panel-bg);
  padding: 0.4rem 0 0.5rem;
}
.rr-group-head h2 { margin: 0; }
.rr-group-count { font-size: 0.8rem; color: var(--db-ink-faint); font-family: var(--db-font-mono); }

.rr-grid { border-collapse: separate; border-spacing: 0; user-select: none; }
.rr-grid th, .rr-grid td { border: none; padding: 0; text-align: center; }

/* --- header ---------------------------------------------------------------
   Weekday initial above the day number: rates are largely a weekday/weekend
   story, so the letter is the label that carries the information. The month
   prints only on the first column and each 1st — repeated above all fourteen
   it was pure noise. Sticky so a long list stays orientable. */
.rr-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--panel-bg);
  border-bottom: 2px solid var(--border);
  padding: 0.35rem 0.15rem 0.3rem;
  font-weight: 600;
  line-height: 1.15;
}
.rr-day { min-width: 2.5rem; }
.rr-day-wd {
  display: block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--db-ink-faint);
}
.rr-day-num {
  display: block;
  font-family: var(--db-font-mono);
  font-size: 0.8rem;
  color: var(--db-ink);
  font-variant-numeric: tabular-nums;
}
.rr-day-month {
  display: block;
  height: 0.85rem;
  font-size: 0.6rem;
  color: var(--db-sand-deep);
  font-weight: 600;
  white-space: nowrap;
}
.rr-day.is-weekend .rr-day-wd { color: var(--db-sand-deep); }
.rr-day.is-weekend .rr-day-num { color: var(--db-sand-deep); }
.rr-day.is-today .rr-day-num {
  background: var(--db-tide);
  color: #fff;
  border-radius: 999px;
  display: inline-block;
  min-width: 1.35rem;
  padding: 0 0.15rem;
}

/* --- property column ------------------------------------------------------ */
.rr-propcell {
  text-align: left !important;
  padding: 0.3rem 0.8rem 0.3rem 0.2rem !important;
  white-space: nowrap;
  background: var(--panel-bg);
  position: sticky;
  left: 0;
  z-index: 3; /* above the sticky header row's own cells when both stick */
}
thead .rr-propcell {
  z-index: 4;
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--db-ink-faint);
}
.rr-propcell a { display: flex; align-items: center; gap: 0.55rem; color: var(--text); text-decoration: none; }
.rr-propcell a:hover .rr-prop-name { color: var(--brand); text-decoration: underline; }
.rr-propcell .row-thumb { width: 30px; height: 30px; flex: none; border-radius: 6px; }
.rr-prop-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.rr-prop-name { font-weight: 600; font-size: 0.85rem; }
/* The row summary: "57 €" flat, "57–70 €" varying. Answers "what is this
   gîte doing over the window" without scanning fourteen cells. */
.rr-prop-range {
  font-family: var(--db-font-mono);
  font-size: 0.7rem;
  color: var(--db-tide-mid);
  font-variant-numeric: tabular-nums;
}
.rr-prop-range.is-flat { color: var(--db-ink-faint); }
.rr-prop-gap { color: var(--db-critical); }

/* --- cells ---------------------------------------------------------------- */
.rr-cell {
  width: 2.5rem;
  height: 2rem;
  cursor: cell;
  font-family: var(--db-font-mono);
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  color: var(--db-ink);
  border-bottom: 1px solid var(--db-line-soft);
  transition: background-color 0.1s ease;
}
/* Heat bands, per row. l0 is deliberately bare: a flat row gets no tint at
   all, so "nothing happens here" reads instantly. */
.rr-cell.l0 { background: transparent; }
.rr-cell.l1 { background: #eef5f6; }
.rr-cell.l2 { background: var(--db-tide-mist); }
.rr-cell.l3 { background: var(--db-tide-mist-2); }
.rr-cell.l4 { background: #a8ccd2; }
.rr-cell.is-empty { color: var(--db-ink-faint); background: repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--db-stone-deep) 3px, var(--db-stone-deep) 4px); }
/* The boundary: where the price changed from the day before. */
.rr-cell.is-change { box-shadow: inset 2px 0 0 var(--db-sand); }
.rr-grid tbody tr:hover .rr-cell { background-color: rgba(28, 74, 88, 0.045); }
.rr-cell.selected {
  background: var(--db-tide) !important;
  color: #fff;
  box-shadow: inset 0 0 0 1px var(--db-tide-deep);
}

/* Une nuit déjà vendue : hors du jeu, et visiblement. Grisée plutôt que
   masquée — le prix reste une information utile, c'est l'action qui n'a plus
   de sens — et rayée en diagonale pour que l'œil la distingue d'une teinte
   basse. Le curseur le dit avant le clic. */
.rr-cell.is-sold {
  color: var(--db-ink-faint);
  cursor: not-allowed;
  background-image: repeating-linear-gradient(
    45deg, rgba(23, 35, 42, 0.10) 0 3px, transparent 3px 7px) !important;
}
.rr-painting .rr-cell.is-sold { cursor: not-allowed; }
.rr-legend .k-sold {
  display: inline-block; width: 0.9rem; height: 0.9rem; vertical-align: -2px;
  border: 1px solid var(--db-line); border-radius: 3px;
  background-image: repeating-linear-gradient(
    45deg, rgba(23, 35, 42, 0.18) 0 3px, transparent 3px 7px);
}

/* (2) Aller à une date, et (3) l'ancre humaine du bandeau. */
.rr-window-label {
  font-size: 0.82rem; font-weight: 600; color: var(--db-ink-soft);
  margin-right: 0.2rem; white-space: nowrap;
}
.rr-window-jump { margin: 0; }
.rr-window-jump input[type="date"] {
  height: 2rem; padding: 0 0.4rem; font-size: 0.8rem; min-width: 0; width: 8.5rem;
}

/* (3b) La colonne d'aujourd'hui : une bande claire, pas une couleur — la
   teinte des cellules porte déjà le prix, et deux informations dans la même
   propriété visuelle se détruisent. */
.rr-cell.is-today { box-shadow: inset 1px 0 0 var(--db-tide), inset -1px 0 0 var(--db-tide); }
.rr-grid tbody tr:last-child .rr-cell.is-today { box-shadow: inset 1px 0 0 var(--db-tide), inset -1px -1px 0 var(--db-tide); }

/* (4) La sélection se lit comme une plage : extrémités arrondies, milieu plus
   clair. Un aplat uniforme rendait cinq clics séparés identiques à un glissé
   de cinq jours — or c'est la plage qu'on valide ensuite. */
.rr-cell.selected { border-radius: 0; }
.rr-cell.selected.is-range-start { border-radius: 999px 0 0 999px; }
.rr-cell.selected.is-range-end { border-radius: 0 999px 999px 0; }
.rr-cell.selected.is-range-start.is-range-end { border-radius: 999px; }
/* Le milieu prend la teinte moyenne du jeton, pas un color-mix : une seule
   déclaration, aucun support à supposer, et le repli est le jeton lui-même. */
.rr-cell.selected.is-range-mid { background: var(--db-tide-mid, #2c6274) !important; }

/* (5) Le bord droit de la colonne gelée : une ombre, pour qu'elle se lise
   comme une couche posée au-dessus et non comme une couture du tableau. */
.rr-propcell { box-shadow: 6px 0 8px -6px rgba(23, 35, 42, 0.28); }

/* (6) La légende vit avec la grille qu'elle explique. */
.rr-panel .rr-legend { margin: 0 0 0.6rem; }

/* ---- La fenêtre de la grille ---------------------------------------------
   Reculer / Aujourd'hui / Avancer, et la largeur de fenêtre, dans l'en-tête du
   tableau qu'ils commandent. En haut de page ils passaient pour un réglage de
   la page entière, et ils sortaient du champ dès qu'on faisait défiler la
   grille — or c'est en la lisant qu'on veut changer de semaine. */
.rr-window { margin-left: auto; display: flex; align-items: center; gap: 0.35rem; }
.rr-window-step,
.rr-window-today {
  display: inline-flex; align-items: center; justify-content: center;
  height: 2rem; padding: 0 0.7rem; border-radius: 999px;
  border: 1px solid var(--db-line); background: var(--db-surface);
  color: var(--db-ink-soft); text-decoration: none; font-size: 0.82rem; font-weight: 600;
}
.rr-window-step { width: 2rem; padding: 0; }
.rr-window-step:hover,
.rr-window-today:hover { background: var(--db-surface-2); color: var(--db-ink); text-decoration: none; }
/* Déjà sur aujourd'hui : le bouton reste là — sa disparition ferait sauter les
   deux flèches d'une position — mais il ne se clique pas et le dit. */
.rr-window-today.is-current {
  color: var(--db-ink-faint); background: var(--db-surface-2);
  border-style: dashed; cursor: default;
}
.rr-window-days { margin: 0; }
.rr-window-days select { height: 2rem; padding: 0 0.5rem; font-size: 0.82rem; min-width: 0; }
@media (max-width: 700px) {
  .rr-window { width: 100%; margin-left: 0; justify-content: space-between; }
}

/* ---- La palette de modèles et le pinceau ---------------------------------
   Un modèle est une pastille de couleur portant son nom et ce qu'il pose. Armé,
   il change le curseur sur toute la grille : le geste n'est plus « sélectionner
   puis dire », c'est « peindre ». La couleur reste sur les cases peintes tant
   que le brouillon n'est pas appliqué — c'est ce qui rend visible d'un coup
   d'œil ce qu'on vient de poser et où. */
.rr-palette { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-start; }
.rr-brush {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.8rem; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--db-line); background: var(--db-surface);
  font: inherit; font-size: 0.85rem; color: var(--db-ink);
}
.rr-brush:hover { border-color: var(--brush, var(--db-ink-faint)); }
/* Armé : la pastille s'inverse. Une simple bordure colorée se confondait avec
   le survol — et l'état « pinceau en main » doit se lire depuis l'autre bout
   de l'écran, puisque c'est lui qui change ce que fait un clic sur la grille.
   Pas de color-mix ici : la teinte vient de la palette, le contraste doit
   tenir sans dépendre du support du navigateur. */
.rr-brush.is-armed {
  background: var(--brush); border-color: var(--brush); color: #fff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
}
.rr-brush.is-armed .rr-brush-value { color: rgba(255, 255, 255, 0.85); }
.rr-brush.is-armed .rr-brush-dot {
  background: #fff; box-shadow: inset 0 0 0 2px var(--brush);
}
.rr-brush-dot {
  width: 0.85rem; height: 0.85rem; border-radius: 50%;
  background: var(--brush, var(--db-ink-faint)); flex: none;
}
.rr-brush-dot-add {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--db-surface-2); color: var(--db-ink-soft); font-weight: 700;
}
.rr-brush-name { font-weight: 600; }
.rr-brush-value { color: var(--db-ink-soft); font-variant-numeric: tabular-nums; }
.rr-brush-new summary { list-style: none; }
.rr-brush-new summary::-webkit-details-marker { display: none; }
.rr-brush-form {
  display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: end;
  margin-top: 0.6rem; padding: 0.7rem; border: 1px solid var(--db-line);
  border-radius: 10px; background: var(--db-surface-2);
}
.rr-brush-form .field { margin: 0; }
.rr-brush-form input, .rr-brush-form select { min-width: 0; width: 10rem; }
.rr-brush-hint { margin-top: 0.6rem; font-weight: 600; }

/* Pinceau en main : le curseur le dit sur toute la grille. */
.rr-painting .rr-cell { cursor: crosshair; }

/* Une case peinte porte la couleur du modèle jusqu'à l'application du lot. */
.rr-cell.rr-painted {
  box-shadow: inset 0 0 0 2px var(--brush);
  background: var(--brush) !important;
  color: #fff;
}
/* Une case en attente sans pinceau (prix tapé à la main) : même idée, teinte
   neutre — l'ancien prix barré, le nouveau en gras, posés par le script. */
.rr-cell.rr-pending { box-shadow: inset 0 0 0 2px var(--db-ink-faint); }
.rr-cell s { opacity: 0.5; }

/* La barre du brouillon, ancrée en bas : ce qui attend d'être relu ne doit pas
   dépendre du défilement. */
.rr-draftbar {
  position: sticky; bottom: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  margin-top: 1rem; padding: 0.7rem 1rem;
  background: var(--db-ink); color: var(--db-surface); border-radius: 10px;
}
.rr-draftbar a { color: var(--db-surface); text-decoration: underline; }

/* Le popover d'édition : les modes sur leur propre ligne (voir la règle
   .rr-popover d'origine, plus bas, qui porte la largeur et la direction). */
.rr-modes { display: flex; flex-wrap: wrap; gap: 0.5rem; font-size: 0.8rem; }
.rr-modes label { display: inline-flex; align-items: center; gap: 0.25rem; margin: 0; }
.rr-modes input[type="radio"] { margin: 0; }
.rr-popover input[type="number"] { width: 100%; }

/* --- bulk-edit popover ---------------------------------------------------- */
.rr-popover {
  position: fixed;
  z-index: 50;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  box-shadow: var(--db-shadow-md);
  padding: 0.6rem;
  display: none;
  gap: 0.4rem;
  /* En colonne et de largeur déclarée depuis que le popover porte les quatre
     modes d'édition : en ligne, il sortait de l'écran dès qu'on cliquait une
     case des derniers jours affichés, et le clic semblait sans effet. */
  flex-direction: column;
  align-items: stretch;
  width: 15rem;
}
.rr-popover input {
  width: 6.5rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 6px);
  font-family: var(--db-font-mono);
}

/* --- legend --------------------------------------------------------------- */
.rr-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin: 0.6rem 0 0;
  font-size: 0.72rem;
  color: var(--db-ink-faint);
}
.rr-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.rr-legend i { width: 13px; height: 13px; border-radius: 3px; display: inline-block; border: 1px solid var(--db-line); }
/* The swatches reuse the band names but not the .rr-cell selector, so the
   fills have to be restated here. */
.rr-legend i.l1 { background: #eef5f6; }
.rr-legend i.l2 { background: var(--db-tide-mist); }
.rr-legend i.l3 { background: var(--db-tide-mist-2); }
.rr-legend i.l4 { background: #a8ccd2; }
.rr-legend i.k-change { border-color: var(--db-line); box-shadow: inset 3px 0 0 var(--db-sand); background: var(--db-surface); }
.rr-legend i.k-empty { background: repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--db-stone-deep) 3px, var(--db-stone-deep) 4px); }
.rr-legend .rr-legend-ramp { gap: 0.15rem; }

@media (max-width: 640px) {
  /* Drag-select needs a mouse; on a phone this is a read-only view, so the
     cells can shrink to fit more of the fortnight on screen. */
  .rr-cell { width: 2.1rem; height: 1.8rem; font-size: 0.7rem; }
  .rr-propcell { max-width: 9.5rem; }
  .rr-prop-name { font-size: 0.78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .rr-propcell .row-thumb { width: 22px; height: 22px; }
}

/* .pdf-preview-inline — the document header rendered as the top of the page's
   own panel rather than as a separate sheet of "paper". Used on
   /invoices/{id}, where the header introduces the frozen line table below it;
   the standalone paper card (.pdf-preview) stays for the previews on
   /owners/{id}/invoices/pending, where each document is
   one item in a list and needs its own edges. */
.pdf-preview-inline {
  max-width: none;
  padding: 0 0 1rem;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  box-shadow: none;
  background: none;
  margin-bottom: 1.25rem;
}
.pdf-preview-period { margin: 0.75rem 0 0; color: var(--muted); font-size: 0.9rem; }

/* Pending-invoice previews: the convert button sits under its document. */
.pending-invoice-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1rem;
}
.pending-invoice-actions form { margin: 0; }
.pending-invoice-actions .field-hint { flex: 1 1 22rem; margin: 0; }

/* ---------------------------------------------------------------- 10-cards-forms.css */
/* ============================================================================
   .cta-card — the one thing a panel invites you to DO, given room to be seen.
   Introduced for /owners/{id}'s "à facturer" block, where the action was a
   bare button pinned between the section heading and a two-line hint, with a
   table starting immediately below: three different kinds of content touching,
   and the actionable one indistinguishable from the prose.
   Leads with the figure so it informs even unclicked.
   ============================================================================ */
.cta-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.15rem;
  margin: 0.25rem 0 1.75rem;
  border: 1px solid var(--db-line);
  border-left: 3px solid var(--db-sand);
  border-radius: var(--radius-md, 10px);
  background: var(--db-surface-2);
}
.cta-card-icon {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--db-sand-mist);
  color: var(--db-sand-deep);
  font-size: 1.05rem;
}
.cta-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.cta-card-title { font-family: var(--db-font-display); font-size: 1.15rem; font-weight: 600; color: var(--db-ink); }
.cta-card-sub { font-size: 0.85rem; color: var(--db-ink-soft); max-width: 60ch; }
.cta-card-btn { flex: none; margin: 0; }

/* Nothing to do: same shape so the panel does not jump between states, but
   drained of the colour that means "this wants you". */
.cta-card.is-quiet { border-left-color: var(--db-line); background: none; }
.cta-card.is-quiet .cta-card-icon { background: var(--db-ok-mist); color: var(--db-ok); }
.cta-card.is-quiet .cta-card-title { font-size: 1rem; }

@media (max-width: 640px) {
  .cta-card { flex-wrap: wrap; }
  .cta-card-btn { width: 100%; text-align: center; }
}

/* A heading INSIDE a panel, below a leading block — marks where the actions
   stop and the record starts.
   
   The top margin was zero, from when this only ever followed a panel's own
   intro paragraph. It now follows tables — the import review stacks three of
   them — and a heading flush against the table above reads as that table's
   footer rather than as the next section's title. Two rem is the least that
   separates them; the bottom stays tight so the heading belongs to what
   follows it, which is the whole job of a subhead. */
.panel-subhead {
  font-family: var(--db-font-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--db-ink-soft);
  margin: 2rem 0 0.5rem;
}
/* Except when it opens the panel: there is nothing above to separate from. */
.panel > .panel-subhead:first-child,
.panel-subhead:first-child { margin-top: 0; }

/* ============================================================================
   .table-form — a table whose cells hold form controls, sized to the page
   instead of to the controls.

   app.css sets `min-width: 200px` on every form input, which is right for a
   stacked form and wrong inside a table: the columns could neither shrink nor
   grow. On /properties/{id}/channels that meant the grid was 801px wide inside
   a 761px column at 1024px (spilling past the content edge), while at 1440px
   it used 922px of the 1100px available and still clipped its own placeholder.

   `table-layout: fixed` plus a <colgroup> hands the sizing back to the page:
   the columns are shares of whatever width there is, and the controls fill
   their column. Pairs with .table-cards, which takes over below 640px where
   the table stops being a table at all.
   ============================================================================ */
.table-form { table-layout: fixed; width: 100%; }
.table-form input:not([type="checkbox"]):not([type="radio"]),
.table-form select,
.table-form textarea {
  width: 100%;
  min-width: 0;
  max-width: 100%;
}
/* Long values (listing ids, URLs) shouldn't force a column open. */
.table-form td { overflow-wrap: anywhere; }

/* Channel mapping columns: the external listing id gets the most room because
   it holds the longest values; the status select needs the least. */
.table-form .col-channel { width: 24%; }
.table-form .col-roomid  { width: 22%; }
.table-form .col-listing { width: 32%; }
.table-form .col-status  { width: 22%; }

@media (max-width: 640px) {
  /* Cards: the colgroup and fixed layout no longer apply, and .cell-form
     (design.css) already gives each control the full card width. */
  .table-form { table-layout: auto; }
}

/* Une maison, telle qu'on la propose : la photo d'abord.
   Une case à cocher dans un tableau de noms demande de connaître les dix-sept
   par cœur ; la vignette dit en un regard lequel on est en train d'offrir. Le
   même bloc sert sur l'écran de sélection (choix multiple, coché) et sous le
   formulaire de devis (choix unique, cliqué), parce que c'est la même
   question posée deux fois. */
.gite-cards {
  display: grid;
  /* 19rem, pas 15 : une carte porte une vignette, un nom de maison entier,
     deux lignes de détail et un bouton. En dessous, c'est le nom qui paie la
     place — et « Gîte La Maison Mer au Guilvinec » sur quatre lignes ne se
     lit plus. */
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 12px;
  margin-top: 12px;
}
.gite-card {
  display: flex;
  gap: 10px;
  padding: 10px;
  border: 1px solid var(--line, #e2ddd0);
  border-radius: 10px;
  background: var(--surface, #fff);
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  width: 100%;
}
/* Le desk habille TOUT <button> : fond de marque, texte blanc, contenu centré,
   marge haute et survol sombre. Une carte est un bouton par sa fonction, pas
   par son allure, et ces quatre-là se défont ici.

   Le contenu centré est ce qui décalait les cartes les unes par rapport aux
   autres : chacune centrait le sien, donc chacune plaçait sa vignette à une
   abscisse différente. Le survol sombre, lui, peignait du texte foncé sur du
   foncé — la carte devenait illisible au passage de la souris. */
.gite-card { justify-content: flex-start; margin: 0; }
.gite-card:hover { background: var(--brand-light); border-color: var(--tide, #1c4a58); }
.gite-card:focus-within,
.gite-card:focus-visible { outline: 2px solid var(--tide, #1c4a58); outline-offset: 2px; }
/* Cochée : la maison est dans la sélection. Les deux écrans qui montrent ces
   cartes cochent de la même façon, et le disent donc de la même façon. */
.gite-card:has(input:checked) {
  border-color: var(--tide, #1c4a58);
  box-shadow: inset 0 0 0 1px var(--tide, #1c4a58);
}
/* La vignette en haut, le prix en bas. Une rangée de la grille a la hauteur
   de sa carte la plus haute ; sans cela un nom qui passe sur deux lignes
   décale le prix de toutes ses voisines. */
.gite-card-thumb {
  width: 64px; height: 64px; flex: 0 0 64px; align-self: flex-start;
  object-fit: cover; border-radius: 8px; background: var(--stone-deep, #e7e3d6);
}
.gite-card-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.gite-card-name { font-weight: 600; font-size: 0.92rem; line-height: 1.25; }
.gite-card-meta { font-size: 0.8rem; color: var(--muted); }
/* Le prix et l'action sur une même ligne, en bas de la carte : le bouton posé
   à droite du texte lui prenait sa largeur, et le nom de la maison — la seule
   chose qu'on lit vraiment — se cassait sur trois lignes pour lui faire
   place. En pied de carte il ne coûte rien au nom, et les prix de toute une
   rangée se lisent à la même hauteur. */
.gite-card-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; margin-top: auto; padding-top: 4px;
}
.gite-card-price { font-size: 0.88rem; font-weight: 600; }
.gite-card-price .muted { font-weight: 400; }
/* La case en haut, à hauteur du nom — au milieu du flanc gauche elle ne
   désigne plus rien. */
.gite-card input[type="checkbox"] { flex: 0 0 auto; align-self: flex-start; margin: 3px 0 0; }
.gite-cards-label { margin: 14px 0 0; font-size: 0.85rem; }

/* ============================================================================
   .stay-grid — le séjour sur une ligne : la maison, les dates, qui vient.
   Empilés en colonne dans des champs plafonnés à 340 px, ces quatre-là
   prenaient trois hauteurs et un tiers de la largeur pour dire une seule
   chose. Ils décrivent un même objet, ils tiennent sur un même rang, et
   chacun occupe sa part de l'écran plutôt qu'une largeur fixe.
   ============================================================================ */
.stay-grid {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.6rem 1.2rem;
}
.stay-grid > * { flex: 1 1 11rem; min-width: 0; margin: 0; display: block; }
.stay-grid > .stay-gite { flex: 2 1 18rem; }
.stay-grid > .daterange { flex: 1.5 1 14rem; }

/* Uniquement les commandes propres à une case : un combinateur d'ENFANT, et
   jamais de descendant.

   Les deux <input type="date"> du sélecteur vivent un niveau plus bas, et
   c'est daterange.js qui les cache (.daterange-js input[type="date"]). Un
   sélecteur de descendant écrit ici a exactement la même spécificité que le
   sien ; venant plus loin dans la feuille, il l'emportait et remettait à
   l'écran les deux champs date et l'étiquette « Date de départ ». La case des
   dates devenait trois fois plus haute que ses voisines, et align-items:
   flex-end faisait alors couler tout le reste de la rangée vers le bas. */
.stay-grid > .field > input,
.stay-grid > .field > select {
  display: block;
  width: 100%;
  max-width: none;
  margin-top: 0.25rem;
}
/* Le déclencheur du calendrier tient la place d'une commande : bloc, pleine
   largeur, aligné en bas comme les listes voisines. */
.stay-grid > .daterange > label { margin: 0; }
.stay-grid > .daterange > .daterange-trigger { display: flex; width: 100%; margin: 0.25rem 0 0; }
.stay-grid-hint { margin: 0.6rem 0 0; }

/* Le geste secondaire de la carte : cocher met la maison dans la sélection,
   ce bouton-là bascule le devis dessus. Deux gestes distincts et tous deux
   visibles, plutôt qu'un seul clic qui devrait deviner lequel. */
.gite-card-quote { flex: 0 0 auto; }

/* Composer la sélection sans quitter l'écran : le mot pour le client et
   l'envoi, sous les cartes qu'on vient de cocher. */
.choice-compose { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line, #e2ddd0); }
.choice-compose textarea { width: 100%; max-width: 60ch; }
.choice-compose .muted { font-size: 0.82rem; }

/* ---------------------------------------------------------------- 11-plus.css */
/* ============================================================================
   /plus — the one index of every page, at both widths (see templates/
   plus.html and cmd/desk/plus.go; /sitemap redirects here).

   The phone keeps exactly what it had: a single column of .action-tile rows,
   section title, nothing else. Everything below that came from the old
   /sitemap — section icons, the per-page "Guide :" line, the /docs/*
   reference block — is .plus-desk-only and only exists from 900px up, the
   breakpoint where the sidebar replaces the bottom tab bar (app.css). Two
   readings of one registry: a launcher you thumb through on site, a table of
   contents you read at a desk.
   ============================================================================ */
/* Scoped under .plus-index for the specificity, not for the scope: Font
   Awesome's stylesheet is the last one plus.html loads and its
   .fa-solid{display:inline-block} would otherwise out-order a bare
   .plus-desk-only{display:none} and leave the section icons on the phone. */
.plus-index .plus-desk-only { display: none; }
/* Reproduces the inline margin the section headings used to carry, so the
   phone's spacing is untouched now that the desktop block can override it. */
.plus-index h2 { margin-top: 1.5rem; }

@media (min-width: 900px) {
  /* Spelled out per element rather than `display: revert`: these are <i>,
     <span>, <p> and <section> alike, and the <i> needs Font Awesome's own
     inline-block back, which reverting to the UA default would not give it. */
  .plus-index i.plus-desk-only { display: inline-block; }
  .plus-index span.plus-desk-only { display: inline; }
  .plus-index p.plus-desk-only,
  .plus-index section.plus-desk-only { display: block; }
  .plus-index .plus-mob-only { display: none; }

  .plus-index { max-width: 1180px; }
  .plus-index > h1 { margin-bottom: 0.4rem; }
  .plus-index > p.muted { max-width: 68ch; }

  /* A page-level search field, not a 220px rail field. */
  .plus-index .nav-search-page { max-width: 560px; margin: 1.4rem 0 2rem; }

  /* Section heading as a labelled rule: small caps, brand icon, hairline —
     the sections are signposts here, not tappable targets, so they step back
     and let the tiles carry the weight. */
  .plus-index h2 {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    margin: 2.25rem 0 0.9rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--border);
    font-size: 0.92rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .plus-sec-icon { color: var(--brand); font-size: 0.95rem; }

  /* Columns instead of one long ladder: 25 pages down a single column is a
     scroll, the same 25 in three columns is a map you can take in at once. */
  .plus-index .action-tile-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
    gap: 0.75rem;
    margin-bottom: 0;
    /* start, not the default stretch: the grid item is the wrapper, and
       stretching it would make a tile WITHOUT a guide line grow taller than
       its neighbours by exactly the height of the line they have — the one
       arrangement that reads as a mistake. Each tile takes its content's
       height instead, and its guide line sits right under it. */
    align-items: start;
  }
  .plus-entry { display: flex; flex-direction: column; }

  .plus-guide {
    margin: 0.25rem 0 0;
    /* Aligned with the tile's label: icon (40px) + gap (0.9rem) + the tile's
       own left padding (1rem). */
    padding-left: calc(40px + 1.9rem);
    font-size: 0.8rem;
    color: var(--muted);
  }
  .plus-guide i { margin-right: 0.35rem; color: var(--brand); }
  .plus-guide a { color: var(--muted); }
  .plus-guide a:hover { color: var(--brand); }

  /* Reference block: same tiles, deliberately quieter — grey icon chips
     rather than brand ones, so the eye reads "documentation" before it reads
     any single title. */
  .plus-docs { margin-top: 2.75rem; }
  .plus-docs > p.muted { max-width: 68ch; }
  .plus-docs-list { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
  .plus-docs .action-tile-icon {
    background: var(--db-stone-deep, #e7e3d6);
    color: var(--muted);
  }
  .plus-docs .action-tile:hover .action-tile-icon {
    background: var(--brand-light);
    color: var(--brand);
  }
}

/* ---------------------------------------------------------------- 12-home.css */
/* ============================================================================
   Home page, second generation — three bands.

   Replaces five KPI tiles (two of them configuration state reading 0 since the
   account opened) above a flat nine-row list where every row wore a permanent
   red or amber left bar. Colouring by CATEGORY meant six rows of nine were
   always lit, so the colour had stopped meaning anything — which is what made
   the list read as noise. Severity now comes from lateness, and a domain with
   nothing urgent renders visibly calm.
   ============================================================================ */
.db-hero-summary {
  margin: 0.35rem 0 0;
  font-size: 0.95rem;
  color: var(--db-ink-soft);
  font-family: var(--db-font-body);
}

/* ---- band 1: Aujourd'hui ------------------------------------------------- */
.db-today {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1px;
  background: var(--db-line);
  border: 1px solid var(--db-line);
  border-radius: 14px;
  overflow: hidden;
  box-shadow: var(--db-shadow-sm);
  margin-bottom: 2.25rem;
  font-family: var(--db-font-body);
}
.db-slot { background: var(--db-surface); padding: 1rem 1.15rem; display: flex; flex-direction: column; gap: 0.55rem; min-width: 0; }
.db-slot-head {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--db-ink-faint);
}
.db-slot-head b {
  margin-left: auto;
  font-family: var(--db-font-mono);
  font-size: 1.05rem;
  color: var(--db-ink);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.db-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: none; }
.db-dot.arrivals { background: var(--db-ok); }
.db-dot.departures { background: var(--db-tide-mid); }
.db-dot.turnovers { background: var(--db-sand); }
.db-dot.catering { background: var(--db-stone); }
.db-slot-item { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; }
/* Whole-row link to the booking. Negative margin lets the hover tint bleed to
   the card padding so the target reads as the full row, not just the text. */
a.db-slot-item.is-link {
  padding: 0.25rem 0.55rem; margin: 0 -0.55rem;
  border-radius: 6px; color: inherit; text-decoration: none;
  transition: background-color 0.12s ease;
}
a.db-slot-item.is-link:hover { background: var(--db-tide-mist); text-decoration: none; }
a.db-slot-item.is-link:hover .db-slot-title { color: var(--db-tide); }
a.db-slot-item.is-link.is-alert:hover .db-slot-title { color: var(--db-critical); }
a.db-slot-item.is-link:focus-visible { outline: 2px solid var(--db-tide); outline-offset: -2px; }
.db-slot-title { font-size: 0.87rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.db-slot-sub { font-size: 0.77rem; color: var(--db-ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.db-slot-item.is-alert .db-slot-title { color: var(--db-critical); }
.db-slot-item.is-alert .db-slot-sub { color: var(--db-critical); }
.db-slot-more { font-size: 0.78rem; font-weight: 600; color: var(--db-tide); text-decoration: none; margin-top: auto; }
.db-slot-more:hover { text-decoration: underline; }
.db-slot-empty { margin: 0; font-size: 0.83rem; color: var(--db-ink-faint); }

/* ---- band 2: domain cards ----------------------------------------------- */
/* One per row inside the split column. With three domains a two-up grid left
   the last card alone beside a hole; stacked, each gets the full column width
   and the group balances against the occupancy panel opposite. */
.db-domains { display: flex; flex-direction: column; gap: 0.85rem; }
.db-domain-lines { margin-top: -0.15rem; }
.db-domain {
  display: flex; flex-direction: column; gap: 0.7rem;
  padding: 1rem 1.1rem;
  background: var(--db-surface);
  border: 1px solid var(--db-line);
  border-radius: 12px;
  box-shadow: var(--db-shadow-sm);
  font-family: var(--db-font-body);
}
/* Nothing to do: same card, same place, drained of emphasis. It keeps its slot
   so the page holds its shape from one day to the next. */
.db-domain.is-clear { background: none; box-shadow: none; border-color: var(--db-line-soft); }
.db-domain-head { display: flex; align-items: center; gap: 0.6rem; }
.db-domain-head h3 { flex: 1; margin: 0; font-size: 0.95rem; font-weight: 700; font-family: var(--db-font-body); }
.db-domain-icon {
  width: 1.9rem; height: 1.9rem; border-radius: 8px; flex: none;
  display: grid; place-items: center;
  background: var(--db-tide-mist); color: var(--db-tide); font-size: 0.82rem;
}
.db-domain.is-clear .db-domain-icon { background: var(--db-stone-deep); color: var(--db-ink-faint); }
.db-domain-chip {
  flex: none; min-width: 1.5rem; padding: 0.05rem 0.5rem; border-radius: 999px;
  font-family: var(--db-font-mono); font-size: 0.74rem; font-weight: 700; text-align: center;
  background: var(--db-stone-deep); color: var(--db-ink-soft);
  font-variant-numeric: tabular-nums;
}
.db-domain-chip.warn { background: var(--db-pending-mist); color: var(--db-pending); }
.db-domain-chip.late { background: var(--db-critical-mist); color: var(--db-critical); }
.db-domain-chip.clear { background: var(--db-ok-mist); color: var(--db-ok); }
.db-domain-lines { display: flex; flex-direction: column; }
.db-domain-line {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  padding: 0.32rem 0; border-bottom: 1px solid var(--db-line-soft);
  font-size: 0.85rem; color: var(--db-ink-soft); text-decoration: none;
}
.db-domain-line:last-child { border-bottom: 0; }
/* Le détail sous le libellé : le compteur dit combien, cette ligne dit
   combien ça coûte et avant quand. En bloc, pour qu'elle ne se batte pas avec
   le chiffre à droite sur une ligne étroite. */
.db-domain-line-detail {
  display: block;
  font-size: 0.76rem;
  color: var(--db-ink-faint);
  margin-top: 0.1rem;
}
.db-domain-line:hover { color: var(--db-tide); text-decoration: none; }
.db-domain-line b { font-family: var(--db-font-mono); font-weight: 600; color: var(--db-ink); font-variant-numeric: tabular-nums; }
.db-domain-line b.warn { color: var(--db-pending); }
.db-domain-line b.late { color: var(--db-critical); }
.db-domain-clear { margin: 0; font-size: 0.84rem; color: var(--db-ink-faint); }
.db-domain-foot { margin-top: auto; font-size: 0.8rem; font-weight: 600; color: var(--db-tide); text-decoration: none; }
.db-domain-foot:hover { text-decoration: underline; }

/* ---- band 3: month figures ---------------------------------------------- */
/* Full page width, below the split — squeezed into the split's left column a
   four-figure row could not hold "108 616,60 €" on one line, and the wrapped
   currency mark was clipped. */
.db-figs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1px; background: var(--db-line);
  border: 1px solid var(--db-line); border-radius: 12px; overflow: hidden;
  box-shadow: var(--db-shadow-sm);
  font-family: var(--db-font-body);
}
.db-fig { background: var(--db-surface); padding: 0.9rem 1.05rem; display: flex; flex-direction: column; gap: 0.25rem; text-decoration: none; color: inherit; }
.db-fig:hover { background: var(--db-surface-2); text-decoration: none; }
.db-fig-label { font-size: 0.74rem; color: var(--db-ink-soft); }
.db-fig-value {
  font-family: var(--db-font-display); font-size: 1.5rem; font-weight: 600; line-height: 1;
  white-space: nowrap;
  color: var(--db-ink); font-variant-numeric: tabular-nums;
}
.db-fig-value.money { color: var(--db-sand-deep); }
.db-fig-sub { font-family: var(--db-font-mono); font-size: 0.72rem; color: var(--db-ink-faint); }

/* The only configuration state left, and only when it blocks something. */
.db-platform {
  margin: 1rem 0 0; font-size: 0.83rem; color: var(--db-ink-soft);
  font-family: var(--db-font-body);
}
.db-platform i { color: var(--db-ink-faint); margin-right: 0.35rem; }

@media (max-width: 640px) {
  .db-today { margin-bottom: 1.5rem; }
  .db-slot { padding: 0.85rem 1rem; }
}

/* ---------------------------------------------------------------- 13-communication.css */
/* ============================================================================
   /communication — the response queue.

   The page was a recency-sorted accordion: name, gîte, timestamp, and nothing
   else until you opened a thread. Recency also ordered the wrong thing — at
   midday the question is who has waited longest and who you are responsible
   for right now, not what arrived most recently.
   ============================================================================ */
.comm-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
  gap: 1px; background: var(--db-line);
  border: 1px solid var(--db-line); border-radius: 14px; overflow: hidden;
  box-shadow: var(--db-shadow-sm); margin-bottom: 1.75rem;
  font-family: var(--db-font-body);
}
.comm-stat { background: var(--db-surface); padding: 0.9rem 1.05rem; display: flex; flex-direction: column; gap: 0.2rem; }
.comm-stat-label { font-size: 0.73rem; color: var(--db-ink-soft); }
.comm-stat-value {
  font-family: var(--db-font-display); font-size: 1.5rem; font-weight: 600; line-height: 1;
  color: var(--db-ink); font-variant-numeric: tabular-nums;
}
.comm-stat-value.is-late { color: var(--db-critical); }
.comm-stat-value.is-clear { color: var(--db-ok); }
.comm-stat-sub {
  font-family: var(--db-font-mono); font-size: 0.72rem; color: var(--db-ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- the queue ---------------------------------------------------------- */
.comm-queue { display: flex; flex-direction: column; gap: 0.7rem; }
.comm-msg {
  border: 1px solid var(--db-line); border-radius: 12px; padding: 0.9rem 1.05rem;
  display: flex; flex-direction: column; gap: 0.55rem; background: var(--db-surface);
}
/* Only past the stage's own tolerance — see lateAfter() in communication.go.
   Colouring every waiting thread would repeat the mistake the old home page
   made, where a permanent warning had stopped being a warning. */
.comm-msg.is-late { border-left: 3px solid var(--db-critical); }
.comm-msg-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.comm-msg-who { font-weight: 700; font-size: 0.94rem; }
.comm-msg-who a { color: inherit; }
.comm-msg-prop { font-size: 0.83rem; color: var(--db-ink-soft); }
.comm-msg-wait {
  margin-left: auto; font-family: var(--db-font-mono); font-size: 0.78rem;
  font-weight: 600; color: var(--db-ink-faint); white-space: nowrap;
}
.comm-msg-wait.is-late { color: var(--db-critical); }
.comm-msg-body {
  margin: 0; font-size: 0.88rem; color: var(--db-ink-soft);
  border-left: 2px solid var(--db-line); padding-left: 0.75rem; overflow-wrap: anywhere;
}
.comm-msg-foot { display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; }

.comm-tag {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.02em;
  padding: 0.1rem 0.55rem; border-radius: 999px;
  background: var(--db-stone-deep); color: var(--db-ink-soft); white-space: nowrap;
}
.comm-tag.stage-stay { background: var(--db-ok-mist); color: var(--db-ok); }
.comm-tag.stage-arrive { background: var(--db-tide-mist); color: var(--db-tide); }
.comm-tag.stage-lead { background: var(--db-sand-mist); color: var(--db-sand-deep); }
.comm-tag.stage-past, .comm-tag.stage-future { background: var(--db-stone-deep); color: var(--db-ink-faint); }
.comm-tag.channel { background: none; border: 1px solid var(--db-line); color: var(--db-ink-faint); font-weight: 600; }
.comm-tag.awaiting { background: var(--db-pending-mist); color: var(--db-pending); }
.comm-tag.owner { background: var(--db-sand-mist); color: var(--db-sand-deep); }

/* Reply without leaving: POST /customers/{id}/messages already existed, the
   page just never called it. */
.comm-reply { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-end; margin: 0; }
.comm-reply textarea {
  flex: 1 1 20rem; min-width: 0; resize: vertical;
  padding: 0.5rem 0.7rem; border: 1px solid var(--db-line); border-radius: var(--radius-sm, 6px);
  font: inherit; font-size: 0.87rem; background: var(--db-surface);
}
.comm-reply button { margin: 0; flex: none; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- presence columns ---------------------------------------------------- */
.comm-two { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: 1rem; }
.comm-col-head {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--db-ink-faint); margin-bottom: 0.6rem;
}
.comm-col-head b {
  margin-left: auto; font-family: var(--db-font-mono); font-size: 1rem;
  color: var(--db-ink); letter-spacing: 0;
}
.comm-rows { display: flex; flex-direction: column; }
/* The whole row is the link, not just the name: the row is one record, so the
   whole strip of it should be the target and should light up as one. Padded
   sideways and pulled back out by the same amount, so the hover fill spans the
   panel's full width while the text stays aligned with everything above it. */
.comm-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  font-size: 0.86rem; padding: 0.4rem 0.6rem; margin: 0 -0.6rem;
  border-bottom: 1px solid var(--db-line-soft);
  border-radius: 6px;
  color: inherit; text-decoration: none;
  transition: background-color 0.12s ease;
}
.comm-row:last-child { border-bottom: 0; }
.comm-row:hover { background: var(--db-tide-mist); text-decoration: none; }
.comm-row:hover .comm-row-main { color: var(--db-tide); }
.comm-row:focus-visible { outline: 2px solid var(--db-tide); outline-offset: -2px; }
.comm-row-main { min-width: 0; }
.comm-row-meta { font-family: var(--db-font-mono); font-size: 0.75rem; color: var(--db-ink-faint); white-space: nowrap; }

/* ---- archive ------------------------------------------------------------- */
.comm-search { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; margin-bottom: 0.9rem; }
.comm-search input[type="search"] {
  flex: 1 1 18rem; min-width: 0; padding: 0.45rem 0.7rem;
  border: 1px solid var(--db-line); border-radius: var(--radius-sm, 6px); font: inherit; font-size: 0.87rem;
}
.comm-filters { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.comm-filter {
  border: 1px solid var(--db-line); background: var(--db-surface); color: var(--db-ink-soft);
  border-radius: 999px; padding: 0.25rem 0.8rem; font: inherit; font-size: 0.79rem; font-weight: 600;
  cursor: pointer;
}
.comm-filter:hover { background: var(--db-surface-2); }
.comm-filter.is-on { background: var(--db-tide); border-color: var(--db-tide); color: #fff; }

.comm-threads { display: flex; flex-direction: column; gap: 0.4rem; }
.comm-thread { margin: 0; border: 1px solid var(--db-line); border-radius: 10px; padding: 0; background: var(--db-surface); }
.comm-thread > summary {
  display: flex; align-items: flex-start; gap: 0.9rem;
  padding: 0.7rem 0.9rem; cursor: pointer; color: inherit; margin: 0; list-style: none;
}
.comm-thread > summary::-webkit-details-marker { display: none; }
.comm-thread > summary:hover { background: var(--db-surface-2); }
.comm-thread[open] > summary { border-bottom: 1px solid var(--db-line-soft); }
.comm-thread-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.comm-thread-label { font-size: 0.89rem; font-weight: 600; }
/* The excerpt is the whole point of the redesign: it is what lets you skip a
   thread without opening it. */
.comm-thread-snippet {
  font-size: 0.82rem; color: var(--db-ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.comm-thread-meta { flex: none; display: flex; align-items: center; gap: 0.4rem; }
.comm-thread-ago { font-family: var(--db-font-mono); font-size: 0.75rem; color: var(--db-ink-faint); white-space: nowrap; }
.comm-thread > *:not(summary) { padding: 0 0.9rem; }
.comm-thread > *:last-child { padding-bottom: 0.7rem; }

.comm-playbook { margin-top: 1.5rem; font-size: 0.85rem; }
.comm-playbook i { color: var(--db-ink-faint); margin-right: 0.35rem; }

@media (max-width: 640px) {
  .comm-msg-wait { margin-left: 0; }
  .comm-thread > summary { flex-direction: column; gap: 0.4rem; }
  .comm-thread-meta { align-self: flex-start; }
  .comm-thread-snippet { white-space: normal; }
}

/* .cell-sub — la ligne secondaire sous la valeur d'une cellule : « 8 nuits ·
   4 pers. » sous des dates, une note de contrat sous un statut. Écrite trois
   fois en style inline avant d'être nommée ici. */
.cell-sub { margin-top: 0.25rem; font-size: 0.78rem; }

/* ---- /properties/{id}/bookings, one gîte's reservations ------------------- */
/* This table had no widths at all: eight content-sized columns, so a long
   guest name or a four-figure total pushed it past its own container and the
   page grew a horizontal scrollbar on a perfectly ordinary 1366 laptop. Same
   treatment as .bk-table and .lb-table — fixed layout, percentages, and a
   floor low enough that the floor is not itself the cause.

   52rem, not the 60 of .bk-table: this list has no gîte column (every row is
   the same gîte) and no payment column, so it fits in less. The lesson from
   .lb-hist applies — a floor set by eye rather than by the columns is how a
   table starts scrolling inside a page that had room for it. */
/* Sizes, not percentages, and no min-width floor.
 *
 * The first attempt copied .bk-table — table-layout: fixed, a percentage per
 * column, a floor under the whole table — and got both failure modes at once.
 * Wide: 13 % of a 1100 px content column is 143 px handed to "965,70 €", which
 * needs half of it, so the columns drifted apart. Narrow: the percentages plus
 * .table-sticky's own `td.cell-primary { min-width: 12rem }` added up to more
 * than 100 %, and the table overflowed its wrapper — the horizontal scrollbar
 * this table was rebuilt to remove, back again on ?show=past, where seventeen
 * rows bring a vertical scrollbar that takes the last few pixels.
 *
 * A percentage is the wrong unit here: an amount, a date and a status pill
 * are all fixed-size things, and only the guest name benefits from whatever
 * is left. So the five known columns are sized in rem and the name takes the
 * remainder. Nothing has a floor, so the table always fits its wrapper. */
.pb-table { table-layout: fixed; width: 100%; }
.pb-table .c-guest  { width: auto; }
.pb-table .c-stay   { width: 8.75rem; }
.pb-table .c-amount { width: 6.5rem; }
.pb-table .c-state  { width: 7.5rem; }
/* Sous 1200 px la barre latérale prend un tiers de l'écran et il ne reste plus
   assez pour six colonnes : le statut et les montants rendent ce qu'ils
   peuvent, plutôt que de laisser la colonne du nom tomber à zéro. */
@media (max-width: 1200px) {
  .pb-table .c-stay   { width: 8rem; }
  .pb-table .c-amount { width: 5.75rem; }
  .pb-table .c-state  { width: 6rem; }
}
/* Overridden from .table-sticky, where 12rem is right for a table whose first
   column is a property name. Here it was a third of the reason the columns did
   not fit. */
.pb-table td.cell-primary { min-width: 0; }
/* A name is the one thing here worth truncating rather than wrapping: the row
   is a link to the booking, where the full name is the page title. */
.pb-table .cell-primary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The status is the compressible one: two short lines beat a pill sticking out
   of its column, and it is the only cell whose meaning survives a wrap. */
.pb-table .c-state, .pb-table td:has(> .status-pill) { white-space: normal; }
.pb-table .status-pill { white-space: normal; text-align: center; }

@media (max-width: 640px) {
  /* .table-cards has taken over and there are no columns left to size. */
  .pb-table { table-layout: auto; }
  .pb-table .c-stay, .pb-table .c-amount, .pb-table .c-state { width: auto; }
}

/* ---- /bookings, the global reservation list ------------------------------ */
/* The floor is set by the widest unbreakable cells — a full date range and a
   four-figure amount — so the table scrolls rather than truncating them. */
.bk-table { table-layout: fixed; width: 100%; min-width: 60rem; }
.bk-table .c-guest  { width: 15%; }
.bk-table .c-prop   { width: 19%; }
.bk-table .c-stay   { width: 21%; }
/* Wide enough for two tags — a row can be both "sur place" and "sans
   réponse", and at 16% the pair spilled over the payment column. */
.bk-table .c-state  { width: 18%; }
.bk-table .c-pay    { width: 11%; }
.bk-table .c-amount { width: 12%; }
.bk-table td[data-label="État"] { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; }
.bk-table .date-cell { white-space: nowrap; }
.bk-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bk-pay { font-size: 0.8rem; font-weight: 600; }
.bk-pay.ok { color: var(--db-ok); }
.bk-pay.pending { color: var(--db-ink-soft); }
.bk-pay.late { color: var(--db-critical); }
.bk-go { text-align: right; }
.bk-go a { color: var(--db-ink-faint); text-decoration: none; }
.bk-go a:hover { color: var(--brand); }
.bk-go-text { display: none; }
@media (max-width: 640px) {
  .bk-table { table-layout: auto; min-width: 0; }
  .bk-ellipsis { white-space: normal; overflow: visible; }
  .bk-go-text { display: inline; }
  .bk-go a { color: var(--brand); font-weight: 600; }
}

/* The header counters are links now — they lead to the same filtered booking
   list the numbers are counted from. */
a.comm-stat { text-decoration: none; color: inherit; transition: background-color .12s ease; }
a.comm-stat:hover { background: var(--db-surface-2); text-decoration: none; }
a.comm-stat:hover .comm-stat-label { color: var(--db-tide); }
.comm-col-link { margin: -0.35rem 0 0.6rem; font-size: 0.8rem; }
.comm-thread-links { display: flex; flex-wrap: wrap; gap: 1rem; }

/* Two filter axes, laid out as two groups so they do not read as one set. */
.bk-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.25rem; margin-bottom: 1rem; }
.bk-filters .filter-pills { margin-bottom: 0; }
/* The independent toggle: outlined rather than filled when off, and carrying a
   dismiss mark when on, so it reads as "also narrow by this" instead of as a
   sixth period. */
.bk-toggle { border-style: dashed; }
.bk-toggle.active { border-style: solid; }
.bk-toggle-off { font-size: 0.7em; opacity: 0.8; margin-left: 0.1rem; }

/* La boîte d'essai (/emails/boite-d-essai) : une liste de messages remis,
   et le message lui-même. Le corps est du texte brut — c'est ce qui a été
   envoyé, et le mettre en forme donnerait une idée fausse de ce que le
   destinataire a lu. */
.dm-table .dm-date { white-space: nowrap; width: 7rem; }
.dm-preview { display: block; font-size: 0.82rem; margin-top: 0.15rem; }
.dm-body {
  margin-top: 1rem;
  padding: 1rem 1.1rem;
  background: var(--off-bg, #f6f8f9);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font: 0.88rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* ---------------------------------------------------------------- 14-booking.css */
/* ============================================================================
   /bookings/{id} — fixed header + tabs.

   The page stacked header, card, options, payment and notes; payment, the only
   place you act, sat at the bottom and the guest name scrolled away on the way
   there. The shape here follows the convention of the systems this app sits
   next to — OPERA Cloud's reservation header with presentation panel tabs,
   Cloudbeds' reservation drawer — plus the Ménage tab that rental software
   (Hostaway, Guesty) has and hotel PMSs do not.
   ============================================================================ */
.bk-head {
  background: var(--db-surface); border: 1px solid var(--db-line);
  border-radius: 12px 12px 0 0; border-bottom: none;
  padding: 1.1rem 1.25rem; display: flex; flex-direction: column; gap: 0.5rem;
  font-family: var(--db-font-body);
}
.bk-head-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.6rem; }
.bk-head-top h1 { margin: 0; font-size: 1.5rem; }
.bk-head-meta { margin: 0; font-size: 0.85rem; color: var(--db-ink-soft); }
.bk-head-figs { display: flex; flex-wrap: wrap; gap: 1.75rem; margin: 0.3rem 0 0; }
.bk-head-figs > div { display: flex; flex-direction: column; gap: 0.1rem; }
.bk-head-figs dt {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--db-ink-faint);
}
.bk-head-figs dd {
  margin: 0; font-family: var(--db-font-mono); font-size: 0.92rem;
  font-variant-numeric: tabular-nums; color: var(--db-ink);
}
.bk-head-figs dd.due, .bk-head-figs .due { color: var(--db-critical); font-weight: 600; }

/* .bk-tabs / .bk-tab removed: the booking page uses design.css's shared
   .tabs component, so it gets the mobile section picker like every other
   tab set. .bk-pip below stays — the attention count is this page's own. */
.bk-pip {
  font-family: var(--db-font-mono); font-size: 0.68rem; font-weight: 700;
  min-width: 1.15rem; text-align: center; padding: 0 0.3rem; border-radius: 999px;
  background: var(--db-pending-mist); color: var(--db-pending);
}
.bk-pip.late { background: var(--db-critical-mist); color: var(--db-critical); }
.bk-panel { border-radius: 0 0 12px 12px; margin-top: 0; }

.bk-clean { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 1.5rem; margin: 0.5rem 0 0.75rem; }
.bk-clean-label {
  display: block; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--db-ink-faint);
}
.bk-clean strong { font-family: var(--db-font-mono); font-size: 0.95rem; }

.bk-events { display: flex; flex-direction: column; }
.bk-event {
  display: flex; align-items: baseline; gap: 0.75rem;
  padding: 0.5rem 0; border-bottom: 1px solid var(--db-line-soft); font-size: 0.87rem;
}
.bk-event:last-child { border-bottom: 0; }
.bk-event-icon { flex: none; width: 1.2rem; text-align: center; color: var(--db-ink-faint); font-size: 0.8rem; }
.bk-event-main { flex: 1; min-width: 0; }
.bk-event-at {
  flex: none; font-family: var(--db-font-mono); font-size: 0.76rem;
  color: var(--db-ink-faint); white-space: nowrap;
}

@media (max-width: 640px) {
  .bk-head-figs { gap: 1rem 1.25rem; }
  .bk-head-top h1 { font-size: 1.25rem; }
}

/* Cleaning, surfaced on the common section rather than only inside its tab:
   after "what is owed", "who cleans after this guest" is the question asked
   most about a booking, and it should not cost a tab switch to read. */
.bk-cleaning-line {
  margin: -0.5rem 0 1.25rem; font-size: 0.86rem; color: var(--db-ink-soft);
  font-family: var(--db-font-body);
}
.bk-cleaning-line i { color: var(--db-ink-faint); margin-right: 0.35rem; }
.bk-cleaning-line .due { color: var(--db-critical); }

/* ---- row actions & the booking Paiement tab ------------------------------
   One emphasised control per row, everything else demoted to a link. Two
   outlined buttons side by side made the operator stop and choose between an
   action that writes to the ledger and one that navigates — and the louder of
   the two was the manual flag, which is the path with no evidence behind it.
   Rapprocher is the button now; marquer payé is the quiet way out. */
.row-actions {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: flex-end; gap: 0.25rem 0.7rem;
}
.row-actions .inline-form { margin: 0; }

/* Outlined action in the desk palette — .btn-secondary still carries the
   pre-redesign brand colours, which read as foreign inside .table-modern. */
.btn-outline {
  display: inline-flex; align-items: center; gap: 0.35rem;
  margin-top: 0; padding: 0.34rem 0.8rem;
  font-size: 0.82rem; font-weight: 600; line-height: 1.35;
  background: var(--db-surface); color: var(--db-tide);
  border: 1px solid var(--db-line); border-radius: 7px;
  text-decoration: none; white-space: nowrap;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.btn-outline:hover {
  background: var(--db-tide-mist); border-color: var(--db-tide);
  text-decoration: none; color: var(--db-tide);
}
.btn-outline:focus-visible { outline: 2px solid var(--db-tide); outline-offset: 1px; }

/* A submit button that reads as a link: same one click, visibly the lesser
   path. Kept a <button> rather than an <a> because it POSTs — the visual
   demotion must not cost the correct semantics. */
.link-action {
  display: inline; margin: 0; padding: 0;
  background: none; border: 0; border-radius: 0;
  font: inherit; font-size: 0.8rem; font-weight: 500;
  color: var(--db-ink-soft); text-decoration: underline;
  text-decoration-color: var(--db-line); text-underline-offset: 2px;
  cursor: pointer; white-space: nowrap;
}
.link-action:hover { background: none; color: var(--db-tide); text-decoration-color: currentColor; }
.link-action:focus-visible { outline: 2px solid var(--db-tide); outline-offset: 2px; }
/* Undoing a recorded encashment is not the same kind of act as recording one,
   and must not look like it — but it must not shout either. A settled booking
   is the common case, and painting its three rows red made the healthy state
   look alarming. So: dotted underline to stay distinct from the forward link
   above, red only on hover, and the real weight carried by the confirm() on
   the form rather than by the colour. */
.link-action.is-undo {
  text-decoration-style: dotted;
  text-decoration-color: var(--db-ink-faint);
}
.link-action.is-undo:hover { color: var(--db-critical); text-decoration-color: currentColor; }

.pay-none { color: var(--db-ink-faint); }

/* Reste à percevoir is the number the tab is opened for; the other two are
   there to make it checkable. */
.pay-sum {
  display: flex; flex-wrap: wrap; gap: 0.6rem 2.2rem;
  align-items: baseline;
  padding: 0.85rem 1rem; margin-bottom: 1.1rem;
  background: var(--db-sand-mist); border: 1px solid var(--db-line-soft);
  border-radius: 10px;
}
.pay-sum-fig { display: flex; flex-direction: column; gap: 0.1rem; }
.pay-sum-label {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--db-ink-soft);
}
.pay-sum-fig strong { font-size: 1.02rem; font-variant-numeric: tabular-nums; }
.pay-sum-fig.is-rest { margin-left: auto; text-align: right; }
.pay-sum-fig.is-rest strong { font-size: 1.28rem; color: var(--db-tide); }
/* Guests gone, money still out: the one case where the figure should shout. */
.pay-sum-fig.is-rest.is-late strong { color: var(--db-critical); }
.pay-sum-fig.is-rest.is-late .pay-sum-label { color: var(--db-critical); }

@media (max-width: 640px) {
  .pay-sum { gap: 0.5rem 1.4rem; }
  .pay-sum-fig.is-rest { margin-left: 0; text-align: left; width: 100%; }
  /* Card mode stacks the row; actions read left-to-right like the rest. */
  .row-actions { justify-content: flex-start; gap: 0.35rem 1rem; }
  /* A demoted control is still a control: on a touch screen both of these
     get a 44px target, which an 17px-tall underlined word does not. */
  .link-action { display: inline-flex; align-items: center; min-height: 44px; }
  .btn-outline { min-height: 44px; padding: 0.5rem 0.95rem; }
}

/* Une échéance dépassée n'est pas une information de configuration : elle se
   lit comme un retard, au même titre qu'une rotation sans personne. */
.db-platform-alert { color: var(--error); }
.db-platform-alert i { color: var(--error); }

/* ---- boîte à clefs ------------------------------------------------------
   Four digits read as digits: monospace, spaced, large enough to be checked
   against a dial at arm's length. */
.lockbox-input {
  font-family: var(--db-font-mono, monospace);
  font-size: 1.4rem; font-weight: 600;
  letter-spacing: 0.35em; text-indent: 0.35em;
  max-width: 8rem;
}
.lockbox-code {
  font-family: var(--db-font-mono, monospace);
  font-size: 1.05rem; font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--db-tide);
}

/* ---- /lockbox : vue d'ensemble et historique ---------------------------- */
/* Le nom porte sa ligne de contexte : trois colonnes valent mieux que six,
   surtout quand la moitié des biens n'a encore rien à y mettre. */
.lb-name { display: block; font-weight: 600; }
.lb-meta {
  display: block; margin-top: 0.1rem;
  font-size: 0.78rem; color: var(--db-ink-soft);
}
.lb-meta.is-missing { color: var(--db-critical); }
/* Quatre tirets plutôt qu'une pastille rouge par ligne : à dix-sept biens sans
   code, la pastille répétée devient un mur d'alarme qui ne dit plus rien. Le
   bandeau en haut porte l'alerte, une fois. */
.lb-none {
  font-family: var(--db-font-mono, monospace);
  font-size: 1.05rem; letter-spacing: 0.1em; color: var(--db-ink-faint);
}
.lb-age {
  margin-left: 0.5rem; font-size: 0.74rem; font-weight: 600;
  color: var(--db-ink-soft);
}

/* Largeurs fixes + plancher de min-width, comme .bk-table : le tableau défile
   dans son .table-wrap au lieu de se faire écraser, et les colonnes ne
   dansent plus d'une ligne à l'autre selon la longueur du nom du gîte. */
.lb-table { table-layout: fixed; width: 100%; min-width: 34rem; }
.lb-table .c-name { width: 62%; }
.lb-table .c-code { width: 30%; }
.lb-table td:first-child { line-height: 1.35; }

/* ---- Les listes « un gîte par ligne » ------------------------------------
   Même traitement que .lb-table et .bk-table, et pour la même raison : sans
   table-layout: fixed ni plancher, un tableau à six ou huit colonnes se
   comprime jusqu'à l'illisible au lieu de défiler.

   DEUX RÈGLES, apprises ici à leurs dépens :

   1. les pourcentages doivent faire 100, exactement. La colonne chevron a été
      retirée quand la ligne entière est devenue cliquable, sa largeur avec —
      et les colonnes restantes ne totalisaient plus que 90 %. En
      table-layout: fixed, les 10 % manquants deviennent une bande vide après
      la dernière colonne : c'est ce qu'on voyait sur /properties.

   2. le plancher se calcule sur les colonnes qui restent. À 52 rem pour six
      colonnes, une fenêtre de 1280 px défilait horizontalement pour montrer
      un tableau qui y tenait largement. */
.lv-table { table-layout: fixed; width: 100%; min-width: 36rem; }
.lv-table .c-name { width: 36%; }
.lv-table .c-code { width: 18%; }
.lv-table .c-flag { width: 11%; }
.lv-table .c-vis  { width: 24%; }

.an-table { table-layout: fixed; width: 100%; min-width: 40rem; }
.an-table .c-name { width: 34%; }
.an-table .c-num  { width: 11%; }

.oc-table { table-layout: fixed; width: 100%; min-width: 38rem; }
.oc-table .c-name { width: 34%; }
.oc-table .c-pct  { width: 14%; }
.oc-table .c-num  { width: 16%; }
.oc-table .c-flag { width: 12%; }

.oc-canaux { table-layout: fixed; width: 100%; min-width: 36rem; }
.oc-canaux .c-name { width: 30%; }
.oc-canaux .c-room { width: 18%; }
.oc-canaux .c-state{ width: 16%; }
.oc-canaux .c-sync { width: 20%; }
.oc-canaux .c-num  { width: 16%; }

.ac-table { table-layout: fixed; width: 100%; min-width: 38rem; }
.ac-table .c-name  { width: 32%; }
.ac-table .c-occ   { width: 16%; }
.ac-table .c-num   { width: 10%; }
.ac-table .c-money { width: 16%; }
.ac-occ { font-weight: 700; }

/* Le tableau « par gîte » du pilotage. NOM DISTINCT : .pb-table était déjà
   pris par les réservations d'un gîte, dont les colonnes n'ont rien à voir —
   nos règles écrasaient les siennes en silence, et son plancher devenait le
   nôtre. */
.pg-table { table-layout: fixed; width: 100%; min-width: 34rem; }
.pg-table .c-name  { width: 36%; }
.pg-table .c-bar   { width: 24%; }
.pg-table .c-money { width: 20%; }
.pb-bar {
  display: block; height: 0.55rem; border-radius: 999px;
  background: var(--db-surface-2); overflow: hidden;
}
.pb-bar-fill { display: block; height: 100%; background: var(--db-tide, #1c4a58); border-radius: 999px; }

/* Le tableau du pilotage : douze lignes, huit colonnes, dont six d'euros.
   Les euros ne se compriment pas — « 179 970,72 € » a la largeur qu'il a — donc
   plancher haut et défilement assumé sur un portable, plutôt qu'un retour à la
   ligne au milieu d'un nombre. En dessous de 640 px, .table-cards prend le
   relais et chaque mois devient une carte. */
.pil-table { table-layout: fixed; width: 100%; min-width: 62rem; }
.pil-table .c-month { width: 14%; }
.pil-table .c-count { width: 10%; }
.pil-table .c-money { width: 16.5%; }


/* 39rem, pas 58 : à 58rem le tableau dépassait sa propre colonne dès 1366 px
   et se mettait à défiler à l'intérieur de la page — un portable à 1280
   voyait une barre de défilement horizontale dans le tableau. Le plancher
   reste là pour les largeurs vraiment étroites : il fait défiler plutôt que
   tronquer, ce qui n'arrive plus qu'en dessous de 1000 px, où la barre
   d'onglets mobile a de toute façon pris le relais. */
.lb-hist { table-layout: fixed; width: 100%; min-width: 39rem; }
.lb-hist .c-code   { width: 12%; }
.lb-hist .c-period { width: 26%; }
.lb-hist .c-who    { width: 34%; }
.lb-hist .c-by     { width: 28%; }
/* Les deux bornes l'une sous l'autre : même information, moitié moins large,
   et le regard descend la colonne au lieu de la balayer. */
.lb-when { display: block; line-height: 1.35; white-space: nowrap; }
.lb-hist td { vertical-align: top; }

/* Chevron sur desktop, lien en toutes lettres en mode carte — même bascule
   que .bk-go sur la liste des réservations, parce qu'une carte sans libellé
   ne dit pas où elle mène. */
.lb-go { text-align: right; }
.lb-go a { color: var(--db-ink-faint); text-decoration: none; }
.lb-go a:hover { color: var(--db-tide); text-decoration: none; }
.lb-go-text { display: none; }

@media (max-width: 640px) {
  .lb-table, .lb-hist { table-layout: auto; min-width: 0; }
  .lb-go-text { display: inline; }
  .lb-go a { color: var(--db-tide); font-weight: 600; }
}
/* La période en cours se lit d'un coup d'œil dans une pile de périodes — le
   fond teinté suffit à la désigner. Pas de barre verticale en plus : dans un
   tableau qui n'a qu'une ligne courante, elle ne distingue rien que la
   couleur ne distingue déjà, et elle décale le premier chiffre du code de
   trois pixels par rapport à ceux du dessous. */
.lb-current td { background: var(--db-tide-mist); }

.wb-history {
  display: flex; flex-wrap: wrap; gap: 1rem;
  align-items: center; justify-content: space-between;
}
.wb-history h2 { margin-top: 0; }
.wb-history > div > :last-child { margin-bottom: 0; }

.doc-eyebrow {
  margin: 0 0 0.2rem; font-size: 0.78rem; font-weight: 600;
  letter-spacing: 0.04em; color: var(--db-ink-soft);
}
/* Le client qui avait ce code, cliquable vers sa réservation : c'est le
   chemin qu'on prend en cas d'incident, pas une simple étiquette. */
.lb-guest {
  display: block; color: inherit; text-decoration: none;
  padding: 0.1rem 0; line-height: 1.3;
}
.lb-guest + .lb-guest { border-top: 1px solid var(--db-line-soft); margin-top: 0.25rem; padding-top: 0.3rem; }
.lb-guest:hover { color: var(--db-tide); text-decoration: none; }
.lb-guest .lb-meta { font-size: 0.74rem; }

/* Ligne d'en-tête de gîte à l'intérieur du suivi de taxe de séjour : le nom
   porte le lien vers la fiche, la ligne de dessous rappelle le tarif et le
   calendrier sans peser autant que le nom. */
.tax-section-meta {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.76rem;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--db-ink-faint, #8c959c);
}

/* ---- /quotes/{id}, le bloc d'actions --------------------------------------
   L'état d'abord, puis ce qu'on en fait. Le panneau ouvrait auparavant sur un
   titre « Actions » et un <select> de statut : l'état du devis se lisait dans
   une liste déroulante, c'est-à-dire au même endroit que la commande servant
   à le changer. */
.quote-state {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.6rem;
  margin-bottom: 1rem;
}
.quote-state-next { margin: 0; color: var(--db-ink-soft); font-size: 0.9rem; }

/* Chaque action porte sa conséquence sous elle — le destinataire d'un envoi,
   ce que déclenche une conversion. La question qu'on se pose devant un bouton
   se répond avant le clic, pas après. */
.quote-action { display: flex; flex-direction: column; gap: 0.3rem; }
.quote-action .btn { width: 100%; justify-content: center; }
.quote-action-hint { font-size: 0.75rem; color: var(--db-ink-soft); text-align: center; }

/* Replié : corriger un statut n'est pas une étape du parcours, c'est une
   réparation quand le client a répondu par téléphone. */
.quote-status-fix { margin-top: 1.25rem; border-top: 1px solid var(--db-line); padding-top: 0.9rem; }
.quote-status-fix > summary {
  cursor: pointer; font-size: 0.85rem; font-weight: 600; color: var(--db-ink-soft);
  list-style: none;
}
.quote-status-fix > summary::-webkit-details-marker { display: none; }
.quote-status-fix > summary::before { content: "▸ "; }
.quote-status-fix[open] > summary::before { content: "▾ "; }
.quote-status-form {
  display: flex; flex-wrap: wrap; align-items: end; gap: 0.6rem; margin-top: 0.8rem;
}
.quote-status-form label { font-size: 0.78rem; color: var(--db-ink-soft); }
.quote-status-help { margin: 0.6rem 0 0; font-size: 0.78rem; color: var(--db-ink-soft); }

/* Confirmation d'une action irréversible. <dialog> natif : focus piégé, Échap,
   et un fond qui n'est pas une alerte système hors charte. */
.confirm-dialog {
  border: none; border-radius: 14px; padding: 1.4rem;
  max-width: 26rem; width: calc(100% - 2rem);
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.2);
  color: var(--db-ink);
}
.confirm-dialog::backdrop { background: rgba(0, 0, 0, 0.4); }
.confirm-dialog h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }
.confirm-dialog p { margin: 0 0 1.1rem; color: var(--db-ink-soft); font-size: 0.9rem; line-height: 1.45; }
.confirm-dialog-actions { display: flex; justify-content: flex-end; gap: 0.6rem; }

/* ---- /compta/depenses ------------------------------------------------------
   Mêmes principes que .pb-table : largeurs en rem plutôt qu'en pourcentage,
   la colonne libre prend le reste, aucun plancher — de quoi tenir dans la
   colonne de contenu quelle que soit la largeur. */
.cx-table { table-layout: fixed; width: 100%; }
.cx-table .c-date   { width: 7rem; }
.cx-table .c-prop   { width: 22%; }
.cx-table .c-cat    { width: 9rem; }
.cx-table .c-desc   { width: auto; }
.cx-table .c-amount { width: 7rem; }
.cx-table .c-state  { width: 8rem; }
.cx-table td.cell-primary { min-width: 0; }

@media (max-width: 640px) {
  .cx-table { table-layout: auto; }
  .cx-table .c-date, .cx-table .c-prop, .cx-table .c-cat,
  .cx-table .c-amount, .cx-table .c-state { width: auto; }
}


/* Le nom d'une section qui n'a pas d'onglet pour le porter (Livret, Dépenses,
   Import…) : sous l'en-tête du gîte, et en h2 — le h1 de la page est le nom du
   gîte, et il n'y en a qu'un. */
.section-title { font-size: 1.35rem; margin: 0 0 0.8rem; }

/* En dessous de 640 px, .table-cards transforme chaque ligne en carte — mais
   le plancher `min-width` posé pour empêcher les colonnes de s'écraser reste,
   lui, sur l'élément table : la pile de cartes héritait donc d'une largeur de
   62 rem et la page défilait horizontalement. Les colonnes n'existent plus à
   cette taille, le plancher non plus. */
@media (max-width: 640px) {
  .lv-table, .an-table, .oc-table, .oc-canaux, .ac-table, .pil-table, .pg-table {
    min-width: 0; table-layout: auto;
  }
  /* La barre de proportion, en mode carte. .table-cards fait de chaque cellule
     une ligne « étiquette / valeur » en flex ; la barre, elle, est un bloc sans
     largeur propre, donc elle se retrouvait écrasée à zéro — la colonne
     existait, on ne voyait rien. Elle prend la place qui reste sur sa ligne. */
  .table-cards td .pb-bar { flex: 1 1 8rem; min-width: 6rem; }
}

.commission-form { padding-bottom: 0.9rem; }
.commission-form .field { margin: 0; max-width: 32rem; }
.commission-input { display: flex; align-items: center; gap: 0.5rem; }
.commission-input input { width: 7rem; }
.commission-unit { font-weight: 600; color: var(--db-ink-soft); }

/* ---- Champs avec unité ---------------------------------------------------
   « 30 » n'est pas « 30 % », et un prix sans € se relit deux fois. L'unité est
   posée DANS le champ, à droite, plutôt qu'en libellé à côté : elle reste
   visible pendant la saisie, elle n'entre pas dans la valeur envoyée, et le
   champ garde une seule boîte au lieu de deux éléments à aligner.

   Le rembourrage droit de l'input laisse la place à l'unité ET à la flèche
   native des champs nombre, qui vient s'y superposer sinon. */
.input-suffix { position: relative; display: block; }
.input-suffix input { width: 100%; padding-right: 2.9rem; }
/* Les flèches natives des champs nombre sont retirées ici : elles occupaient
   la place de l'unité, qui se retrouvait par-dessus, et personne n'ajuste un
   prix de 150 € par pas de 0,01 en cliquant sur une flèche. Le clavier et la
   molette continuent de fonctionner. */
.input-suffix input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.input-suffix input[type="number"]::-webkit-outer-spin-button,
.input-suffix input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.input-suffix-unit {
  position: absolute; right: 0.7rem; top: 50%; transform: translateY(-50%);
  font-size: 0.85rem; color: var(--db-ink-faint); pointer-events: none;
}
.field-narrow { max-width: 22rem; }
.field-req { color: var(--error, #a3271f); font-weight: 700; }

/* Les actions d'un formulaire, sur leur propre ligne et jamais dans la grille
   des champs — c'est la règle tirée de la page Chambres, où le bouton posé en
   colonne finissait par-dessus son voisin. */
.form-actions { display: flex; align-items: center; gap: 0.9rem; margin-top: 1rem; flex-wrap: wrap; }
.page-intro { max-width: 60rem; }

/* Le tableau des périodes tarifaires : sept colonnes dont deux dates, deux
   montants et une action. */
/* La courbe de remplissage : deux barres l'une derrière l'autre, cette année
   devant, l'an dernier en fantôme. Superposées et non côte à côte — c'est un
   écart qu'on lit, pas deux séries indépendantes. */
.stats-bars { height: 10rem; }
.stats-bars .year-bar-track { position: relative; }
.year-bar-ghost {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--db-stone-deep, #e7e3d6); border-radius: 4px 4px 0 0;
}
.stats-bars .year-bar-fill { position: relative; opacity: 0.92; }
.stats-change { color: var(--brand); font-size: 1.1rem; line-height: 0.6; height: 0.7rem; }
.pickup-up { color: #1d6f42; font-weight: 700; }
.pickup-down { color: var(--error, #a3271f); font-weight: 700; }

.rs-table { table-layout: fixed; width: 100%; min-width: 38rem; }
.rs-table .c-month { width: 16%; }
.rs-table .c-num   { width: 14%; }
.rs-table .c-money { width: 14%; }
@media (max-width: 640px) { .rs-table { min-width: 0; table-layout: auto; } }

/* Les tableaux du cycle des tarifs : la revue d'un lot, l'historique des lots,
   les nuits orphelines. Mêmes règles que les autres — pourcentages qui font
   100, plancher calibré sur une fenêtre étroite. */
.rb-table { table-layout: fixed; width: 100%; min-width: 40rem; }
.rb-table .c-name  { width: 22%; }
.rb-table .c-dates { width: 16%; }
.rb-table .c-num   { width: 9%; }
.rb-table .c-money { width: 13%; }
.rb-table .c-warn  { width: 14%; }
.rb-table .c-act   { width: 4%; }

.rl-table { table-layout: fixed; width: 100%; min-width: 36rem; }
.rl-table .c-name  { width: 34%; }
.rl-table .c-date  { width: 16%; }
.rl-table .c-who   { width: 16%; }
.rl-table .c-state { width: 16%; }
.rl-table .c-num   { width: 9%; }

/* Le tableau des creux porte maintenant deux champs par ligne — le prix et le
   séjour minimum — et le bouton qui les envoie. D'où le plancher relevé : à
   40 rem, les deux champs et le verbe se marchaient dessus. */
.rg-table { table-layout: fixed; width: 100%; min-width: 52rem; }
.rg-table .c-name  { width: 26%; }
.rg-table .c-dates { width: 16%; }
.rg-table .c-money { width: 12%; }
.rg-table .c-price { width: 21%; }
.rg-table .c-min   { width: 13%; }
.rg-table .c-act   { width: 12%; }

/* La ligne comme éditeur : le champ prend la largeur de sa colonne, et le
   rappel (« proposé 96 € · −20 % ») passe dessous plutôt qu'à côté — à côté,
   il volait au champ la moitié de sa place. */
.rg-edit { display: flex; flex-direction: column; gap: 0.25rem; }
.rg-price input, .rg-min input { width: 100%; margin: 0; }
.rg-table td .input-suffix { display: block; }
/* Les boutons d'une cellule : empilés, sans la marge haute que porte tout
   bouton du desk — ici la cellule EST la barre d'action. */
.rg-table .c-act { white-space: nowrap; }
.rg-form { display: inline-block; margin: 0; }
.rg-form button { margin-top: 0; }
.rg-form + .rg-form { margin-left: 0.3rem; }
/* Un creux déjà repris dans le lot en cours se lit sans avoir à comparer les
   deux boutons de sa ligne : fond distinct et pastille « au lot ». C'est elle
   qui explique pourquoi le bouton de cette ligne dit « Mettre à jour » quand
   ses voisines disent « Ajouter au lot » — ce n'est pas le même geste, et
   « ajouter » sur une ligne déjà dans le lot serait un mensonge. */
.rg-staged > td { background: var(--db-surface-2, #f4f1ea); }
.rg-staged .cell-primary { font-weight: 600; }
.rg-table .cell-primary .status-pill { margin-left: 0.45rem; vertical-align: 1px; }

@media (max-width: 640px) {
  .rg-form { display: block; }
  .rg-form + .rg-form { margin-left: 0; margin-top: 0.4rem; }
}

@media (max-width: 640px) {
  .rb-table, .rl-table, .rg-table { min-width: 0; table-layout: auto; }
}

/* Bouton d'action réduit à son icône, pour les colonnes qui n'ont pas la
   place d'un verbe. Il garde la taille de cible d'un vrai bouton (36 px) —
   une croix de douze pixels ne se clique pas au trackpad. */
.btn-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; padding: 0; border-radius: 8px;
  font-size: 0.9rem; line-height: 1;
}

/* Plancher calibré sur la fenêtre réelle et non sur un grand écran : une
   fenêtre de 1000 px CSS laisse ~40 rem au contenu une fois le rail déduit,
   et un plancher au-dessus fait défiler un tableau qui tiendrait. */
.rt-table { table-layout: fixed; width: 100%; min-width: 34rem; }
.rt-table .c-name  { width: 28%; }
.rt-table .c-dates { width: 22%; }
.rt-table .c-money { width: 15%; }
.rt-table .c-min   { width: 13%; }
.rt-table .c-act   { width: 7%; }
/* Les deux bornes empilées : « du » et « au » en gris pour que les chiffres,
   qui sont l'information, ressortent seuls à l'œil. */
.rt-dates { line-height: 1.5; white-space: nowrap; }
.rt-date-prep { color: var(--db-ink-faint); font-size: 0.82em; }
@media (max-width: 640px) { .rt-table { min-width: 0; table-layout: auto; } }

/* ---- Champs étroits dans une grille --------------------------------------
   app.css pose `form input { min-width: 200px }` — bon plancher pour un
   formulaire en colonne, désastreux dans une grille : une colonne de 7 rem
   (112 px) reçoit un champ qui refuse de descendre sous 200 px, et l'élément
   déborde sur ses voisins. C'est ce qui faisait passer le bouton « Ajouter »
   sous le champ « Lits doubles » et la flèche du champ « Ordre » par-dessus
   le champ « Nom ».
   min-width: 0 est le correctif canonique : dans une grille ou un flex, c'est
   la piste qui décide de la largeur, pas le contenu. */
.num-form input,
.bedroom-row input,
.rate-form input,
.rate-form .daterange-trigger,
.commission-input input,
.input-suffix input { min-width: 0; }

/* ---- Les formulaires de la page Chambres ---------------------------------
   Deux formes, une seule grammaire : libellé au-dessus du champ, champs
   dimensionnés par ce qu'ils reçoivent, actions au bout de la ligne et jamais
   par-dessus un champ. Le bouton « Enregistrer » se posait sur le champ
   « Lits » parce qu'il était une colonne de la grille alignée en bas alors que
   les champs, eux, portent un libellé — il est maintenant hors grille. */
.num-form {
  display: grid; gap: 0.9rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  max-width: 46rem;
}
.num-form .field { margin: 0; }
.num-form label, .bedroom-row label {
  display: block; font-weight: 600; font-size: 0.82rem; margin-bottom: 0.25rem;
  color: var(--db-ink-soft);
}
.field-unit { font-weight: 400; color: var(--db-ink-faint); }
.num-form input { width: 100%; }
.form-intro { display: block; margin: 0 0 0.9rem; max-width: 46rem; }

.bedroom-rows { display: flex; flex-direction: column; gap: 0.5rem; }
.bedroom-row {
  display: grid; gap: 0.6rem 0.9rem; align-items: end;
  grid-template-columns: 6rem minmax(9rem, 1fr) 8rem 8rem auto;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--db-line); border-radius: 10px;
  background: var(--db-surface);
}
.bedroom-row.is-new { background: var(--db-surface-2); }
.bedroom-row .field { margin: 0; min-width: 0; }
.bedroom-row input { width: 100%; }
.bd-actions { display: flex; gap: 0.4rem; align-items: end; }

@media (max-width: 800px) {
  /* Deux colonnes : le nom sur toute la largeur, les nombres par paires, les
     actions en pied de carte — la chambre reste une unité visuelle. */
  .bedroom-row { grid-template-columns: 6rem 1fr; }
  .bedroom-row .bd-name { grid-column: 2; }
  .bedroom-row .bd-num { grid-column: span 1; }
  .bd-actions { grid-column: 1 / -1; justify-content: flex-end; }
}

/* ---- Le formulaire « ajouter une période tarifaire » ----------------------
   Six colonnes, deux rangées : l'identité de la période puis son chiffrage.
   Les libellés s'alignent en haut — un champ avec une aide dessous ne doit
   pas décaler le libellé de ses voisins. */
.rate-form {
  display: grid; gap: 1.1rem 1rem; align-items: start;
  grid-template-columns: repeat(6, 1fr);
}
.rate-form .field { margin: 0; min-width: 0; }
.rate-form .rf-name  { grid-column: span 3; }
.rate-form .rf-dates { grid-column: span 3; }
.rate-form .rf-num   { grid-column: span 2; }
.rate-form > .field > label,
.rate-form .daterange > label {
  display: block; font-weight: 600; font-size: 0.82rem; margin: 0 0 0.25rem;
  color: var(--db-ink-soft);
}
.rate-form input { width: 100%; }
.rate-form .field-hint { display: block; margin-top: 0.25rem; font-size: 0.75rem; }
/* daterange.js cache le champ de fin et son libellé, puis pose son bouton
   juste après le libellé de début : celui-ci doit donc porter le nom du champ
   (« Dates »), et le bouton occuper toute la largeur comme un champ. */
.rate-form .daterange { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: end; }
.rate-form .daterange > label { flex: 1 1 8rem; margin-bottom: 0; }
.rate-form .daterange-trigger { flex: 1 1 100%; justify-content: flex-start; }

@media (max-width: 900px) {
  .rate-form { grid-template-columns: repeat(2, 1fr); }
  .rate-form .rf-name, .rate-form .rf-dates { grid-column: span 2; }
  .rate-form .rf-num { grid-column: span 1; }
}
@media (max-width: 560px) {
  .rate-form { grid-template-columns: 1fr; }
  .rate-form .rf-name, .rate-form .rf-dates, .rate-form .rf-num { grid-column: auto; }
  .form-actions button { width: 100%; }
}

/* ---- Une carte entière qui est un lien -------------------------------------
   .panel-link n'a longtemps été qu'un nom dans un gabarit : aucune règle ne le
   définissait. Or un <a> est en ligne par défaut, si bien que le fond, la
   bordure et le rayon de .panel se fragmentaient en boîtes de ligne — d'où les
   deux moignons de coins arrondis qui traînaient au-dessus et au-dessous du
   graphique, avant et après le contenu de la carte.

   Le display:block et le survol viennent de .card-link / .card-elevated, la
   convention déjà en place sur les tuiles du guide ; ce qui suit ne traite que
   ce qu'un <a> fait au texte qu'il contient : il le repeint entièrement en
   couleur de lien. Une carte doit se lire comme une carte — le chevron dit
   qu'elle mène ailleurs, pas la couleur de son titre.

   Se pose À CÔTÉ de .panel, jamais à sa place : la carte garde son fond, sa
   bordure et son rayon. */
.panel-link { color: var(--db-ink, var(--text)); }
.panel-link:hover { text-decoration: none; }
.panel-link h2 { color: var(--brand); }
.panel-link .muted { color: var(--muted); }
.panel-link-go {
  display: inline-block;
  font-size: 0.7em;
  margin-left: 0.4rem;
  vertical-align: middle;
  color: var(--db-ink-faint);
  transition: transform .15s ease, color .15s ease;
}
.panel-link:hover .panel-link-go { transform: translateX(3px); color: var(--brand); }

/* ---- La petite courbe de l'année ------------------------------------------
   Douze colonnes dont la hauteur est un pourcentage du meilleur mois. Pas de
   bibliothèque : ce graphique dit une saison, pas des euros — les euros sont
   dans le tableau de détail, et une dépendance de 200 kB pour douze rectangles
   se paierait à chaque page vue. */
.year-bars {
  display: grid; grid-template-columns: repeat(12, 1fr);
  gap: 0.35rem; align-items: end; height: 8.5rem; margin: 0.4rem 0 0.8rem;
}
.year-bar { display: flex; flex-direction: column; align-items: center; height: 100%; gap: 0.3rem; }
.year-bar-track {
  flex: 1; width: 100%; display: flex; align-items: flex-end;
  background: var(--db-surface-2); border-radius: 4px; overflow: hidden;
}
.year-bar-fill {
  width: 100%; background: var(--db-tide, #1c4a58); border-radius: 4px 4px 0 0;
  min-height: 2px;
}
.year-bar-label { font-size: 0.7rem; color: var(--db-ink-faint); white-space: nowrap; }
.year-bar:hover .year-bar-fill { background: var(--db-tide-mid, #2c6274); }


/* La légende des colonnes d'argent, sous le tableau des mois. Écrite en dl
   plutôt qu'en phrases : « Prestations » n'est pas devinable, et une note en
   bas de tableau se lit plus volontiers qu'une infobulle qu'il faut trouver. */
.col-legend { display: grid; gap: 0.35rem 1.4rem; margin: 0.7rem 0 0; font-size: 0.82rem; }
.col-legend > div { display: grid; grid-template-columns: minmax(8rem, 10rem) 1fr; gap: 0.6rem; }
.col-legend dt { font-weight: 600; color: var(--db-ink-soft); }
.col-legend dd { margin: 0; color: var(--db-ink-faint); }
@media (max-width: 640px) {
  .col-legend > div { grid-template-columns: 1fr; gap: 0.1rem; }
}

/* ---- Le bandeau de mesures d'un gîte -------------------------------------
   Cinq mesures sur toute la largeur : occupation, séjours, CA, dû, devis.

   Ce ne sont pas des « cartes » : une carte annonce un objet à ouvrir, or on
   ne clique pas sur 42 %. Valeur, libellé, note — posés sur un bandeau clair,
   séparés par un filet d'un pixel.

   La carte propriétaire occupait un quart de la ligne et poussait les cinq
   mesures dans les trois quarts restants : « 24 887,95 € » s'y faisait couper
   au milieu. Elle est partie dans les Réglages, et la valeur ne se coupe plus
   — nowrap, et une taille qui s'adapte à la place disponible plutôt qu'un
   1,5 rem qui déborde. */
.prop-figures {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px;
  background: var(--db-line-soft, #ece8dc);
  border: 1px solid var(--db-line); border-radius: 12px; overflow: hidden;
  margin-bottom: 1rem;
}
.prop-figure {
  background: var(--db-surface); padding: 0.85rem 1rem;
  display: flex; flex-direction: column; gap: 0.15rem; min-width: 0;
}
.prop-figure-value {
  font-family: var(--db-font-display, Georgia), serif;
  font-size: clamp(1.15rem, 1.6vw, 1.5rem); line-height: 1.15;
  color: var(--db-ink); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.prop-figure-label { font-size: 0.78rem; font-weight: 600; color: var(--db-ink-soft); }
.prop-figure-note { font-size: 0.75rem; color: var(--db-ink-faint); overflow-wrap: anywhere; }
.prop-figure.is-alert .prop-figure-value { color: var(--error, #a3271f); }

@media (min-width: 700px)  { .prop-figures { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1000px) { .prop-figures { grid-template-columns: repeat(5, 1fr); } }

/* ---- L'en-tête d'un gîte et ses quatre vues -------------------------------
   Les boutons sont posés sur la photo et centrés dessous : sur un fond
   photographique, une rangée centrée se lit comme un objet à part entière
   plutôt que comme la suite du titre.

   TOUS les sélecteurs sont préfixés par .prop-hero, et c'est nécessaire :
   app.css porte une règle `.property-header[style*="background-image"] a
   { color: #fff }` — deux classes et un élément — qui l'emportait sur
   `.prop-area-pick.is-active`, deux classes. Le bouton actif recevait donc du
   blanc sur blanc. Trois classes reprennent la main. C'est la même leçon que
   le `.fa-solid { display: inline-block }` de Font Awesome : sur une feuille
   qu'on ne contrôle pas entièrement, la spécificité se compte, elle ne
   s'espère pas. */
/* Tout est centré, pas seulement les boutons : un titre à gauche au-dessus
   d'une rangée centrée donne deux axes dans un bloc de six centimètres de
   haut, et l'œil ne sait plus lequel suivre. */
.prop-hero { padding: 1.5rem 1.75rem 1.25rem; text-align: center; }
.prop-hero h1 { margin-bottom: 0.15rem; }
.prop-hero .prop-hero-address { margin: 0 0 0.9rem; font-size: 0.92rem; }

.prop-hero .prop-areas {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  justify-content: center;
}
.prop-hero .prop-area-pick {
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.4rem 0.9rem; border-radius: 999px;
  font-size: 0.85rem; font-weight: 600; letter-spacing: 0.01em;
  text-decoration: none;
  color: #fff; background: rgba(12, 24, 20, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.35);
  backdrop-filter: blur(2px);
}
.prop-hero .prop-area-pick:hover {
  background: rgba(12, 24, 20, 0.75); text-decoration: none; color: #fff;
  border-color: rgba(255, 255, 255, 0.6);
}
/* L'actif : blanc plein, encre presque noire. Pas var(--brand), qui est un
   bleu-vert foncé lisible sur blanc mais qui, ici, se faisait écraser par la
   règle d'app.css ci-dessus — et qui, même gagnante, contraste moins. */
.prop-hero .prop-area-pick.is-active {
  background: #fff; color: var(--db-ink); border-color: #fff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
}
.prop-hero .prop-area-pick i { font-size: 0.82em; }
.prop-hero .prop-area-pick .prop-area-ext { font-size: 0.62em; opacity: 0.8; margin-left: 0.1rem; }

/* Sans photo, l'en-tête est un bloc clair : les boutons y reprennent le
   traitement discret du reste du desk, sinon on aurait du blanc sur blanc —
   pour de bon, cette fois. */
.property-header:not([style*="background-image"]) .prop-area-pick {
  color: var(--db-ink-soft); background: transparent; border-color: var(--db-line);
  box-shadow: none;
}
.property-header:not([style*="background-image"]) .prop-area-pick:hover {
  background: var(--db-surface-2); color: var(--db-ink);
}
.property-header:not([style*="background-image"]) .prop-area-pick.is-active {
  background: var(--db-ink); color: var(--db-surface); border-color: var(--db-ink);
}

/* L'autre métier du même gîte, à droite de la barre. Sans lui, aller des
   photos aux tarifs repasse par l'Aperçu : deux clics et un changement de
   page pour traverser cinquante pixels. Discret par défaut — c'est une
   sortie, pas une invitation — et il s'efface sur mobile, où le retour à
   l'Aperçu suffit et où la place manque. */

/* ---- L'atelier commercial -------------------------------------------------
   Le même bandeau que les onglets d'un gîte, à l'autre zoom : là un gîte et
   ses métiers, ici un métier et tous les gîtes. Il vit sous le h1 de
   /offre-commerciale et de ses trois onglets. */
.prop-tabs {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  margin: 0.2rem 0 1rem; padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--db-line);
}
.prop-tab {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.4rem 0.8rem; border-radius: 999px;
  font-size: 0.85rem; font-weight: 600;
  color: var(--db-ink-soft); text-decoration: none;
  border: 1px solid transparent;
}
.prop-tab:hover { background: var(--db-surface-2); color: var(--db-ink); text-decoration: none; }
.prop-tab.is-active {
  background: var(--db-surface-2); border-color: var(--db-line); color: var(--db-ink);
}

/* ---- /compta : le pipeline de facturation ---------------------------------
   Les quatre états d'une facture, en tête de page, chacun avec son compte et
   son montant. Ce n'est pas une décoration : c'est la table des matières de
   la page, et le seul endroit où l'on voit le mois entier d'un coup d'œil. */
.pipe {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px;
  background: var(--db-line);
  border: 1px solid var(--db-line);
  border-radius: 14px;
  overflow: hidden;
  margin-bottom: 2rem;
  box-shadow: var(--db-shadow-sm);
}
.pipe-stage {
  display: flex; flex-direction: column; gap: 0.25rem;
  padding: 1rem 1.1rem;
  background: var(--db-surface);
  color: inherit; text-decoration: none;
}
.pipe-stage:hover { background: var(--db-surface-2); text-decoration: none; }
.pipe-n { font-size: 1.5rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.pipe-label { font-size: 0.74rem; font-weight: 600; color: var(--db-ink-soft); }
.pipe-sum { font-size: 0.82rem; color: var(--db-ink-soft); font-variant-numeric: tabular-nums; }
/* Une étape vide n'est pas une alerte : seules celles qui attendent quelque
   chose portent la couleur. */
.pipe-stage.is-live .pipe-n { color: var(--db-tide); }

.ci-table { table-layout: fixed; width: 100%; }
.ci-table th:nth-child(1), .ci-table td:nth-child(1) { width: 10rem; }
.ci-table th:nth-child(4), .ci-table td:nth-child(4) { width: 8rem; }
.ci-table th:nth-child(5), .ci-table td:nth-child(5) { width: 9rem; }
.ci-table th:nth-child(6), .ci-table td:nth-child(6) { width: 7.5rem; }
.ci-table th:nth-child(7), .ci-table td:nth-child(7) { width: 7rem; }
.ci-table td.cell-primary { min-width: 0; font-variant-numeric: tabular-nums; }

@media (max-width: 640px) {
  .ci-table { table-layout: auto; }
  .ci-table th, .ci-table td { width: auto; }
}

/* La première étape a une colonne de plus que les suivantes et pas de numéro
   de facture : elle décrit ce qui n'est pas encore un document. */
.cf-table th:nth-child(1), .cf-table td:nth-child(1) { width: auto; }
.cf-table th:nth-child(2), .cf-table td:nth-child(2) { width: 9rem; }
.cf-table th:nth-child(3), .cf-table td:nth-child(3) { width: 9rem; }
.cf-table th:nth-child(4), .cf-table td:nth-child(4) { width: 13rem; }
.cf-table th:nth-child(5), .cf-table td:nth-child(5) { width: 8rem; }
@media (max-width: 640px) { .cf-table th, .cf-table td { width: auto; } }

/* Deux panneaux sur une rangée : la taxe de séjour et la restauration.
   Elles ne se ressemblent pas, mais occupent la même place dans la lecture
   d'un séjour — ce qui s'ajoute à la nuitée et qu'il faut vérifier. */
.bk-side-by-side { display: grid; gap: 1rem; grid-template-columns: 1fr; margin: 0 0 1.25rem; }
.bk-side-by-side > .panel { margin: 0; }

/* .bk-cleaning-line remonte de 0,5 rem pour se coller au panneau qui la
   précède — une marge négative réglée à l'époque où c'était un .panel avec sa
   propre marge basse. La grille ci-dessus retire celle de ses panneaux, alors
   la ligne remontait dans les cartes. Elle reprend une marge normale ici. */
.bk-side-by-side + .bk-cleaning-line { margin-top: 0; }

@media (min-width: 60rem) {
  .bk-side-by-side { grid-template-columns: 1fr 1fr; align-items: start; }
}

/* Un panneau sans contenu à montrer : présent, et visiblement en attente.
   Estompé plutôt que masqué — c'est ce qui distingue « il n'y en a pas » de
   « l'écran ne sait pas le montrer ». */
.panel-empty { background: transparent; border-style: dashed; box-shadow: none; }
.panel-empty h2 { color: var(--db-ink-faint); }

/* ---------------------------------------------------------------- 15-markdown-editor.css */
/* ==========================================================================
   15 — L'éditeur de contenu
   ==========================================================================
   LES JETONS SONT CEUX DU BUREAU. Cette feuille était écrite avec ceux du
   site public — --card, --accent, --ink, --serif — qui n'existent pas ici :
   le bureau a --panel-bg, --brand, --text, --db-font-display. Une propriété
   dont la valeur est une variable inconnue est simplement invalide, et rien
   ne le signale : la fenêtre d'aperçu n'avait donc pas de fond (on voyait la
   page du bureau autour de la vue téléphone) et « outline: 2px solid
   var(--accent) » ne dessinait aucun anneau de focus. Voir le test
   TestStylesheetVariablesAreDefined dans css_test.go.

   La page Vitrine > Contenu : cinq champs, deux langues côte à côte, et
   l'aperçu de la vraie page dans une fenêtre.

   Le champ reste un <textarea> : la correction orthographique du navigateur
   est ce que ces zones ont de plus utile — plusieurs centaines de mots de
   français par gîte — et c'est précisément ce qu'un éditeur en contenteditable
   rend approximatif. La barre d'outils est posée autour par
   static/markdown-editor.js ; sans le JS, tout ce qui suit ne s'applique à
   rien et le champ fonctionne. */

/* ------------------------------------------------------------------ la page */

/* Une colonne, alignée à gauche. La version précédente mettait « où va ce
   texte » dans une deuxième colonne flottante à droite : à cette largeur elle
   passait à la ligne et se retrouvait centrée sous le paragraphe, sans y
   toucher — un bloc de texte posé au milieu de rien. */
.content-head{max-width:74ch;margin-bottom:1.4rem;}
.content-head h2{margin-bottom:.3rem;}
.content-head p{margin:0 0 .4rem;}
.content-head-where{font-size:.86rem;}

.content-toolbar{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;
  padding-bottom:.9rem;margin-bottom:1.1rem;border-bottom:1px solid var(--border);}
.content-toolbar-actions{margin-left:auto;display:flex;gap:.5rem;}

/* Un champ : son titre et son aide à gauche de rien, ses deux langues en
   dessous. Le titre porte le nom que la section prend sur le site — « La
   maison », « Le quartier » — pour qu'on écrive en sachant sous quel titre
   ça paraîtra. */
.content-field{padding:1.4rem 0;border-bottom:1px solid var(--border);}
.content-field:last-of-type{border-bottom:0;}
.content-field-head h3{font-size:1.05rem;margin:0 0 .2rem;}
/* .field-hint porte déjà sa couleur et sa taille (01-base) : ici seulement
   ce qui change dans ce contexte-ci. */
.content-field-head .field-hint{margin:0 0 .5rem;max-width:70ch;}
/* Le reste de l'aide est replié : on le lit une fois. Cinq pavés gris
   permanents, c'était la moitié de la page entre l'auteur et ses champs. */
.field-more{margin:0 0 .9rem;}
/* Le marqueur par défaut du navigateur est un triangle noir posé devant le
   texte : dans une page qui n'en a pas un seul autre, il se lit comme une
   sortie de débogage. Un chevron, à la taille du texte qu'il ouvre. */
.field-more summary{cursor:pointer;color:var(--brand);font-size:.82rem;width:max-content;
  list-style:none;display:inline-flex;align-items:center;gap:.35rem;}
.field-more summary::-webkit-details-marker{display:none;}
.field-more summary::before{content:"";width:.4rem;height:.4rem;border-right:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .12s ease;}
.field-more[open] summary::before{transform:rotate(45deg);}
.field-more summary:hover{text-decoration:underline;}
.field-more p{margin:.5rem 0 0;font-size:.86rem;color:var(--muted);max-width:70ch;}

/* Les deux langues côte à côte : l'anglais s'écrit contre le français, pas à
   sa place. Le sélecteur ne fait que replier une colonne — les deux champs
   restent dans le formulaire, donc replier ne peut rien perdre. */
/* minmax(0,1fr), not 1fr. They are the same until something inside a column
   refuses to be narrow, and then they are not: `1fr` is `minmax(auto,1fr)`,
   and that `auto` is the column's min-content — a floor the track cannot go
   under, whatever the other column needs.

   The floor was reached by the one thing that only appears when a field is
   empty in English and filled in French: the line quoting what the site still
   shows, which is `white-space:nowrap` so as to be elided rather than wrapped.
   Elided it was not. The English track grew to the length of that sentence,
   the French track was left with what remained — some forty pixels — and the
   French column rendered its toolbar as a vertical stack and its text one
   letter per line. The gîte with no English translation is exactly the gîte
   whose page you have come to write, so it was the normal case that broke.

   With an explicit 0 minimum the two tracks are halves whatever they contain,
   and what does not fit is the overflowing element's problem — which is what
   min-width:0 below hands it. */
.content-cols{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1.1rem;}
/* A grid item is min-width:auto by default, so a column would still be held
   open from the inside. This is what actually lets the ellipsis happen. */
.content-col{min-width:0;}
.content-panel[data-cols="fr"] .content-cols,
.content-panel[data-cols="en"] .content-cols{grid-template-columns:1fr;}
.content-panel[data-cols="fr"] .content-col[data-col="en"],
.content-panel[data-cols="en"] .content-col[data-col="fr"]{display:none;}
/* Une seule langue affichée : le libellé de colonne ne dit plus rien que le
   sélecteur ne dise déjà. */
.content-panel:not([data-cols="both"]) .content-col-label{display:none;}
@media (max-width:900px){.content-cols{grid-template-columns:1fr;}}

.content-col-label{display:block;font-size:.78rem;text-transform:uppercase;letter-spacing:.04em;
  color:var(--muted);margin-bottom:.3rem;}
/* Les champs grandissent avec leur texte (content-editor.js). Une zone de
   trois lignes qui coupe une phrase au milieu et rend une barre de défilement
   demande de faire défiler pour relire ce qu'on vient d'écrire — sur cinq
   champs et deux langues, c'est dix endroits où le texte est caché par la
   fenêtre censée le montrer. */
.content-col textarea{display:block;width:100%;box-sizing:border-box;
  border:1px solid var(--border);border-radius:8px;padding:.7rem .85rem;
  background:var(--panel-bg);overflow:hidden;resize:none;min-height:5.5rem;
  font-family:var(--db-font-display, Georgia), serif;font-size:.98rem;line-height:1.65;}
.content-col textarea:focus{outline:2px solid var(--brand);outline-offset:-2px;}

.content-col-foot{display:flex;gap:.8rem;align-items:baseline;flex-wrap:wrap;
  margin-top:.35rem;min-height:1.2rem;}
.word-count{font-size:.78rem;color:var(--muted);font-variant-numeric:tabular-nums;}

/* Vide ici ne veut pas dire vide sur le site : la page retombe sur le front
   matter d'avant la migration. Le champ cite donc ce que le site affiche
   encore, faute de quoi « j'ai effacé » et « c'est effacé » se ressemblent
   trop. Ambre et non rouge : ce n'est pas une erreur, c'est une surprise. */
.field-fallback{font-size:.78rem;color:#8a6d3b;flex:1 1 20ch;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.field-fallback i{margin-right:.25rem;}

/* ------------------------------------------------------ enregistrer, ou pas */

/* La barre suit : le formulaire fait deux écrans et demi, et un bouton tout
   en bas demande de faire défiler pour savoir si ce qu'on vient d'écrire est
   en sécurité. */
/* Les marges négatives reprennent exactement le rembourrage de .panel
   (1.25rem 1.5rem) : sinon la barre ne va pas jusqu'aux bords, son fond ne
   couvre rien, et le bouton flotte au-dessus du champ suivant — ce qu'il
   faisait. */
.content-actions{position:sticky;bottom:0;z-index:5;
  display:flex;align-items:center;gap:1rem;justify-content:flex-end;
  margin:1.4rem -1.5rem -1.25rem;padding:.85rem 1.5rem;
  /* Un fond légèrement distinct de celui du panneau, et une vraie ligne :
     à fond identique et bordure discrète, la barre disparaissait et il ne
     restait qu'un bouton flottant au-dessus du champ suivant. */
  background:var(--bg);border-top:1px solid var(--border);
  border-radius:0 0 var(--radius-lg,16px) var(--radius-lg,16px);}
/* L'état à gauche est toujours écrit : une barre qui ne porte qu'un bouton
   ne dit pas si le travail est en sécurité, et c'est la seule chose qu'on
   veut savoir en écrivant. */
.save-state{margin-right:auto;font-size:.86rem;color:var(--muted);
  display:inline-flex;align-items:center;gap:.4rem;}
.content-actions.is-dirty{background:#fdf6e3;border-top-color:#e0cf9a;}
.content-actions.is-dirty .save-state{color:#8a6d3b;font-weight:500;}

/* ------------------------------------------------------------- l'aperçu */

/* La vraie page du gîte, presque plein écran : ce que l'auteur veut voir,
   c'est la page, pas le paragraphe. Le cadre est en sandbox — les scripts du
   site (la visionneuse de photos) ne servent à rien pour juger un texte, et
   le document est assemblé à partir du corps d'une requête. */
.preview-dialog{width:96vw;max-width:96vw;height:94vh;padding:0;border:0;border-radius:12px;
  background:var(--panel-bg);box-shadow:0 24px 60px rgba(0,0,0,.35);overflow:hidden;}
.preview-dialog::backdrop{background:rgba(20,18,16,.55);}
/* La barre est claire, pas pierre : la piste du sélecteur segmenté est en
   pierre, et une piste pierre sur une barre pierre ne se voit pas. */
.preview-bar{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;
  padding:.55rem .9rem;border-bottom:1px solid var(--border);background:var(--panel-bg);}
.preview-bar-spacer{flex:1 1 auto;}
.preview-note{font-size:.78rem;color:#8a6d3b;border:1px solid #e0cf9a;background:#fdf6e3;
  border-radius:999px;padding:.1rem .6rem;}
.preview-dialog iframe{display:block;width:100%;height:calc(94vh - 3.1rem);border:0;background:#fff;
  margin:0 auto;transition:width .15s ease;}
/* 390px, la largeur d'un téléphone courant : un paragraphe qui se lit bien en
   1200 peut être un mur en 390, et c'est là que ces pages sont lues. */
.preview-dialog[data-width="phone"] iframe{width:390px;height:calc(94vh - 4.6rem);
  margin-top:.75rem;border-radius:14px;
  box-shadow:0 0 0 1px var(--border),0 10px 30px rgba(20,18,16,.18);}

/* ------------------------------------------------------- la barre d'outils */

.md-editor{border:1px solid var(--border);border-radius:8px;overflow:hidden;background:var(--panel-bg);}
.md-editor textarea{display:block;width:100%;box-sizing:border-box;border:0;border-radius:0;
  padding:.8rem .9rem;resize:vertical;background:transparent;
  /* Du texte, pas du code. Le monospace était l'idiome d'un éditeur de
     programme appliqué à de la prose française : il fatigue l'œil sur trois
     cents mots et donne une fausse idée de la longueur des lignes publiées. */
  font-family:var(--db-font-display, Georgia), serif;font-size:1rem;line-height:1.7;}
.md-editor textarea:focus{outline:2px solid var(--brand);outline-offset:-2px;}

.md-toolbar{display:flex;align-items:center;gap:.15rem;padding:.3rem .35rem;
  background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;}
.md-toolbar-spacer{flex:1 1 auto;}
.md-sep{width:1px;align-self:stretch;margin:.2rem .35rem;background:var(--border);}

.md-btn{display:inline-flex;align-items:baseline;gap:.1rem;min-width:1.9rem;height:1.9rem;
  padding:0 .45rem;justify-content:center;border:1px solid transparent;border-radius:5px;
  background:transparent;color:var(--text);cursor:pointer;font-size:.82rem;line-height:1.9rem;}
.md-btn:hover{background:var(--panel-bg);border-color:var(--border);}
/* Le focus clavier doit rester visible : la barre se parcourt à la tabulation
   comme le reste du formulaire. */
.md-btn:focus-visible{outline:2px solid var(--brand);outline-offset:1px;}
.md-btn.is-on{background:var(--brand);border-color:var(--brand);color:#fff;}
/* Chaque lettre porte l'effet qu'elle applique : le bouton gras est gras, le
   bouton italique est en italique. C'est le libellé le plus court possible et
   il n'a besoin d'aucune police d'icônes. */
.md-btn-bold{font-weight:800;}
.md-btn-italic{font-style:italic;font-family:var(--db-font-display, Georgia), serif;}
.md-btn-strike{text-decoration:line-through;}
.md-btn-h3,.md-btn-h4{font-weight:700;font-size:.74rem;letter-spacing:.02em;}
.md-btn-ul,.md-btn-quote{font-size:1rem;line-height:1.7rem;}

/* Plein écran pour les champs longs : « La maison » fait seize lignes dans un
   formulaire qui en fait deux cents, et on écrit mieux sans le reste. */
.md-editor.is-zen{position:fixed;inset:3vh 8vw;z-index:60;display:flex;flex-direction:column;
  box-shadow:0 24px 60px rgba(0,0,0,.35);}
.md-editor.is-zen textarea{flex:1 1 auto;height:auto;font-size:1.05rem;padding:1.6rem 2rem;}
body.has-zen-editor{overflow:hidden;}
body.has-zen-editor::after{content:"";position:fixed;inset:0;z-index:59;background:rgba(20,18,16,.5);}

/* -------------------------------------------------------- le sélecteur */

/* Un contrôle segmenté : une piste, un curseur posé dessus. Trois options
   qui s'excluent, et une seule question — « qu'est-ce que je regarde ».
   Il sert ici pour les colonnes de langue, et dans la fenêtre d'aperçu pour
   la langue et la largeur.
 *
 * IL FAUT D'ABORD DÉFAIRE LE BOUTON. La règle générale de 01-base.css donne
 * à tout <button> l'identité du bouton d'action principal : fond de la
 * marque, rayon de 10px, graisse 600, marge haute de 0.9rem, et un survol
 * bleu nuit. Une feuille qui n'en redéfinit qu'une partie donne exactement ce
 * qu'on avait — trois pastilles arrondies posées dans une boîte arrondie,
 * toutes en gras, et un survol qui peignait le segment en bleu nuit sous un
 * texte gris.
 *
 * ET LE CLAIR DOIT ÊTRE LE CHOISI. La version précédente mettait le segment
 * actif en pierre (#f1efe7) et les autres en blanc : l'option retenue avait
 * l'air enfoncée et désactivée, les deux autres avaient l'air disponibles et
 * proéminentes. C'est l'inverse de ce que fait un contrôle segmenté depuis
 * qu'il en existe — la piste est le fond sombre, le curseur est la surface
 * claire qui se détache dessus. */
.segmented{display:inline-flex;gap:2px;padding:3px;
  background:var(--bg);border:1px solid var(--border);border-radius:9px;line-height:1;}
.segmented button{
  margin:0;padding:.36rem .9rem;min-height:1.9rem;
  border:0;border-radius:6px;background:transparent;
  color:var(--muted);font-size:.84rem;
  /* La graisse ne change pas d'un état à l'autre : passer de 500 à 600
     élargit le mot, et le contrôle se réarrange sous le curseur à chaque
     clic. Le fond et l'encre disent l'état, la largeur ne bouge pas. */
  font-weight:500;
  transition:background .12s ease,color .12s ease;}
.segmented button:hover{background:rgba(23,35,42,.05);color:var(--text);}
.segmented button.is-on,
.segmented button.is-on:hover{
  background:var(--panel-bg);color:var(--text);
  box-shadow:0 1px 2px rgba(20,18,16,.10),0 0 0 1px rgba(20,18,16,.04);}
.segmented button:focus-visible{outline:2px solid var(--brand);outline-offset:1px;}

/* ------------------------------------------------- l'état d'une publication */

/* La publication tourne en arrière-plan et la page la regarde. Le compteur de
   secondes est en chiffres tabulaires : sans cela la ligne se dandine à chaque
   passage de 9 à 10. */
.publish-live{margin:0 0 1rem;}
.publish-elapsed{font-variant-numeric:tabular-nums;color:var(--muted);}
/* .publish-summary porte déjà sa mesure (01-base, pour l'historique) : ici
   seulement l'écart qui la sépare de l'état au-dessus. */
.publish-live .publish-summary{margin-top:.6rem;}
/* Un bouton désactivé doit se voir désactivé, sinon le second clic vient. */
[data-publish-form] button[disabled]{opacity:.55;cursor:not-allowed;}

