/* ══════════════════════════════════════════════════════════════════════════════
   components.css — the reusable pieces
   ══════════════════════════════════════════════════════════════════════════════

   Layer 2 of 3. Every primitive is defined here exactly once. A module
   stylesheet may compose these; it must never redefine one.

   Rule for anyone adding to this file: if a class here needs a `display` value,
   remember that an author `display` rule beats the UA stylesheet's
   `[hidden] { display: none }` at equal specificity. That is what silently broke
   a JS visibility toggle in the previous project. The `[hidden]` rule at the
   bottom of this file re-asserts it with enough weight to win, so `element.hidden`
   works everywhere in this codebase.
   ────────────────────────────────────────────────────────────────────────── */

/* ── Card ───────────────────────────────────────────────────────────────────*/

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

.card-pad { padding: var(--gutter); }

.card-head {
  padding: 11px 14px;
  border-bottom: 1px solid var(--line-2);
  background: var(--surf);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-3);
}

/* ── Page heading ───────────────────────────────────────────────────────────*/

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

/* A page whose heading is elsewhere — the patient-scoped screens, where §2.1's
   identity card is the `<h1>`. Only the return control is left, so it goes to
   the right and the strip stops claiming a heading's worth of space. */
.page-head-bare {
  justify-content: flex-end;
  align-items: center;
  margin-bottom: var(--gap-s);
}

.page-title {
  font-family: var(--font-display);
  font-size: var(--fs-h);
  font-weight: 600;
  color: var(--ink);
}

.page-sub {
  font-size: 12px;
  color: var(--ink-3);
  margin-top: 2px;
}

.section-label {
  font-size: 10.4px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--ink-3);
  font-weight: 700;
  margin-bottom: 9px;
}

/* A sentence about a whole SECTION, under its label and above its fields —
   §4.2's « renseignez au moins un numéro de téléphone », which is true of the
   three slots and the person to call together and of no single field among
   them. Sentence case and readable, unlike the label above it, because it is
   prose rather than a heading. */
.section-note {
  margin: -4px 0 var(--gap-s);
  color: var(--ink-3);
  font-size: var(--fs-s);
}

/* ── Button ─────────────────────────────────────────────────────────────────
   One base class plus modifiers. `.btn` alone is the neutral, safe action;
   colour is added only where the action deserves emphasis.

   Minimum height 34px so a control is reliably tappable on a tablet, which
   §0.9 names as a fully supported target rather than a degraded desktop. */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-s);
  min-height: 34px;
  padding: 8px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  background: var(--card);
  color: var(--ink-2);
  font-size: var(--fs-s);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .12s, background .12s, color .12s;
}

.btn:hover { border-color: var(--brand); color: var(--brand-dk); text-decoration: none; }

/* Disabled must still be readable and must still explain itself. §5.1 relies on
   this: the arrival button is disabled with a one-line reason beside it when the
   viewed date is not today, and the disabled state is meant to teach the rule.
   So: dimmed, not invisible, and `cursor: not-allowed` rather than `none`. */
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: .55;
  cursor: not-allowed;
  border-color: var(--line);
  color: var(--ink-3);
}

.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-dk); color: #fff; }

/* Amber. For actions that create something new. */
.btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-accent:hover { background: var(--accent-dk); color: #fff; }

/* Destructive. Never the default action in a group, never focused first. */
.btn-danger { background: transparent; border-color: var(--err-line); color: var(--err); }
.btn-danger:hover { background: var(--err-bg); border-color: var(--err); color: var(--err); }

.btn-quiet { background: transparent; border-color: transparent; color: var(--brand-dk); }
.btn-quiet:hover { background: var(--line-2); }

.btn-small { min-height: 28px; padding: 5px 9px; font-size: 11.2px; border-radius: 6px; }
/* A full-width button in a grid cell, which is the one shape that may hold a
   sentence rather than a word — §6.3 step 8's « Ajouter / modifier le
   diagnostic oncologique » in a `minmax(220px, 1fr)` track.
   ⚠ `.btn` is `white-space: nowrap`, built for a short inline control, so the
   label ran straight out of the box ("text out of limits", found by the user).
   Overridden **here on the modifier** rather than on `.btn` itself: §9 traps
   13, 23 and 26 are all the same lesson — change the one property the new use
   needs, never the primitive's. `min-height` still holds a one-line button at
   exactly the height it had. */
.btn-block {
  width: 100%;
  justify-content: flex-start;
  white-space: normal;
  text-align: left;
}

/* ── Badge ──────────────────────────────────────────────────────────────────
   Always carries a word, never colour alone: colour-blind users and a
   sun-washed tablet screen are both real here. */

/* The heading of a card, one step down from a page title. Promoted out of
   oncologie.css in build step 8, when the library screens became its second
   consumer — the layering rule CLAUDE.md §10 point 4 sets out, and the same
   move `.facts` made in build step 6a. */

.card-title {
  font-size: var(--fs);
  font-weight: 700;
  color: var(--ink-2);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  font-size: 10.7px;
  font-weight: 700;
  white-space: nowrap;
}

.badge-ok { background: var(--ok-bg); color: var(--ok); }
.badge-info { background: var(--info-bg); color: var(--info); }
.badge-warn { background: var(--warn-bg); color: var(--warn); }
.badge-err { background: var(--err-bg); color: var(--err); }
.badge-neutral { background: var(--line-2); color: var(--ink-3); }

/* ── Form controls (§0.7) ───────────────────────────────────────────────────
   "All choice fields must be styled consistently with the rest of the
   application — not the browser's default dropdown. Same border radius, border
   color, height, font, and focus ring as text inputs. Custom chevron."

   The classes are applied by AliceFormMixin in core/forms.py, not written into
   each template, so a new form cannot forget them. */

.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.field > label {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: .03em;
}

/* ⚠️ A LONE CHECKBOX MUST NOT STRETCH, and until now every one of them did.
   `AliceFormMixin` puts `.alice-checkbox` on a `CheckboxInput` and nothing
   styled it, so the input inherited `.field`'s column stretch and measured
   **827px** on §5.7.1 — and a stretched `<input type=checkbox>` renders its
   tick CENTRED in that width, four hundred pixels from the caption it belongs
   to. The user found it on « Antinéoplasique » and drew the arrow: « the text
   is too far away from the box to tick and it is not intuitive what to do with
   it. » Forty-nine boolean fields across the project had it.

   `align-self` rather than a change to `.field`, because the column layout is
   right for every other control — this is the one that must not fill it. Same
   rule as §9 traps 13, 23 and 26: change the property the new use needs, never
   the primitive's own. */
input.alice-checkbox {
  align-self: flex-start;
  width: auto;
  min-height: 0;
  margin: 2px 0 0;
}

/* A required field is marked inline as well as in the panel. §0.8 is explicit
   that the panel is the primary mechanism and the inline marker is secondary —
   so this is a quiet asterisk, not a red border on an untouched field. */
.field-required > label::after {
  content: " *";
  color: var(--accent-dk);
}

.alice-input,
.alice-select,
.alice-textarea {
  width: 100%;
  min-height: 36px;
  padding: 8px 11px;
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  background: var(--surf);
  font-size: 12.6px;
  color: var(--ink);
}

.alice-input:focus,
.alice-select:focus,
.alice-textarea:focus {
  outline: 2px solid var(--brand-lt);
  outline-offset: -1px;
  background: var(--card);
}

.alice-textarea { resize: vertical; min-height: 62px; line-height: 1.5; }

/* The custom chevron is an inline data: URI, not an external asset or an icon
   font — nothing in this application may make a network request for a glyph. */
.alice-select {
  appearance: none;
  cursor: pointer;
  font-weight: 500;
  padding-right: 32px;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%236B7773' stroke-width='1.8' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 11px;
}

/* A field the current role may not write renders as read-only content, not as a
   disabled input. §5.6: "a greyed-out form invites clicking; a clean read-only
   panel reads as reference material, which is what it is." */
.field-readonly .field-value {
  padding: 8px 0;
  font-size: 12.6px;
  font-weight: 600;
  color: var(--ink);
}

.field-error { color: var(--err); font-size: 11.5px; }
.field-help { color: var(--ink-3); font-size: 11.5px; }

/* ── A one-time code, to be read aloud ────────────────────────────────────────
 * Was the whole of `core/css/comptes.css`, moved here in build step 16 phase D
 * on the same rule as the two blocks below: a SECOND module now shows a code
 * somebody dictates — §3.2's account code, and phase C's satellite enrolment
 * code. §8's synchronisation console was already using all four of these names
 * while linking only `administration.css`, so the enrolment code — the one thing
 * on that screen an administrator has to read down a telephone — rendered as
 * plain body text in a plain paragraph.
 *
 * `comptes.css` is now deleted rather than left empty: an empty fourth layer
 * that two templates still link is a file the next reader has to open to learn
 * it says nothing.
 */

.code-callout {
  margin-bottom: var(--gutter);
  /* Accent, not a status colour. The code is neither an error nor a success —
     it is a thing to act on, and --accent is what this application uses to mark
     "you are meant to do something with this". */
  border-color: var(--accent);
  border-left-width: 3px;
  background: var(--accent-lt);
}

.code-callout-for {
  color: var(--ink-2);
  font-size: var(--fs-s);
  margin-bottom: 8px;
}

/* The code itself. Large, monospaced and letter-spaced because its first job is
   to be read aloud accurately down a hospital phone — that is the whole reason
   the alphabet excludes 0/O and 1/I/L (accounts/services.py). */
.code-value {
  font-family: var(--font-mono);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: 0.09em;
  color: var(--ink);
  /* Selectable and easy to hit: an administrator reading this over the phone
     will also want to copy it into a message. */
  user-select: all;
  padding: 10px 12px;
  border: 1px solid var(--accent);
  border-radius: var(--r-s);
  background: var(--card);
  display: inline-block;
  margin-bottom: 10px;
}

.code-callout-note {
  color: var(--ink-2);
  font-size: var(--fs-s);
  max-width: 60ch;
}

/* ── A muted word beside a stronger one, and a failure sentence ───────────────
 * Moved DOWN from `backup/css/sauvegarde.css` in build step 16 phase D, on the
 * rule session 53 set: a component moves to this layer when its consumer count
 * passes one. They were `.bk-muted` and `.bk-reason`, and §8's synchronisation
 * console had been borrowing both — while linking only `administration.css`, so
 * on that screen they styled NOTHING. A class from another app's stylesheet is
 * an invented class wherever that stylesheet is not loaded, and it fails the
 * same silent way.
 */

/* A username beside a display name, a file name beside a state. Never the sole
 * carrier of a clinical fact — `--ink-3` is the lightest ink that may carry
 * meaning at all (`base.css`). */
.inline-muted {
  color: var(--ink-3);
  font-size: 0.85em;
}

/* Why one thing failed, under its own badge or row. ⚠️ Deliberately NOT a
 * banner: it belongs to a single row, and `.banner` is `display: flex`, which
 * inside a table cell is §9 trap 23 exactly. */
.error-note {
  margin-top: 0.25rem;
  max-width: 26rem;
  color: var(--err);
  font-size: 0.85em;
}

/* §5.4 step 6's live echo — what the figure just typed comes to in the other
   unit (D249). `--ink-2` and not `--ink-3`: this carries a number a clinician
   reads, and `base.css` calls `--ink-3` the lightest that may carry meaning
   (D161). Written by `alice.js` §17 and by nothing else; it is display copy,
   never a form value, so with no script it is simply absent. */
.dose-echo { display: block; color: var(--ink-2); font-size: 11.5px; margin-top: 2px; }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 13px; }
.form-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 13px; }
/* A heading over a run of fields that answer one question — §5.4 step 8's
   « Dose administrée », which on a fixed combination is a pair rather than one
   figure. Sized as a field label rather than as a section head: it sits inside
   a form grid, and `.section-head` would shout it (§9 trap 20). */
.form-group-head {
  margin: var(--gap-s) 0 0;
  font-size: var(--fs-s);
  font-weight: 600;
  color: var(--ink);
}

.form-grid .field-wide,
.form-grid-3 .field-wide { grid-column: 1 / -1; }

/* `AliceFormMixin.note_text` — a sentence about the run of fields under it,
   which is a <p> rather than the <small class="field-help"> a help text is,
   because it has to be hideable WITHOUT its field (alice.js §7 hides a target's
   whole `.field`, so a conditional help text would take its control with it).
   So it needs the margins a <p> brings and <small> does not. */
.form-note { margin: var(--gap-s) 0 0; }

/* `alice.js` §7b — what a tickbox in a group is ALREADY answering elsewhere,
   appended to its own label. Muted, because it is not part of the choice: it
   is the reason the choice looks like a contradiction and is not one. */
.box-note { color: var(--ink-3); }

/* A choice group and a textarea both need the row: nine checkboxes in half a
   popup lay out one per line, which is the arrangement §4.5's « Arguments »
   was unreadable in. `:has()` rather than a class the form has to remember,
   because a form that forgets it produces exactly that unreadable column and
   nothing says so. */
.form-grid > .field:has(.alice-choice-group),
.form-grid-3 > .field:has(.alice-choice-group) { grid-column: 1 / -1; }

/* Three fields that are one clinical statement — Nottingham's components,
   TNM's categories — sit on one line inside their own row. */
.form-grid > .field:has(.field-inline),
.form-grid-3 > .field:has(.field-inline) { grid-column: span 1; }

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 9px;
  margin-top: var(--gutter);
  padding-top: 14px;
  border-top: 1px solid var(--line-2);
}

/* ── Guidance panel (§0.8) ──────────────────────────────────────────────────
   One panel at the top of the form, one line per missing field, each line a
   link that scrolls to and focuses the control. */

.guidance {
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  border-left: 3px solid var(--accent);
  border-radius: 9px;
  padding: 11px 14px;
  margin-bottom: 15px;
}

.guidance-title {
  margin: 0 0 6px;
  font-size: 12.3px;
  color: var(--accent-dk);
  display: flex;
  align-items: center;
  gap: 7px;
}

.guidance ul { margin: 0; font-size: 12px; color: var(--ink-2); }
.guidance li { margin-bottom: 3px; }
.guidance li a { color: var(--accent-dk); text-decoration: underline; }

/* The step marker on a multi-step form, so a line can say which step to jump
   to as well as which field. */
.guidance .guidance-step {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--accent-dk);
  text-transform: uppercase;
  letter-spacing: .04em;
  margin-right: 5px;
}

/* ── Message banners ────────────────────────────────────────────────────────*/

.banner {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 10px 13px;
  border-radius: 9px;
  border: 1px solid;
  font-size: 12.4px;
  margin-bottom: var(--gap);
}

.banner-ok { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok); }
.banner-info { background: var(--info-bg); border-color: var(--info-line); color: var(--info); }
.banner-warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.banner-err { background: var(--err-bg); border-color: var(--err-line); color: var(--err); }

/* ⚠️ §9 trap 23: `.banner` is `display: flex; align-items: flex-start` — built
   for ONE line of text beside an icon — so a banner carrying block children
   lays them out as columns side by side. Build step 9a met this with a
   three-paragraph duplicate warning; build step 17's merge confirmation is the
   second case, and rather than meet it a third time by accident there is now a
   modifier for it.

   It changes the ONE property the new use needs, and touches neither the
   primitive's padding nor its colour — which is trap 23's own rule. */
.banner-block {
  flex-direction: column;
  gap: 4px;
}

/* Django's own message tags, which are not our four words.
   `messages.success()` renders `banner-success`, not `banner-ok`, so without
   these a success message came out unstyled — visible, but grey and indistinct
   from a neutral notice. The templates interpolate `message.tags` directly, so
   the fix belongs here rather than in a per-template mapping that every future
   template would have to remember to copy. */
.banner-success { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok); }

/*
   A confirmation fades after ten seconds (alice.js §15) — it is news, not
   furniture. Errors and warnings are deliberately not included: they report
   something still to be dealt with.

   The transition is what stops the page jumping under the cursor the instant
   the timer fires; `hidden` follows it and does the actual removal.
*/
.banner-success,
.workspace > .banner-info {
  transition: opacity .35s ease, transform .35s ease;
}

.banner.is-dismissed {
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
}
.banner-warning { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.banner-error { background: var(--err-bg); border-color: var(--err-line); color: var(--err); }
.banner-debug { background: var(--line-2); border-color: var(--line); color: var(--ink-3); }

/* ── Table ──────────────────────────────────────────────────────────────────
   The clinic-style table: dense, quiet rules, an identifier column in mono so
   an ALICE ID can be compared against a paper chart at a glance. */

.table { width: 100%; border-collapse: collapse; font-size: var(--fs-s); }

.table th {
  text-align: left;
  padding: 9px 10px;
  border-bottom: 1px solid var(--line);
  background: var(--surf);
  font-size: 10.2px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--ink-3);
}

.table td { padding: 10px; border-bottom: 1px solid var(--line-2); vertical-align: middle; }
.table tr:hover td { background: var(--surf); }
.table .cell-name { font-weight: 700; color: var(--ink); }
.table .cell-id { font-family: var(--font-mono); font-size: 11.2px; color: var(--ink-3); }

/* Wide tables scroll inside their own container. The page body must never
   scroll horizontally. */
.table-scroll { overflow-x: auto; }

/* ── Empty state ────────────────────────────────────────────────────────────
   An empty state says what is absent and what to do about it — never a bare
   "aucun résultat". */

.empty {
  padding: 26px var(--gutter);
  text-align: center;
  color: var(--ink-3);
  font-size: 12.6px;
}

.empty-title { font-weight: 700; color: var(--ink-2); margin-bottom: 4px; }

/* ── [hidden] ───────────────────────────────────────────────────────────────
   Re-asserted with attribute+class specificity so it beats any author `display`
   rule above. This is the fix for the class of bug that silently broke the
   hospitalisation discharge form's field toggle in the previous project:
   `.regimen-form-field { display: grid }` won against the UA stylesheet and
   `element.hidden = true` did nothing at all.

   With this rule, `element.hidden` works everywhere in this codebase, and no
   future toggle needs to know the history. */
[hidden] { display: none !important; }

/* Same class of bug as `[hidden]` above, found in build step 3 and fixed the
   same way.

   `.visually-hidden` (base.css) collapses an element to 1×1 and clips it, so a
   screen reader still reaches it. But `.alice-select` is defined *later*, in
   this file, at equal specificity — so on the real <select> the type-ahead
   hides, `width: 100%` and `overflow: visible` won. The select stayed clipped
   and therefore invisible, while remaining a full-width absolutely-positioned
   box sitting beside the combo input: the patient-creation form's scrollWidth
   came to 1033px inside a 556px workspace, and every page carrying a type-ahead
   scrolled sideways. §0.9 says that must not happen.

   Re-asserted here rather than "fixed" by reordering the layers, because base ->
   components -> layout -> module is the load order the whole architecture rests
   on. A utility whose entire job is to override needs to be able to.

   Padding and border are reset too, and that is not belt-and-braces: with
   `box-sizing: border-box` a `width: 1px` cannot go below the element's own
   padding plus border, so `.alice-select`'s 44px of horizontal padding kept the
   "1px" select 45px wide — still enough to overhang the workspace. */
.visually-hidden {
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  border: 0 !important;
  overflow: hidden !important;
}

/* ── Date control ───────────────────────────────────────────────────────────
   A text input the user can type into, plus a button that opens the browser's
   own picker (alice.js §9). The wrapper and the button are built by the script,
   so without it the text input renders alone and unstyled by any of this —
   which is exactly the control that was there before.

   The native `<input type="date">` beside it is a picker only: never submitted,
   never focusable, and kept off-screen rather than `display: none` because a
   `display: none` input cannot open a picker in any engine. */

.alice-date { position: relative; display: block; }

/* Room for the button, so a long date never runs underneath it. */
.alice-date .alice-input { padding-right: 40px; }

.alice-date-native {
  position: absolute;
  right: 10px;
  bottom: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.alice-date-button {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: var(--r-s);
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
}

.alice-date-button:hover { background: var(--line-2); color: var(--brand-dk); }

.alice-date-button:focus-visible {
  outline: 2px solid var(--brand-lt);
  outline-offset: 1px;
}

/* ── Type-ahead combobox (§0.7) ─────────────────────────────────────────────
   Built by alice.js over a real <select>, which stays in the DOM and remains
   the element that submits. These styles apply only once the enhancement has
   run; without JavaScript the plain .alice-select above is what renders. */

.alice-combo { position: relative; }

.alice-combo-list {
  position: absolute;
  z-index: 40;
  top: calc(100% + 3px);
  left: 0;
  right: 0;
  max-height: 240px;
  overflow-y: auto;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  box-shadow: var(--sh);
}

.alice-combo-option {
  padding: 7px 10px;
  border-radius: 6px;
  font-size: 12.4px;
  cursor: pointer;
}

/* Hover and keyboard highlight share one appearance, so a user switching
   between mouse and keyboard mid-list does not see two competing cursors. */
.alice-combo-option:hover,
.alice-combo-option.is-active { background: var(--surf); color: var(--brand-dk); }

.alice-combo-option[aria-selected="true"] { font-weight: 700; }

/* ── A TREE INSIDE THE LIST — contract 0.2.40's classification ──────────────

   The user drew it: vertical guides down the left of the list so a reader can
   see which node contains which while they scroll. A `<select>` cannot nest,
   so `ClassTreeSelect` sends `data-depth` and `alice.js` copies it onto the
   row; everything below is one repeating gradient rather than one element per
   level, because a guide is a rule and not a thing to click.

   ⚠️ `--tree-step` is the only number here, and the indent it draws is
   DELIBERATELY not the same value as `services.CLASS_TREE_INDENT`: that one is
   the fallback a plain `<select>` renders with no script, this one is the
   drawn version. Coupling them would mean a stylesheet that cannot be adjusted
   without a Python change.

   ⚠️ **Every row is visible**: the list folded for one pass and the user
   rejected it — see `alice.js` §2. So these guides are the only thing
   carrying the hierarchy, which is why they are a rule and not decoration.

   A list whose options carry no `data-depth` — every other type-ahead in the
   application — matches none of this and is untouched. */
.alice-combo-option[data-depth] {
  --tree-step: 14px;
  position: relative;
  padding-left: calc(10px + var(--depth, 0) * var(--tree-step));
  background-image: repeating-linear-gradient(
    to right,
    var(--line) 0 1px,
    transparent 1px var(--tree-step)
  );
  background-repeat: no-repeat;
  background-position: 14px 0;
  background-size: calc(var(--depth, 0) * var(--tree-step)) 100%;
}

.alice-combo-option[data-depth="0"] { --depth: 0; }
.alice-combo-option[data-depth="1"] { --depth: 1; }
.alice-combo-option[data-depth="2"] { --depth: 2; }
.alice-combo-option[data-depth="3"] { --depth: 3; }
.alice-combo-option[data-depth="4"] { --depth: 4; }
.alice-combo-option[data-depth="5"] { --depth: 5; }
.alice-combo-option[data-depth="6"] { --depth: 6; }

/* The hover background would paint over the guides, so it goes behind them. */
.alice-combo-option[data-depth]:hover,
.alice-combo-option[data-depth].is-active {
  background-color: var(--surf);
}


/* ── The same control over a MULTIPLE select ────────────────────────────────

   « it should be like the "Classe pharmacologique" the way it is presented
   with the same options ». The shell, the search box, the list, the indent and
   the guides are all `.alice-combo`'s already; what this adds is where the
   chosen ones are shown.

   ⚠️ The chips sit ABOVE the search box, not inside it: a chip row growing
   inside the field pushes the caret about and makes the control's height jump
   as classes are picked. */
.alice-combo-multi .alice-combo-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 5px;
}

.alice-combo-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 5px 3px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surf);
  color: var(--ink);
  font-size: 12px;
}

.alice-combo-chip-remove {
  border: 0;
  padding: 0;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-3);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.alice-combo-chip-remove:hover { background: var(--line); color: var(--err); }

/* A chosen row stays in the list and says so, because picking four in a row is
   the ordinary case and the list does not close between them. */
.alice-combo-multi .alice-combo-option[aria-selected="true"] {
  font-weight: 700;
  color: var(--brand-dk);
}

.alice-combo-multi .alice-combo-option[aria-selected="true"]::after {
  content: " \2713";
  color: var(--brand);
}

.alice-combo-empty {
  padding: 9px 10px;
  font-size: 12.2px;
  color: var(--ink-3);
  list-style: none;
}

/* ── Modal editor (§2.5) ────────────────────────────────────────────────────
   "A button opens a popup, you add/edit, you save, the popup closes and the
   underlying panel refreshes. One component, parameterized."

   A native <dialog>, so the backdrop, focus trapping and Escape are the
   browser's. The rules below are appearance only — nothing here is load-bearing
   for behaviour, which is what lets the same markup render as a plain page when
   `alice.js` has not upgraded it (`.modal-sheet`).

   `max-height: 88vh` with the body scrolling and the head and actions fixed:
   the seven §5.5 editors range from four fields to eleven, and on a tablet the
   longest one is taller than the viewport. §0.9 forbids the *page* scrolling
   sideways; a modal that scrolls vertically inside itself is the way a long
   form stays usable there. */

.modal {
  width: min(720px, calc(100vw - 2 * var(--gutter)));
  max-height: 88vh;
  padding: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: 0 10px 40px -12px rgba(20, 45, 42, .35);
  color: var(--ink);
}

.modal::backdrop { background: rgba(20, 45, 42, .38); }

.modal form {
  display: flex;
  flex-direction: column;
  max-height: 88vh;
}

.modal-head {
  padding: var(--gutter);
  border-bottom: 1px solid var(--line-2);
}

.modal-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h);
  font-weight: 600;
  color: var(--ink);
}

.modal-intro {
  margin: 5px 0 0;
  font-size: var(--fs-s);
  color: var(--ink-3);
}

.modal-body {
  overflow-y: auto;
  padding: var(--gutter);
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 9px;
  padding: 12px var(--gutter);
  border-top: 1px solid var(--line-2);
  background: var(--surf);
}

/* The same form rendered as a page rather than in the dialog. It keeps the
   internal spacing and drops the height cap: a page has the whole viewport and
   nothing to scroll inside. */
.modal-sheet .modal-head,
.modal-sheet .modal-body,
.modal-sheet .modal-actions { padding-left: 0; padding-right: 0; }
.modal-sheet .modal-actions { background: transparent; }
.modal-sheet form { max-height: none; }

/* A computed value shown beside the fields it comes from — pack-years (§1).
   Deliberately not styled as an input: it cannot be edited, and a box that
   looks like a field but refuses typing is worse than a plain figure. */
.field-computed {
  margin: 0;
  padding: 8px 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--brand-dk);
}

/* ── Fact list ────────────────────────────────────────────────────────────
   A read-only `label: value` grid, used wherever a panel presents facts about
   one object rather than a table of many: §2.2's tab panels, §4.5's diagnosis
   cards, §4.6's assessment card.

   It lived in `registre.css` from build step 3, when the tab strip was its only
   consumer. Build step 6a made it the third, and CLAUDE.md §10's layering says
   a primitive shared by several modules belongs here — a module stylesheet
   depending on another module's is how one screen's spacing change silently
   moves a different screen.

   `auto-fill` with a floor rather than a fixed column count, because the
   working panel changes width when « Agrandir le panneau » collapses the
   identity column (§2.3), and the grid has to answer that without a media
   query. */
/* ⚠️ **A pair must read as a pair, and it did not.**

   The user: « I still have a problem with this presentation. When I look at it
   directly it is the association in black that my brain does — you need to
   focus too much to identify the "Biologie moléculaire". It is like that almost
   everywhere in the app. »

   The cause is an inversion, and it is arithmetic rather than taste. `.fact`
   was `justify-content: space-between`, which pushes the label hard left and
   the value hard right — 100 to 200px apart inside one pair — while the grid
   put only `--gap` (12px) between one pair and the next. So the two nearest
   things on the line were a *value* and the *following label*, and the eye
   binds nearest neighbours: « Non réalisé Extension métastatique » reads as one
   unit, which is exactly the association the user described.

   So the two distances swap places. The label and its value now sit together
   with `--gap-s` between them, and the columns are `--gutter` apart — and the
   dashed rule under each pair, which already spanned label *and* value, becomes
   the delimiter it was always meant to be rather than a decoration. */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 9px var(--gutter);
  margin: 0;
}

.fact {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* Not `space-between` — see the note on `.facts`. The pair is the unit. */
  justify-content: flex-start;
  gap: 0 var(--gap-s);
  padding-bottom: 5px;
  border-bottom: 1px dashed var(--line-2);
}

/*
   **Both halves must be readable, not just the value.** The user, of the whole
   application rather than of one screen:

       « THESE POLICE ARE NOT VERY EASY TO SEE … I THINK YOU WANTED TO
         EMPHASIZE ON THE REAL VALUE BUT IT IS VERY DIFFICULT TO FOCUS AND
         FOLLOW WHEN THE DESCRIPTIONS ARE DIFFICULT TO SPOT »

   That is exactly what had happened: the label was `--fs-xs` (10.8px) at
   `--ink-3`, which base.css itself describes as « the lightest that may carry
   meaning » — so the emphasis was built by making the label nearly invisible
   rather than by making the value stand out.

   The value keeps the emphasis and gains it honestly, through weight. The
   label moves up a size and a shade, which is what makes a two-column fact
   list scannable in both directions.
*/
.fact dt {
  font-size: var(--fs-s);
  color: var(--ink-2);
  /* Never squeezed to a scrap while the value keeps its width: the label is
     half the fact (D161), and a wrapped one still starts the line. */
  flex: 0 1 auto;
}

.fact dd {
  margin: 0;
  font-size: var(--fs-s);
  font-weight: 600;
  color: var(--ink);
  /* Left, beside its label. Right-alignment is what put it against the next
     pair's label. */
  text-align: left;
}


/* ── Section head ─────────────────────────────────────────────────────────
   A section's own title with its action beside it — « Diagnostic » and
   « Enregistrer », « Profil du site » and « Modifier le profil ».

   ⚠️ NOT `.card-head`, which is a different thing that looks superficially
   similar: that one is an uppercased 11px bar with a bottom border, and a
   heading put inside it is SHOUTED (§9 trap 20 — `text-transform` inherits).

   Moved out of `oncologie.css` in build step 13, when §8 became its second
   consumer — §10 point 4's rule, the eighth instance. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap);
  margin-bottom: var(--gap-s);
}

@media (max-width: 900px) {
  /* The action reads under its own title rather than beside it once the
     column is too narrow to hold both. */
  .section-head {
    flex-direction: column;
    align-items: stretch;
  }
}


/* ── Module tab strip ─────────────────────────────────────────────────────
   A row of tabs naming the sections of one module — §5's five Chimiothérapie
   tabs and §8's four Administration tabs.

   Moved out of `chimiotherapie.css` in build step 13, when the site
   administration became its second consumer. §10 point 4's own rule, and the
   seventh instance after `.vn-*`, `.facts` and `.start-visit`: a primitive
   with two consumers belongs in the components layer, or the second module's
   stylesheet comes to depend on the first's.

   What did NOT move is the treatment of a tab that is not built yet — the
   dashed border and the reason line. That is genuinely chemotherapy's, because
   §8 has no unbuilt tab, and it stays in `chimiotherapie.css` layered on these
   two classes. */
/* ── A module strip's SECOND level ───────────────────────────────────────────
   Pills under `.module-tabs`, for a tab that gathers several panels. §5.7's
   libraries drew exactly this from build step 8 to session 50 as
   `.ct-library-tab`, until session 51 made them a module and the pills became
   the strip itself; session 60 needed the shape again for §8's « Le site »,
   so it lands here rather than being re-invented per module (§12's rule for a
   helper with a second consumer).

   ⚠️ Deliberately NOT `.module-tab`'s `flex: 1 1 0`. A module strip divides
   the full width between its tabs; a sub-strip sits under one of them and
   must read as subordinate, so each pill is its own width and the row is left
   aligned. */

.sub-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-s);
  margin: calc(var(--gap) * -0.5) 0 var(--gap);
}

.sub-tab {
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--card);
  color: var(--ink-2);
  font-size: var(--fs-s);
  text-decoration: none;
}

.sub-tab:hover { border-color: var(--brand-lt); color: var(--brand); }

.sub-tab.is-active {
  background: var(--brand-lt);
  border-color: var(--brand-lt);
  color: #fff;
  font-weight: 600;
}

.module-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-s);
  margin-bottom: var(--gap);
}

.module-tab {
  display: flex;
  flex: 1 1 0;
  /* Lets a flex item shrink below its content (§9 trap 26). Without it a long
     label makes the strip overflow rather than wrap. */
  min-width: 0;
  flex-direction: column;
  gap: 2px;
  align-items: center;
  padding: 7px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  background: var(--card);
  color: var(--ink-2);
  font-size: var(--fs-s);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

.module-tab:hover { border-color: var(--brand-lt); color: var(--brand); }

.module-tab.is-active {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

/* A modifier for a `.module-tabs` strip that sits above a distinct region
   rather than beside a heading — D539's own two-regions reasoning
   (`--gap-lg`, not `--gap`), applied to a tab strip rather than a filter
   card. Its own class rather than changing `.module-tabs` itself: the
   Chimiothérapie and Bibliothèques strips this component already served
   sit right beside a heading, where the larger gap would be wrong. */
.view-toggle { margin-bottom: var(--gap-lg); }


/* ── Choice group ─────────────────────────────────────────────────────────
   A `CheckboxSelectMultiple` or `RadioSelect` — §4.5's « Arguments » is nine
   checkboxes. Django renders it as a wrapper holding one `<div><label><input>
   text</label></div>` per option, which is neither a control nor a list, and
   which `AliceFormMixin` used to hand `.alice-input`: the group then looked
   like a text box with nine full-width rows inside it, each checkbox floated
   away from the word it belonged to. Found in a browser.

   The pair is the target. A bordered pill per option means the box and its word
   cannot be read as belonging to different rows, and the whole pill is
   clickable rather than just the 13px square.
   ────────────────────────────────────────────────────────────────────────── */

/* **Every selector below is element-qualified**, and that is not style. Django
   applies a widget's attrs to the group's container `<div>` *and* to each
   `<input>` inside it, so both carry this class. Left unqualified,
   `display: grid` landed on each checkbox and made every one of them a
   200px-wide grid of its own. Found in a browser, after the un-qualified
   version looked right in the stylesheet. */
div.alice-choice-group {
  display: grid;
  /* 200px fits the longest option in one line — « Anatomopathologie »,
     « Examen clinique » — which is the point: a label that wraps under its own
     checkbox is the arrangement this component exists to replace. */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--gap-s);
}

div.alice-choice-group > div { min-width: 0; }

div.alice-choice-group label {
  display: flex;
  align-items: center;
  gap: var(--gap-s);
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  background: var(--surf);
  font-size: var(--fs-s);
  color: var(--ink);
  cursor: pointer;
  /* `.field label` is styled as a field *caption* — small caps, letter-spaced.
     An option's own text is prose and must not inherit that. */
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}

div.alice-choice-group label:hover { border-color: var(--brand-lt); }

/* **Every text-input property is reset here, not just `width`.** Django applies
   the widget's attrs to each `<input>` inside the group, so each checkbox also
   carries `.alice-input` — 44px of padding, a border, a full-width box. Left
   alone the box swelled and pushed its own label onto a second line, which is
   the same shape of conflict `.visually-hidden` versus `.alice-select` had in
   build step 3: a shared class landing on a control it was never written for.
   Found in a browser, twice. */
input.alice-choice-group {
  flex: none;
  width: auto;
  min-width: 0;
  height: auto;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  accent-color: var(--brand);
}

/* `:has()` keeps the checked pill legible without a class the server has to
   set — and where it is unsupported the control still works, which is the only
   requirement. */
div.alice-choice-group label:has(input:checked) {
  border-color: var(--brand);
  background: var(--card);
  font-weight: 600;
}


/* ══════════════════════════════════════════════════════════════════════════
   §2.4's rendered visit note — date list on the left, compte rendu on the right

   Promoted out of `consultations.css` in the review, when §5's séance became
   its second consumer: §2.4 draws **one** previous/current pattern and every
   visit type mounts it, so this is a primitive shared by several modules and
   CLAUDE.md §10 point 4 puts those here. Leaving it where it was would have
   made `chimiotherapie.css` depend on `consultations.css` — the same reason
   `.facts` moved out of `registre.css` in build step 6a.

   `vn-` for « visit note »: the shape belongs to no one module now.
   ══════════════════════════════════════════════════════════════════════════ */

.vn-previous {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: var(--gutter);
  align-items: start;
}

.vn-datelist { padding: var(--gap-s); }
.vn-datelist .section-label { padding: 4px 6px; }

.vn-dates {
  margin: 0;
  padding: 0;
  list-style: none;
}

.vn-date {
  display: block;
  padding: 7px 8px;
  border-radius: var(--r-s);
  color: var(--ink-2);
  text-decoration: none;
}

.vn-date:hover { background: var(--line-2); }

.vn-date.is-active {
  background: var(--card);
  border: 1px solid var(--brand);
  color: var(--brand-dk);
}

.vn-date-day { display: block; font-size: var(--fs-s); }
.vn-date-kind { display: block; color: var(--ink-3); font-size: var(--fs-xs); }
.vn-date.is-active .vn-date-kind { color: var(--brand-lt); }

.vn-note { min-width: 0; }

.vn-note-meta {
  margin: 0 0 var(--gutter);
  color: var(--ink-3);
  font-size: var(--fs-s);
}

.vn-block { margin-bottom: var(--gutter); }

/* §6.4: "step titles clearly visible". A note whose sections run together is
   the thing this rule exists to prevent. */
.vn-block-title {
  margin: 0 0 4px;
  color: var(--ink);
  font-size: var(--fs-s);
  font-weight: 700;
}

.vn-block-text {
  margin: 0;
  color: var(--ink-2);
  font-size: var(--fs);
  line-height: 1.65;
}

/* A block rendered although it holds nothing (D188) — today only the
   decisions. Italic and lighter than the prose around it, because it is the
   note saying something *about* itself rather than reporting a clinical fact;
   `--ink-2` all the same, since D161 is that both halves of a statement have to
   be readable. */
.vn-block-empty {
  margin: 0;
  color: var(--ink-2);
  font-size: var(--fs-s);
  font-style: italic;
}

.vn-block-lines {
  margin: 0 0 var(--gap-s);
  padding-left: 18px;
  color: var(--ink-2);
  font-size: var(--fs);
  line-height: 1.65;
}

/* A complement is visibly an addition to the block above it rather than part
   of it: a validated note is frozen, and prose that read as if it had always
   been there would hide that (D105). */
.vn-complement {
  margin-top: var(--gap-s);
  padding: 8px 10px;
  border-left: 3px solid var(--accent);
  background: var(--surf);
  border-radius: 0 var(--r-s) var(--r-s) 0;
  color: var(--ink-2);
  font-size: var(--fs-s);
  line-height: 1.6;
}

.vn-complement-meta {
  margin: 0 0 3px;
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

.vn-complement p:last-child { margin: 0; }

/* Tablet (§0.9): the date list becomes a scrolling band above the note. It
   owns its own `overflow-x` — the page must never scroll sideways, which is
   the mistake §2.2's tab strip made and CLAUDE.md §12 records. At the foot of
   the file because a media query adds no specificity. */
@media (max-width: 900px) {
  .vn-previous { grid-template-columns: minmax(0, 1fr); }

  .vn-dates {
    display: flex;
    gap: var(--gap-s);
    overflow-x: auto;
  }

  .vn-date { white-space: nowrap; }
}

/* ── « Démarrer une visite » ───────────────────────────────────────────────
   The only control that creates a visit, now that §2.1's card opens the
   workspace rather than silently creating one (D187). It sits under whichever
   §2.4 panel is showing, so it is separated by a rule rather than wrapped in a
   card: it is an action about the patient's visits, not a section of either
   panel.

   **In the components layer because two modules mount it** — §6's consultation
   and §5's séance — which is §10 point 4's rule, the same one that moved
   `.vn-*` out of `consultations.css` and `.facts` out of `registre.css`.
   Leaving it in `consultations.css` would have made `chimiotherapie.css`
   depend on it.
   ────────────────────────────────────────────────────────────────────────── */
.start-visit {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--gap-s);
  margin-top: var(--gutter);
  padding-top: var(--gutter);
  border-top: 1px solid var(--line);
}

.start-visit-note {
  margin: 0;
  font-size: var(--fs-s);
  color: var(--ink-2);
}

/* ── The visit's team (D190) ───────────────────────────────────────────────
   Who conducted this visit, on the frame step of either visit type. In the
   components layer because both modules mount the same partial — §10 point 4's
   rule, the sixth instance after `.vn-*`, `.facts` and `.start-visit`.

   Separated from the step's form by a rule rather than wrapped in a card: it is
   a fact about the visit rather than a section of the step, and it sits after
   the form because HTML forbids a nested `<form>`.
   ────────────────────────────────────────────────────────────────────────── */
.vt-team {
  margin-top: var(--gutter);
  padding-top: var(--gutter);
  border-top: 1px solid var(--line);
}

.vt-team-title {
  margin: 0 0 var(--gap-s);
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--ink);
}

.vt-team-list {
  margin: 0 0 var(--gap);
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-s);
}

.vt-team-member {
  display: flex;
  align-items: center;
  gap: var(--gap-s);
  padding: 5px 10px;
  background: var(--surf);
  border: 1px solid var(--line);
  border-radius: var(--r-s);
}

.vt-team-name { font-size: var(--fs-s); color: var(--ink); }

/* The role is the qualifier and the name is the fact — both readable, which is
   D161's rule: emphasis is built with weight, never by fading the other half. */
.vt-team-role { font-size: var(--fs-xs); color: var(--ink-2); }

.vt-team-empty {
  margin: 0 0 var(--gap);
  font-size: var(--fs-s);
  font-style: italic;
  color: var(--ink-2);
}

.vt-team-add {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--gap-s);
}

.vt-team-add .form-grid {
  flex: 1 1 320px;
  min-width: 0;
}

/* ── Where this server is on the Wi-Fi (session 51) ─────────────────────────
   One card on the login screen and on Accueil: the link a person reads out
   to the next desk. The first address is the one meant to be typed, so it is
   the big one; the others are fallbacks and read as such. */

/* ⚠️ `.network-card` was removed with D686 and NOT renamed: the fragment is
   content only now, and its one consumer is §0.3's state popup, where a card
   inside `.modal-body` would be a panel inside a panel (§9 trap 23). Nothing
   in the project carries the class, so a rule for it would be dead CSS —
   which is the standing debt §9 trap 47 is about, read from the other end. */

.network-links {
  list-style: none;
  margin: 8px 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.network-links li { font-size: var(--fs-s); color: var(--ink-2); word-break: break-all; }

.network-link-primary {
  font-size: 17px;
  font-weight: 600;
  color: var(--brand);
}

.network-qr { margin-top: 10px; width: 132px; }
.network-qr svg { display: block; width: 132px; height: 132px; }


/* A second form's heading inside one popup — the grade after the histology
   in the anatomopathology popup (session 51). It sits INSIDE the form grid
   now, so it takes the whole row. */
.modal-subtitle { margin: 18px 0 8px; }

/* ── Accueil fits itself, and scrolls inside its cards (D695) ────────────────

   The page is a column that fills the workspace exactly: head and tiles take
   their own height, the two card rows share what is left, and « Accès rapides »
   sits at the bottom where it can always be seen. Nothing here scrolls; each
   CARD's own list does.

   ⚠️ `min-height: 0` appears on every level of this and is not decoration. A
   flex item's default minimum is its content, so without it a long list pushes
   the column taller than the workspace instead of scrolling inside its card —
   and the page scrolls after all, which is the whole thing being fixed. */

.home {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.home > .page-head,
.home > .stats,
.home > .quick-title,
.home > .qgrid { flex: 0 0 auto; }

/* The two card rows share the remaining height. `1 1 0` rather than `1 1 auto`
   so they divide the space by the ratio below and NOT by how much content each
   happens to hold — a 74-row decision list would otherwise take the lot.

   ⚠️ **`grid-auto-rows: minmax(0, 1fr)` is the half that actually makes this
   work, and leaving it out looked like it worked.** Both rows are GRIDS, and a
   grid's implicit row is `auto` — sized to its tallest item's CONTENT. So
   capping the flex height alone left the row itself 300px tall inside a 185px
   box: the cards overflowed, the inner scrollers never engaged, and the page
   went on scrolling. Measured, not guessed — `.dgrid` appeared to work only
   because its two cards happened to be short enough that day. The `0` minimum
   is the same `minmax(0, 1fr)` the tiles need, for the same reason: `1fr`'s own
   minimum is `auto`. */
.home > .dgrid {
  /* ⚠️ **THE ONLY ZONE NOW**, since session 84 moved `.worklists` inside it
     (D793). What used to be `flex: 1.4` against `.worklists`' `1` is simply
     the whole of the page's spare height: the donut and the ward list stack
     in column 1 and the day spans both, so the ratio that D792 reversed has
     nothing left to divide. **The shape the user drew made the number
     unnecessary rather than wrong.**

     ⚠️ **`grid-auto-rows` carries the FLOOR now, and that is D749 arriving
     here.** It was `minmax(0, 1fr)` on the argument that « its own comment
     solved this by making the wrap impossible — two cards at a 320px minimum
     — and a wrap that cannot happen needs no floor under it ». That was true
     while `.dgrid` was one row. It now has a row per left-column card, and a
     zone floor divided among them is exactly what D749 found: 150px becoming
     70px becoming a scroller 0px tall.

     ⚠️ And `min-height: auto`, not `0`, for `.worklists`' own recorded
     reason: `auto` IS the floor transitively — a grid's automatic minimum is
     the sum of its rows' minimums — so the zone asks for « enough for the
     cards I actually have », which no single number on the zone can say. */
  flex: 1 1 0;
  min-height: auto;
  grid-auto-rows: minmax(var(--home-card-floor), 1fr);
}

/* ⚠️ `stretch` here REVERSES `.worklists`' own `align-items: start`, and both
   are right in their own case. D689 chose `start` because a card stretched to
   its tallest neighbour fills with empty chrome — true on a page that scrolls,
   where a short card has nothing to do with the extra height. On a FIXED page
   it is the opposite: the row is capped, the card must fill it, and the height
   goes into the card's own scroller rather than into blank space. Without this
   the cards simply ignored the capped row — measured at 398px inside a 185px
   box, overflowing the workspace, which is how the page went on scrolling after
   everything else had been fixed. */
/* ⚠️ **`minmax(var(--home-card-floor), 1fr)` and NOT `minmax(0, 1fr)`, and
   this is the half the floor was missing** (D749, corrected in the browser
   that found it). `--home-card-floor` is declared on the flex ROW
   (`layout.css`), which is right and is D743's own finding — but a flex row is
   not a grid row. `.worklists` holds **three** cards in an `auto-fit` grid, so
   at any workspace between 640px and 959px wide it is TWO columns and
   therefore **two grid rows**, and `minmax(0, 1fr)` divided the row's 150px
   floor between them: 70px per card, which leaves `.worklist-rows`
   **ZERO pixels** after the heading, the footer link and the padding.

   ⚠️ **That is what the user actually met.** « these 3 tabs from the 'Accueil'
   are not scrollable » — the scroller was there, `overflow-y: auto` and all,
   and it was 0px tall. Raising the token from 120px to 150px moved it from
   19px to 0px, because the card count had crossed from one grid row to two.
   **A floor divided among an unknown number of rows is not a floor.**

   ⚠️ **`.dgrid` keeps `minmax(0, 1fr)` and that is deliberate.** Its own
   comment solved this by making the wrap impossible — two cards at a 320px
   minimum — and a wrap that cannot happen needs no floor under it. Three cards
   cannot be kept on one row by any minimum: at two columns a third card wraps
   whatever the width. **A wrap that cannot be prevented has to be survived**,
   and surviving it means the workspace scrolls, which is exactly the escape
   `.workspace.is-fixed` keeps open.

   Measured on the running tier-2 server at 1021×738: before, card 70px and
   rows 0px; after, every card 150px and rows 49–78px with the first row
   whole. */
/* ⚠️ **`.home > .worklists` IS GONE** (session 84, D793). The zone is no
   longer a child of `.home`: the user's drawing put the ward list in the LEFT
   COLUMN under the donut, so it is a grid item inside `.dgrid` now and the
   flex share, the `min-height` and the floor all moved up to the zone that
   actually holds the rows — see `.home > .dgrid` above, which inherited every
   one of D749's arguments along with them.

   What stays on `.worklists` itself is its OWN grid: it still lays its cards
   out, still floors its own rows, and still uses `align-items: start` for
   D689's reason. Those are below, where they always were. */

/* A card in a fixed page is itself a column: its heading and its footer link
   stay put and the list between them is what moves. */
.home .card {
  display: flex;
  flex-direction: column;
  /* `min-height: 0` as a FLEX item, `height: 100%` as a GRID one — the cards
     are grid items here and a grid item does not stretch to a row shorter than
     its content without being told its height. */
  min-height: 0;
  height: 100%;
  overflow: hidden;
}

.home .card > .card-title,
.home .card > .worklist-head,
.home .card > .worklist-link,
.home .card > .worklist-more { flex: 0 0 auto; }

/* ⚠️ THE one scroller — « we scroll though the open decisions without moving
   the whole workspace ». `overscroll-behavior: contain` stops a flick that
   reaches the end of this list from scrolling whatever is behind it, which on a
   tablet is exactly how a fixed page starts moving again. */
.home .worklist-rows,
.home .agenda,
.home .donutwrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ⚠️ NO `align-content: center` here, and it was tried.

   Centring a flex container that OVERFLOWS pushes its first line above the
   container's own top edge, where scrolling cannot reach it — the scrollbar
   starts at 0 and the content begins at −20px. Measured: the donut's `<svg>`
   began 20px above its wrapper, and « Foie », the largest slice at 26 %, was
   simply unreachable while sitting perfectly well in the DOM. The legend's six
   rows were all there and the reader could see four.

   The same trap as `safe center`'s whole reason for existing. Left at the
   default `flex-start`, so the first line is always reachable. */
.home .donutwrap { align-content: flex-start; }

.home .worklist-rows::-webkit-scrollbar,
.home .agenda::-webkit-scrollbar { width: 7px; }
.home .worklist-rows::-webkit-scrollbar-thumb,
.home .agenda::-webkit-scrollbar-thumb { background: #CFD8D4; border-radius: 7px; }
.home .worklist-rows::-webkit-scrollbar-track,
.home .agenda::-webkit-scrollbar-track { background: transparent; }

/* ⚠️ **The breakpoint that used to be here is gone (D743).** It relaxed the
   fixed layout below 760px of viewport, and `layout.css` carries the floor now
   — as a minimum CARD height rather than a viewport number, because Accueil's
   own height is set by how many zones `ALICE_TIER` installed and a breakpoint
   calibrated against one page cannot know that. The rules below are what the
   relaxation did; they are not needed, because a card that has hit its floor
   makes the workspace scroll on its own.

   Kept as a comment rather than deleted, because the SHAPE of the relaxation
   is the thing a future session would otherwise re-derive:

       .home { height: auto; }
       .home > .dgrid, .home > .worklists { flex: 0 0 auto; }
       .home > .worklists { align-items: start; }
       .home .card { overflow: visible; height: auto; }
       .home .worklist-rows, .home .agenda, .home .donutwrap
                              { overflow-y: visible; }
*/


/* ── Accueil's four zones (D689) ─────────────────────────────────────────────

   The mockup's own disposition and, where it states one, its own value —
   `alice_ois_mockup_v4.html`'s DASHBOARD block. CLAUDE.md names that file as
   carrying the visual language, so a number here that differs from it should
   differ on purpose.

   ⚠️ `.stats` NEVER WRAPS, whatever the width — the user: « THIS ONE LINE
   SHOULD STAY ONE LINE EVEN IN TABLET MODE. SHRINK THE TABS TO FIT ». An
   earlier version used `auto-fit` with a 170px floor, which is the usual
   defence against §9 trap 26 and is wrong here: a floor is exactly what makes a
   row wrap once it cannot be met. `grid-auto-flow: column` with
   `minmax(0, 1fr)` columns puts however many tiles this reader has on ONE line
   and lets each shrink — the `0` is load-bearing, since `1fr`'s own default
   minimum is `auto` and a grid item will not shrink below its content without
   it. The mockup fixes `repeat(5, …)`; the count varies with permissions here,
   which is why this is `auto-flow` rather than a literal five. */

.stats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 10px;
}

.stat {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 11px 12px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--sh);
}


/* A tile is an `<a>` where it has somewhere to go and a `<div>` otherwise, so
   it must not take the underline the shell gives every other anchor. */
a.stat:hover { text-decoration: none; border-color: var(--brand-lt); }
a.stat:hover .stat-value { color: var(--brand); }

.stat-icon {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--line-2);
  color: var(--ink-2);
}

/* One tint per tile, the mockup's own pairs. */
.stat-icon-patients        { background: #E7F0EF; color: var(--brand); }
.stat-icon-consultations   { background: var(--info-bg); color: var(--info); }
.stat-icon-seances         { background: #F5E5D2; color: var(--accent-dk); }
.stat-icon-hospitalisations{ background: #EFEBF5; color: #6A5A8C; }
.stat-icon-decisions       { background: var(--warn-bg); color: var(--warn); }

.stat-text { min-width: 0; }

.stat-label {
  display: block;
  font-size: 10.6px;
  color: var(--ink-3);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The figure, in the display face — the mockup's « 1 248 ». */
.stat-value {
  display: block;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.15;
}

.stat-move {
  display: block;
  margin-top: 1px;
  font-size: 9.9px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stat-move-ok    { color: var(--ok); }
.stat-move-warn  { color: var(--warn); }
.stat-move-quiet { color: var(--ink-3); }

/* ⚠️ Narrow: the icon is the first thing to go, and this block MUST come after
   every `.stat-*` rule it has to beat.

   A media query adds NO specificity, so `.stat-icon { display: none }` written
   above `.stat-icon { display: flex }` simply loses on source order — it was,
   and the icons stayed put at 768px with every one of the five labels clipped
   while the rule sat correctly in the served file. Measured, not read.

   Five tiles on a 768px tablet are 125px each; a 32px icon plus its gaps takes
   40 of them, and a label truncated to « Séances de ch… » is worse than no
   glyph. The figure and the label are the tile. */
@media (max-width: 1100px) {
  .stats { gap: 7px; }
  .stat { padding: 9px 10px; gap: 7px; }
  .stat-icon { display: none; }
  .stat-label { font-size: 10px; }
  .stat-value { font-size: 18px; }
}

/* ⚠️ The mockup's `1.15fr 1fr`, but collapsing to one column rather than
   holding two: §0.9 forbids the workspace scrolling sideways, and a donut plus
   a legend plus a name plus a badge does not fit in half of 813px. */
/* ⚠️ `stretch`, not `start` — the user drew two arrows down the empty space
   under « Rendez-vous du jour »: « SPAN THE TAB TO OCCUPY THE SPACE ». The two
   cards are deliberately different lengths (a donut beside a day that may be
   empty) and the short one should take the row's height rather than leave a
   band of page under it. ⚠️ This is the OPPOSITE of what `.worklists` wants and
   of what `.topstrip` wanted, and the difference is real: there, a stretched
   box is empty chrome; here, the box has somewhere to put the height. */
/* ⚠️ **TWO NAMED COLUMNS since session 84, where it was `auto-fit`**, and the
   user's drawing is why: the donut and the ward list stacked on the LEFT, and
   « Rendez-vous du jour » beside them spanning both rows.

   `auto-fit` cannot express that. It decides the column COUNT from the
   available width, so no item can be told which column it is in — and three
   sessions of adjusting a flex ratio between `.dgrid` and `.worklists` could
   not produce the shape either, because the request was never about
   proportions. It was about which box sits where. ⚠️ **A ratio cannot say
   that, and nor can a test**; it took the user drawing it.

   `1.15fr 1fr` IS the mockup's own ratio for this grid — and it is a COLUMN
   ratio, which is what it was always for (D792 found it copied onto a
   VERTICAL share, where it made the ward list taller than the day).

   The 320px minimum it replaces is kept as the collapse point below, for its
   own recorded reason. */
.dgrid {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: var(--gap);
  align-items: stretch;
}

/* The left column: the donut, then whatever worklists are deployed.
   ⚠️ **A WRAPPER rather than a row span**, and the first attempt is why. It
   placed all three as grid items and gave the day `grid-row: 1 / -1` — which
   spanned ONE row, because `-1` counts the lines of the EXPLICIT grid and
   this one declares `grid-template-columns` only. Every row here is implicit,
   so `-1` resolved to line 1, the span collapsed **in silence**, and the day
   card came out exactly as tall as the donut — the shape the user had just
   asked to change. Measured, not noticed: 228px against a left column of 468.

   `span 2` would have worked and would have been a COUNT, which is a
   deployment fact: a general service has no ward list and a later tier may
   contribute a second. Two items in one row, the left one a column of its
   own, needs no span at all. */
.dgrid-stack {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
  min-height: 0;
}

/* ⚠️ The floor lives here now — one per CARD, which is what D749 is about.
   `flex: 1 1 0` divides the column evenly; `min-height` stops the division at
   the point where a card is a heading and one row.

   ⚠️ **`.home` on the front, and that is not tidiness.** `.home .card` below
   sets `min-height: 0` — correct, and it is what lets a card be capped by its
   row — and it is TWO classes to this rule's one, so it won and the donut
   came out at 70px inside a one-column collapse. Measured at 880×900, where
   the floor was simply not there. Specificity is the whole of the bug and the
   whole of the fix. */
.home .dgrid-stack > * {
  flex: 1 1 0;
  min-height: var(--home-card-floor);
}

/* ⚠️ **The donut's floor is its own**, and the list floor was never about it:
   `--home-card-floor` is a heading, a footer link and rows between them, which
   can shrink to one row and still say something. A donut is a fixed 140px
   graphic plus a legend that wraps — 207px of content, measured — so in a
   150px box the circle is cut in half. Found on a tablet, where the two-column
   layout had just been restored and the donut was the one card still wrong. */
.home .dgrid-stack > .dgrid-donut { min-height: var(--home-donut-floor); }

/* ⚠️ **320px is still the collapse point, now stated as a query.** The base
   rule used to reach it through `minmax(320px, 1fr)`: « at 340 the two cards
   drop to ONE column at a 696px tablet workspace — 340×2+16 = 696 is exactly
   the width, so it loses — and on a FIXED page an extra row is not a longer
   page, it is every card losing a share of a fixed height. Measured on a
   768px tablet, where it squeezed the donut to two legend rows. »

   Two columns need 2×320 + the gap, so 656px of WORKSPACE. The query is on
   the viewport and the workspace is narrower than it by the sidebar and the
   gutters — the same offset `.obs-actions`' own breakpoint comment works
   through — so 900px of viewport is where it is taken. */
/* ⚠️ **760px, and 900 was WRONG — it collapsed a layout that fitted.**
   The rule this replaced was `repeat(auto-fit, minmax(320px, 1fr))`, whose own
   comment explains the 320: « at 340 the two cards drop to ONE column at a
   696px tablet workspace … Measured on a 768px tablet, where it squeezed the
   donut to two legend rows. » **The minimum was chosen precisely so that two
   columns SURVIVE on a tablet**, and replacing it with a viewport breakpoint
   threw that away — the user: « the disposition is perfect in full view but in
   tablet view it is not. »

   `auto-fit` asks the CONTAINER and a media query asks the VIEWPORT, so the
   number has to be converted. Measured at a 768px tablet: the workspace is
   660px and `.dgrid` 655px, against 2 × 320 + a 12px gap = **652** — it fits,
   by three pixels. The workspace runs about 108px narrower than the viewport
   once the sidebar and the gutters have taken their share, so 652 + 108 = 760.

   ⚠️ **A breakpoint converted from a container measurement is only as good as
   that offset**, which is why the arithmetic is written out rather than the
   answer: a sidebar that changes width moves this number. */
@media (max-width: 760px) {
  .dgrid { grid-template-columns: 1fr; }

  /* ⚠️ **The rows must size to their CONTENT once the columns collapse**, and
     this is the half the collapse was missing. In two columns `.dgrid` has ONE
     row, so `minmax(floor, 1fr)` on `.home > .dgrid` is inert. In one column
     it has TWO — the stack, then the day — and `1fr` makes them EQUAL: the day
     card was given 312px for 81px of content, 231px of blank below three
     links, and the page scrolled because of it. Measured at 768×1024.

     `auto` as the maximum lets each row ask for what it holds; the floor stays
     as the minimum, so a card still never shrinks below a heading and a row.
     ⚠️ And `flex: 0 0 auto` on the zone, because a zone whose rows have
     stopped stretching must stop stretching too — otherwise the flex share
     hands back the height the rows just gave up. */
  .home > .dgrid {
    flex: 0 0 auto;
    grid-auto-rows: minmax(var(--home-card-floor), auto);
  }
}

.donutwrap {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}

.donut { flex: 0 0 auto; }

.donut-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 160px;
  min-width: 0;
}

.legrow {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 0;
  font-size: var(--fs-s);
}

.legdot { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 auto; }
.legrow .lgn { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legrow .lgv { font-family: var(--font-mono); font-size: 12px; font-weight: 700; }

/* « Rendez-vous du jour » — the mockup's `agrow`. */
.agenda { list-style: none; margin: 0; padding: 0; }

.agrow {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 4px;
  border-bottom: 1px solid var(--line-2);
}

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

.agt {
  flex: 0 0 46px;
  font-family: var(--font-mono);
  font-size: 11.4px;
  font-weight: 600;
  color: var(--brand);
}

.agwho { flex: 1 1 auto; min-width: 0; }
.agwho:hover { text-decoration: none; }
.agwho:hover .agn { color: var(--brand); }

.agn { display: block; font-size: 12.4px; font-weight: 600; color: var(--ink); }
.agi { display: block; font-size: 10.8px; color: var(--ink-3); }

/* « Accès rapides » — ⚠️ the mockup fixes `repeat(6, …)` and this page rendered
   at most TWO tiles when D689 wrote this, so a fixed count would stretch each
   to half the pane. The cap is what keeps them tile-sized however many there
   are, and it still has to.

   ⚠️ **BUT `repeat(auto-fit, minmax(170px, 220px))` COUNTED ITS TRACKS WITH
   THE CAP**, which is the defect the user found in tablet view: when a track's
   MAX is definite, the repeat count is computed from the max, not the
   minimum — so at 768px the 655px row fitted `220px 220px`, the third tile
   wrapped to a second line, and **215px sat empty beside the first two**. The
   minimum the tiles were allowed to shrink to was never consulted, so the one
   number written to make them fit did nothing.

   Flex wrap asks the question the other way round: how many fit at their
   BASIS, then grow them up to the cap. At 768px that is 3 × 170 + 18 = 528 in
   655, so all three sit on one line at 212px each; on a wide pane they grow to
   220 and stop, packed left, which is the tile look the cap exists for.
   ⚠️ **A third tile is not hypothetical** — §0.4's row is a registry, and
   `extensions.chemotherapy` contributed « Plan de traitement » as the second
   while « Adresse réseau » is the foundation's own. */
.quick-title { margin: 0 0 9px; }

.qgrid {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
}

.qlink {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  /* The pair the grid used to state as `minmax(170px, 220px)`: grow from the
     basis, never past the cap. `min-width: 0` above is what lets a long label
     ellipsis rather than push the basis out. */
  flex: 1 1 170px;
  max-width: 220px;
  padding: 11px 10px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--sh);
  font-size: 11.2px;
  font-weight: 600;
  color: var(--ink-2);
}

.qlink:hover { border-color: var(--brand-lt); color: var(--brand-dk); text-decoration: none; }
.qlink .qi { flex: 0 0 auto; color: var(--brand); display: flex; }
.qlink span:last-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* « Adresse réseau » (D702, session 68) is a `<button>`, not an `<a>` — it
   opens a dialog rather than navigating. `button, input, select, textarea {
   font: inherit }` in base.css already matches its type; a button's own
   default centred text and default cursor are the only two divergences a
   `<button class="qlink">` needs reset, same family as §9 trap 13's
   element-qualified selectors on a widget that is more than one tag. */
button.qlink { cursor: pointer; text-align: left; }


/* ── Accueil's worklists (D688) ──────────────────────────────────────────────

   Five cards answering « what needs me today? », in the order a day runs. A
   grid rather than a stack, because the sections are short by design
   (`worklist.SECTION_ROW_LIMIT`) and a column of five half-empty cards is the
   launcher §10 asked this page to stop being.

   ⚠️ `minmax(320px, 1fr)` with `auto-fit`: the pane is 813px at the narrowest
   §0.9 allows, so this gives two columns on a desktop and one on a tablet
   without a breakpoint — and §0.9 forbids the workspace scrolling sideways,
   which a fixed column count would cause the moment a name is long. */

.worklists {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--gap);
  /* ⚠️ `start`, not the grid's default `stretch` — the same mistake D686 found
     one layer up in `.topstrip`. The five sections are deliberately different
     lengths (a ward with one patient beside a queue of 74 decisions), and
     stretching each card to its row's tallest neighbour fills it with empty
     chrome rather than with information. Seen on real rows, not predicted. */
  align-items: start;
}

/* ⚠️ `min-width: 0` on the card, not only on its rows: a grid item refuses to
   shrink below its content without it, which is §9 trap 26 one container up —
   there, `flex: 0 0 auto` meant a box could not be narrowed and its own
   `flex-wrap` never engaged. */
.worklist { min-width: 0; }

.worklist-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}

/* The section's answer when it has nothing — words, never a blank box. §10
   keeps « status codes over blank fields », and an empty card is a question
   about whether the page worked. */
.worklist-empty { margin: 0; font-size: var(--fs-s); color: var(--ink-3); }

.worklist-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.worklist-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 0;
  border-top: 1px solid var(--line-2);
}

.worklist-row:first-child { border-top: 0; }

/* The patient takes the room and everything after it keeps its own width. */
/* ⚠️ The whole row's patient is one link, so it must not take the underline
   the shell gives every other anchor — a list of six underlined names reads as
   a paragraph of links rather than as a table. The colour change on hover is
   the affordance instead, which is `a.sync-pill`'s own treatment. */
.worklist-who { flex: 1 1 auto; min-width: 0; }
.worklist-who:hover { text-decoration: none; }

.worklist-name {
  display: block;
  font-size: var(--fs-s);
  font-weight: 600;
  color: var(--ink);
}

.worklist-who:hover .worklist-name { color: var(--brand); }

.worklist-id { display: block; font-size: 10.6px; color: var(--ink-3); }

/* ⚠️ Truncated to one line, with the whole text on the row's `title`.

   Four of the five sections put something short here — an hour, a bed — and
   the fifth puts a clinician's free text: « Poursuivre le protocole au cycle
   suivant. » Found in a browser on real rows, where it squeezed the patient's
   name into a two-line wrap and the card stopped being scannable, which is the
   one thing a worklist has to be. §12's « Truncation » convention (build step
   6b) read once more; the whole decision is on the patient's own screen, which
   the name beside it links to. */
.worklist-detail {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 55%;
  font-size: var(--fs-s);
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: right;
}

.worklist-more {
  margin: 8px 0 0;
  font-size: var(--fs-s);
  color: var(--ink-3);
}

.worklist-link { margin: 12px 0 0; font-size: var(--fs-s); }


/* ── §0.3's state popup (D686) ───────────────────────────────────────────────

   The two halves the welcome bar's one control collapses: each its own label
   and its own explanatory sentence, with the console link beside it for a
   reader who may open it. A row, not a card — it already sits inside a
   dialog's `.modal-body`.

   ⚠️ Every colour here is a token defined in `base.css`'s `:root` and nowhere
   else (§9 trap 49: an unknown custom property with no fallback kills the
   whole declaration in silence, which is how `.bk-muted` carried no colour at
   all for four sessions). */

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

.state-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 11px 12px;
  border: 1px solid var(--ok-line);
  border-radius: var(--r);
  background: var(--ok-bg);
  color: var(--ok);
}

/* The dot is `currentColor`, so the row's tone carries it and the two can
   never disagree about whether something is wrong. */
.state-dot {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin-top: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* ⚠️ `min-width: 0` is not decoration: without it a flex item refuses to
   shrink below its content, and the sentences here are long enough to push the
   console button out of the dialog — §9 trap 26, which put 824px of controls
   in an 813px pane. */
.state-text { flex: 1 1 auto; min-width: 0; }

.state-label {
  display: block;
  font-size: var(--fs-s);
  font-weight: 600;
}

/* The sentence the collapsed button can only carry as a tooltip. Deliberately
   in the body ink rather than in the row's tone: the tone is the verdict, and
   a whole paragraph in the warning colour reads as shouting. */
.state-note {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-s);
  color: var(--ink-2);
}

.state-row-warn {
  border-color: var(--warn-line);
  background: var(--warn-bg);
  color: var(--warn);
}

.form-grid > .form-grid-span { grid-column: 1 / -1; }
