/* ateliers — mobile-first, rounded UI. No external assets (CSP:
   default-src 'self'). Light & dark themes.

   The design language is the one of the sibling apps (todo, booking): the same
   neutral scale, the same violet accent, the same radius/shadow steps, the same
   generous tap targets. What is specific here is what carries meaning: the
   colour a thématique or a discipline is given (a fixed palette of keys, never
   an inline style — the CSP forbids those), and the duration bar that fills as
   ateliers are added to a séance. */

:root {
  --bg: #eef1f8;
  --bg-elev: #f7f9fd;
  --card: #ffffff;
  --ink: #171d2b;
  --muted: #6a7488;
  --line: #e6eaf3;
  --line-strong: #d6dce9;

  --accent: #6c5ce7;
  --accent-2: #8b7bff;
  --accent-ink: #ffffff;
  --accent-soft: #efecfe;
  --accent-chip-ink: #4a3fc0; /* accent text on --accent-soft, AA-contrast */

  --danger: #e5484d;
  --danger-soft: #fdecec;
  --danger-ink: #8f1d1f;
  --success: #16a34a;
  --success-soft: #e7f6ec;
  --success-ink: #10633a;
  --warn: #f5a623;
  --warn-soft: #fdf3e0;
  --warn-ink: #6b4400;

  --radius: 20px;
  --radius-md: 15px;
  --radius-sm: 11px;
  --pill: 999px;

  --shadow-sm: 0 1px 2px rgba(23, 29, 43, 0.06), 0 1px 3px rgba(23, 29, 43, 0.05);
  --shadow-md: 0 6px 20px rgba(23, 29, 43, 0.10);
  --shadow-lg: 0 16px 40px rgba(23, 29, 43, 0.22);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #10131b;
    --bg-elev: #171b25;
    --card: #1b2029;
    --ink: #e9edf6;
    --muted: #98a2b6;
    --line: #29303d;
    --line-strong: #333c4b;

    --accent: #8b7bff;
    --accent-2: #a79bff;
    --accent-ink: #14101f;
    --accent-soft: #241f3c;
    --accent-chip-ink: #b7abff;

    --danger: #ff6b6f;
    --danger-soft: #3a1f22;
    --danger-ink: #ffc9cb;
    --success: #22c55e;
    --success-soft: #12301f;
    --success-ink: #9ff0bd;
    --warn: #f7b955;
    --warn-soft: #3a2c12;
    --warn-ink: #ffdca8;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
    --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);
  }
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-tap-highlight-color: transparent;
  padding-bottom: env(safe-area-inset-bottom);
}

/* --- Entity palette ---------------------------------------------------
   A thématique and a discipline each carry a colour chosen from a fixed set.
   The strict CSP rules out per-row inline colours, so the entity stores a
   palette *key* which maps to one of these classes (see ateliers.ColorKeys).
   Every class sets --ecolor (the strong ink/fill) and --ecolor-soft (the tint a
   chip sits on), so one rule paints chips, dots and left borders alike. */
.ec-violet { --ecolor: #7c3aed; --ecolor-soft: #f0e9ff; }
.ec-blue   { --ecolor: #2563eb; --ecolor-soft: #e5eeff; }
.ec-cyan   { --ecolor: #0e7490; --ecolor-soft: #e0f4f8; }
.ec-teal   { --ecolor: #0f766e; --ecolor-soft: #dff4f0; }
.ec-green  { --ecolor: #15803d; --ecolor-soft: #e3f5e8; }
.ec-lime   { --ecolor: #4d7c0f; --ecolor-soft: #eef6dd; }
.ec-amber  { --ecolor: #b45309; --ecolor-soft: #fdf0dc; }
.ec-orange { --ecolor: #ea580c; --ecolor-soft: #ffeade; }
.ec-red    { --ecolor: #dc2626; --ecolor-soft: #fde8e8; }
.ec-pink   { --ecolor: #db2777; --ecolor-soft: #fde7f1; }
.ec-brown  { --ecolor: #78350f; --ecolor-soft: #f5ebe2; }
.ec-slate  { --ecolor: #475569; --ecolor-soft: #eaeef4; }

@media (prefers-color-scheme: dark) {
  /* Dark mode keeps the same hues but lifts them off the dark card and darkens
     the tint, so a chip stays readable without a second palette to maintain. */
  .ec-violet { --ecolor: #a78bfa; --ecolor-soft: #2a2145; }
  .ec-blue   { --ecolor: #7aa5ff; --ecolor-soft: #16243f; }
  .ec-cyan   { --ecolor: #4ec5db; --ecolor-soft: #10303a; }
  .ec-teal   { --ecolor: #4bc3b5; --ecolor-soft: #0f2f2c; }
  .ec-green  { --ecolor: #56cc7c; --ecolor-soft: #12301f; }
  .ec-lime   { --ecolor: #a3c750; --ecolor-soft: #232c11; }
  .ec-amber  { --ecolor: #e6a44e; --ecolor-soft: #362512; }
  .ec-orange { --ecolor: #ff8f52; --ecolor-soft: #3a2113; }
  .ec-red    { --ecolor: #ff7b7b; --ecolor-soft: #3a1c1c; }
  .ec-pink   { --ecolor: #f472b6; --ecolor-soft: #3a1a2a; }
  .ec-brown  { --ecolor: #c39471; --ecolor-soft: #2f2219; }
  .ec-slate  { --ecolor: #9aa7ba; --ecolor-soft: #232a35; }
}

/* --- Prose ------------------------------------------------------------ */
h1 { font-size: 1.45rem; font-weight: 800; letter-spacing: -0.02em; margin: 0 0 0.35rem; }
h2 { font-size: 1.05rem; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 0.7rem; }
p { margin: 0 0 0.7rem; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* --- Top bar ---------------------------------------------------------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  border-bottom: 1px solid var(--line);
  padding-top: env(safe-area-inset-top);
}
.topbar-inner {
  max-width: 760px;
  margin: 0 auto;
  padding: 10px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.app-title {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topbar-badges { display: flex; align-items: center; gap: 6px; flex: none; }

.icon-btn {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--pill);
  background: var(--card);
  color: var(--muted);
  /* Half of these carry a glyph rather than an <svg> (✎, ✕, ‹, ›): without the
     reset a <button> would draw them at the UA's own 13px. */
  font: inherit;
  padding: 0;
  cursor: pointer;
  text-decoration: none;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.icon-btn:hover { color: var(--ink); text-decoration: none; }
.icon-btn:active { transform: scale(0.92); }
.icon-btn svg { width: 20px; height: 20px; display: block; }
.icon-danger:hover { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--line)); }

/* The page. Its bottom padding clears the tab bar AND the FAB sitting above it,
   so the last card of a list is never hidden under the chrome. */
.container {
  max-width: 760px;
  margin: 0 auto;
  padding: 16px 14px calc(120px + env(safe-area-inset-bottom));
}

/* --- Bottom tab bar --------------------------------------------------- */
/* Five destinations, always one thumb away: Aujourd'hui / Séances / Ateliers /
   Modèles / Personnes. The app is used standing up, phone in one hand, while a
   séance runs — which is why navigation lives at the bottom here and not in the
   header as it does in the sibling apps. */
.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.tab {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  position: relative;
  height: 56px;                 /* > 44px: the whole cell is the tap target */
  padding: 4px 2px 6px;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.15s ease;
}
.tab:hover { text-decoration: none; color: var(--ink); }
.tab:active { opacity: 0.6; }
/* The glyph is an inline <svg> sitting straight inside the tab (views.tab puts
   nothing around it), so it is sized here rather than on a wrapper. */
.tab svg { width: 22px; height: 22px; display: block; }
/* The label is what a narrow phone drops first; the glyph and the dot still say
   where you are. */
.tab-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tab.is-active { color: var(--accent); }
/* A dot rather than a filled pill: it marks the current tab without competing
   with the FAB floating just above the bar. */
.tab.is-active::after {
  content: "";
  position: absolute;
  bottom: 3px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}
@media (max-width: 340px) {
  .tab-label { display: none; }
}

/* --- Floating action button ------------------------------------------ */
/* Sits above the tab bar, never on it — 56px of bar + a gap. */
.fab {
  position: fixed;
  right: max(18px, env(safe-area-inset-right));
  bottom: calc(74px + env(safe-area-inset-bottom));
  z-index: 30;
  display: grid;
  place-items: center;
  width: 58px;
  height: 58px;
  border: 0;
  border-radius: var(--pill);
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  box-shadow: var(--shadow-md), 0 8px 20px color-mix(in srgb, var(--accent) 40%, transparent);
  cursor: pointer;
  text-decoration: none;
  transition: transform 0.12s ease, box-shadow 0.15s ease;
}
.fab:hover { transform: translateY(-2px); text-decoration: none; }
.fab:active { transform: scale(0.94); }

/* --- Empty state ------------------------------------------------------ */
.empty { text-align: center; padding: 52px 20px; color: var(--muted); }
.empty-emoji { font-size: 2.8rem; margin-bottom: 8px; }
.empty-title { margin: 0 0 4px; font-size: 1.12rem; font-weight: 700; color: var(--ink); }
.empty-sub { margin: 0; font-size: 0.94rem; }

/* --- Cards & lists ---------------------------------------------------- */
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: 15px 15px;
  margin-bottom: 14px;
}
.card > h2 { margin-top: 0; }
.card-hint { margin: -4px 0 14px; color: var(--muted); font-size: 0.86rem; }

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

/* A row is a card that is a list item: the whole tile is the tap target, so it
   carries no buttons of its own. */
.row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.row:hover { text-decoration: none; border-color: var(--line-strong); box-shadow: var(--shadow-md); }
.row:active { transform: scale(0.995); background: var(--bg-elev); }
/* .row-main is the tappable inside of a row — an <a> on some screens and a
   <button> on others, hence the reset. It lays its children out ACROSS: an
   hour, an avatar, a rank or a thumbnail, and then the column of text, which is
   .row-body. The negative margin pulls it over the row's own padding, so the
   tap target is the full height of the row and not just the words in it. */
.row-main {
  flex: 1 1 auto;
  min-width: 0;
  align-self: stretch;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: -12px 0;
  padding: 12px 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.row-main:hover { text-decoration: none; }
.row-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.row-title { font-weight: 700; line-height: 1.3; overflow-wrap: anywhere; }
/* The second line of a row: an ajustement on an atelier, a note of the day on
   an attendee. */
.row-note { font-size: 0.86rem; color: var(--muted); overflow-wrap: anywhere; }
.row-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* An archived library entry stays readable everywhere it is already used — it
   just stops asking for attention. */
.row.is-archived, .card.is-archived, .tile.is-archived { opacity: 0.6; }

/* --- Chips ------------------------------------------------------------ */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.76rem;
  font-weight: 650;
  line-height: 1;
  padding: 4px 9px;
  border: 1px solid var(--line);
  border-radius: var(--pill);
  background: var(--bg-elev);
  color: var(--muted);
  white-space: nowrap;
}
/* "modifiée" — the séance departed from its modèle; "annulée" — it will not
   happen. Both are facts about the séance rather than warnings, so they ride in
   the same chip row as the discipline and the hour. */
.chip-modified { background: var(--accent-soft); border-color: transparent; color: var(--accent-chip-ink); }
.chip-cancelled { background: var(--danger-soft); border-color: transparent; color: var(--danger-ink); }

/* A chip carrying an entity colour wears its .ec-<key> class next to these, and
   reads the two variables that class sets — one rule, twelve palettes. */
.chip-theme,
.chip-discipline {
  background: var(--ecolor-soft);
  border-color: transparent;
  color: var(--ecolor);
}
/* A discipline is what a séance IS; a thématique is only what it touches. The
   dot (drawn, not marked up) is what tells them apart at a glance. */
.chip-discipline { font-weight: 700; }
.chip-discipline::before {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ecolor);
}
/* Coverage, as Modele.Coverage() tells it: a declared thématique nothing in the
   modèle answers reads hollow ("non couvert"), one the ateliers carry without
   the modèle claiming it reads dashed and muted ("en plus"). Neither is an
   error — a modèle in progress is allowed to be incomplete. */
.chip-missing {
  background: transparent;
  border: 1px dashed var(--ecolor, var(--line-strong));
  color: var(--ecolor, var(--muted));
  opacity: 0.85;
}
.chip-extra {
  background: transparent;
  border: 1px dashed var(--line-strong);
  color: var(--muted);
}

/* A duration in minutes, wherever one is shown: tabular figures so a column of
   them lines up. */
.badge-min {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The palette as a bare mark — a thématique's or a discipline's colour beside a
   name that has no room for a whole chip (les réglages). */
.swatch {
  flex: none;
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--ecolor, var(--muted));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
}

/* --- Duration bar ----------------------------------------------------- */
/* How much of the séance's length the ateliers already fill — on a modèle
   (against its targeted length) and on a séance (against the real slot). */
.durbar {
  position: relative;
  height: 10px;
  border-radius: var(--pill);
  background: var(--line);
  overflow: hidden;
}
.durbar-fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width 0.25s ease, background 0.2s ease;
}
/* The width comes from a CLASS, never from style="width:45%": style-src is
   'self' with no 'unsafe-inline', so a server-rendered inline style would be
   dropped by the browser. Hence this ladder — 5 % steps, which is finer than
   the bar is wide on a phone (a 760px bar moves 38px per step). Go rounds the
   ratio to the nearest step. */
.durbar.fill-0 .durbar-fill { width: 0%; }
.durbar.fill-5 .durbar-fill { width: 5%; }
.durbar.fill-10 .durbar-fill { width: 10%; }
.durbar.fill-15 .durbar-fill { width: 15%; }
.durbar.fill-20 .durbar-fill { width: 20%; }
.durbar.fill-25 .durbar-fill { width: 25%; }
.durbar.fill-30 .durbar-fill { width: 30%; }
.durbar.fill-35 .durbar-fill { width: 35%; }
.durbar.fill-40 .durbar-fill { width: 40%; }
.durbar.fill-45 .durbar-fill { width: 45%; }
.durbar.fill-50 .durbar-fill { width: 50%; }
.durbar.fill-55 .durbar-fill { width: 55%; }
.durbar.fill-60 .durbar-fill { width: 60%; }
.durbar.fill-65 .durbar-fill { width: 65%; }
.durbar.fill-70 .durbar-fill { width: 70%; }
.durbar.fill-75 .durbar-fill { width: 75%; }
.durbar.fill-80 .durbar-fill { width: 80%; }
.durbar.fill-85 .durbar-fill { width: 85%; }
.durbar.fill-90 .durbar-fill { width: 90%; }
.durbar.fill-95 .durbar-fill { width: 95%; }
.durbar.fill-100 .durbar-fill { width: 100%; }
/* Full: not an error, but there is no minute left to place. Keyed off .is-full
   and NOT off fill-100, because both roundings reach the last rung early — 59
   minutes of 60 already land on fill-100 and that is not full. .is-full is set
   only when the two numbers are equal (views.DurbarClass, and setDurbar). */
.durbar.is-full .durbar-fill { background: var(--warn); }
/* Overflow: the bar cannot say "110 %", so it stays full and turns red. The
   width is pinned here too, so an is-over bar reads full whatever ladder class
   came with it. */
.durbar.is-over .durbar-fill { width: 100%; background: var(--danger); }

.durbar-legend {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 6px;
  font-size: 0.82rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* Two spans, never one text node: setDurbar rewrites the count in .durbar-total
   and the verdict in .durbar-rest separately, so writing on the <p> itself
   would delete the very hooks it is aiming at. */
.durbar-total { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.durbar-rest { font-weight: 700; }
/* "10 min de trop" — the one half of the legend that is a verdict. */
.durbar-rest.is-over { color: var(--danger); }

/* --- Sortable lists --------------------------------------------------- */
/* Any reorderable list: <ul class="sortable" data-reorder="…"> with a
   <button class="drag-handle"> in each <li>. app.js POSTs the new id order on
   drop and re-pulls the list if the save fails, so the DOM never keeps an order
   the server refused. */
.sortable { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.sortable > li { position: relative; }

.drag-handle {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 44px;                 /* full-height comfortable grab target */
  margin: 0 -6px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--line-strong);
  cursor: grab;
  touch-action: none;           /* let SortableJS own the gesture */
  border-radius: var(--radius-sm);
}
.drag-handle:active { cursor: grabbing; color: var(--muted); background: var(--bg-elev); }
.drag-handle svg { fill: currentColor; }

/* forceFallback:true means the row under the finger is the fallback clone —
   put the "lift" there; the placeholder left in the list just dims. */
.drag-ghost { opacity: 0.3; }
.drag-chosen { box-shadow: var(--shadow-md); }
.drag-fallback,
.sortable-fallback {
  opacity: 1;
  box-shadow: var(--shadow-lg);
  transform: scale(1.03) rotate(-1.2deg);
}

/* --- Bottom sheet ----------------------------------------------------- */
/* The one add/edit surface of the app: every [data-sheet] click htmx-swaps a
   form into #sheet-body. Mid-séance the atelier picker uses the same sheet, so
   choosing what to do next never leaves the séance. */
#sheet {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(15, 18, 28, 0);
  visibility: hidden;
  pointer-events: none;
  transition: background 0.22s ease, visibility 0.22s;
}
/* The scrim IS #sheet: a click landing on it (rather than on the panel) closes
   the sheet, so no extra overlay node is needed. */
#sheet.open { visibility: visible; pointer-events: auto; background: rgba(15, 18, 28, 0.5); }
#sheet-panel {
  position: relative;
  width: 100%;
  max-width: 620px;
  height: 100dvh;                 /* phone: a full-height form */
  max-height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--card);
  box-shadow: var(--shadow-lg);
  padding: 10px 18px calc(24px + env(safe-area-inset-bottom));
  transform: translateY(100%);
  transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
#sheet.open #sheet-panel { transform: translateY(0); }
@media (min-width: 640px) {
  #sheet { align-items: center; padding: 20px; }
  #sheet-panel { height: auto; max-height: 88dvh; border-radius: var(--radius); }
}
/* htmx swaps the form in here. The min-height keeps the panel from collapsing
   to nothing between the tap and the arrival of the markup, which is exactly
   when #sheet-loading needs a surface to spin on. */
#sheet-body { min-height: 180px; }
/* Lock the page behind the sheet so only the sheet scrolls. */
body.sheet-open { overflow: hidden; }
.sheet-title { margin: 4px 0 16px; font-size: 1.3rem; font-weight: 800; letter-spacing: -0.01em; }

/* htmx hangs .htmx-request on this while a swap is in flight: the panel keeps
   its size and says it is working rather than blanking. */
#sheet-loading {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: color-mix(in srgb, var(--card) 78%, transparent);
  backdrop-filter: blur(2px);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.15s ease;
}
#sheet-loading.htmx-request { opacity: 1; visibility: visible; }
/* htmx only tags its own requests: the same overlay is driven by hand for the
   sheet app.js fetches itself (a [data-sheet] URL, or the no-htmx fallback),
   which sets .loading on #sheet. And [hidden] needs saying out loud here — the
   id rule above is an author rule, so it beats the UA's [hidden]{display:none}
   and the overlay would otherwise stay in the layout for ever. */
#sheet.loading #sheet-loading { opacity: 1; visibility: visible; }
#sheet-loading[hidden] { display: none; }

/* --- Confirm dialog (styled replacement for window.confirm) ----------- */
/* Anything with data-confirm="question" asks here before it acts. app.js builds
   the dialog itself (see confirmViaDialog) as a modal <dialog>: showModal() is
   what provides the backdrop, the focus trap and the Escape key, so there is no
   overlay node to paint and no .open class to track — the UA's own [open] is
   the state, and the stacking is the top layer's, above sheet and cropper. */
.confirm-dialog {
  width: calc(100% - 40px);
  max-width: 360px;
  padding: 22px 20px 18px;
  border: 0;
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}
.confirm-dialog::backdrop { background: rgba(15, 18, 28, 0.5); }
.confirm-msg { margin: 0 0 20px; font-size: 1.05rem; font-weight: 600; line-height: 1.4; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions .btn { flex: 1; }

/* --- Forms ------------------------------------------------------------ */
.field { display: block; margin: 0 0 16px; }
.field-label { display: block; font-size: 0.85rem; font-weight: 600; color: var(--muted); margin-bottom: 7px; }
.field-hint { margin: 6px 0 0; font-size: 0.8rem; color: var(--muted); }
.input,
select.input,
textarea.input {
  width: 100%;
  padding: 12px 14px;
  min-height: 46px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  color: var(--ink);
  font: inherit;
  transition: border-color 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}
.input:focus { outline: none; border-color: var(--accent); background: var(--card); box-shadow: 0 0 0 3px var(--accent-soft); }
/* Les étapes are typed one per line and read one per line (see Steps()), so the
   textarea is given room to breathe: the line split has to be visible while
   typing, since it is what the séance will be read from. */
textarea.input { min-height: 5rem; resize: vertical; line-height: 1.55; }
/* Two fields on one line (le jour et l'heure). Flex rather than two columns, so
   the same row holding a single field — the durée of an atelier — still fills
   the width instead of leaving a hole. */
.field-row { display: flex; gap: 12px; }
.field-row > .field { flex: 1 1 0; min-width: 0; }
@media (max-width: 380px) {
  .field-row { flex-wrap: wrap; gap: 0 12px; }
  .field-row > .field { flex-basis: 100%; }
}

.form-actions { display: flex; gap: 10px; margin-top: 18px; }
.form-actions .btn { flex: 1; }
/* Enregistrer/Annuler always in the thumb zone, whatever the scroll position:
   inside the sheet it is the panel that scrolls, so the actions stick to its
   bottom edge. On a page (views.SimpleMessage) they scroll with the card. */
.sheet-form .form-actions {
  position: sticky;
  bottom: 0;
  z-index: 3;
  padding: 12px 0 2px;
  background: var(--card); /* covers the scrolled content; no band on the sheet */
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 18px;
  min-height: 46px;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  color: var(--ink);
  font: inherit;
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
  text-decoration: none;   /* .btn is also worn by links (e.g. "Annuler") */
  transition: filter 0.12s ease, background 0.12s ease, transform 0.1s ease;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: scale(0.98); }
.btn svg { width: 20px; height: 20px; display: block; }
.btn-block { display: flex; width: 100%; }
.btn-sm { min-height: 38px; padding: 8px 14px; font-size: 0.9rem; }
.btn-primary { background: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { filter: brightness(1.06); }
.btn-ghost { background: var(--bg-elev); color: var(--ink); }
.btn-ghost:hover { background: var(--line); }
.btn-danger { background: var(--danger-soft); color: var(--danger-ink); }

/* Chip picker — the many-of-N choice used for a modèle's thématiques and for an
   atelier's. Each option is a <label class="chip chip-theme chip-check ec-…">
   with the checkbox hidden inside it, so an unchecked option has to be talked
   back down from the filled .chip-theme look it inherits; checked, it lights in
   the entity's own colour, in the accent when it has none. */
.chip-picker { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-check {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  padding: 8px 14px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--pill);
  background: var(--bg-elev);
  color: var(--ink);
  font-size: 0.9rem;
  font-weight: 650;
  cursor: pointer;
  user-select: none;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.chip-check input { position: absolute; opacity: 0; pointer-events: none; }
.chip-check:has(input:checked) {
  background: var(--ecolor-soft, var(--accent-soft));
  border-color: var(--ecolor, var(--accent));
  color: var(--ecolor, var(--accent-chip-ink));
}
.chip-check:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Colour picker — the twelve palette keys, as themselves. Each option is a
   <label> wearing the key's own .ec-<key>: the radio is hidden inside it and the
   dot is the whole of what is seen (the label is read by .sr-only). */
.color-picker { display: flex; flex-wrap: wrap; gap: 12px; }
.color-option { position: relative; display: block; }
.color-option input { position: absolute; opacity: 0; pointer-events: none; }
.color-dot {
  position: relative;
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--ecolor, var(--muted));
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.color-option input:checked + .color-dot { transform: scale(1.12); box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--ink); }
.color-option input:focus-visible + .color-dot { box-shadow: 0 0 0 3px var(--accent-soft); }
/* 44px of tap around a 32px dot, without spacing the row out. */
.color-dot::after { content: ""; position: absolute; inset: -6px; }

/* --- Filter chips ----------------------------------------------------- */
/* The horizontal filter row (a thématique, a discipline). It scrolls inside
   itself rather than widening the page. */
.filters {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  margin: 0 0 14px;
  padding-bottom: 4px;
  scrollbar-width: none;
}
.filters::-webkit-scrollbar { display: none; }
/* A filter is a .chip too, so this has to undo the chip's small type and, for a
   discipline, the filled colour it would otherwise wear before being chosen: an
   off filter must read off. It comes after .chip-theme / .chip-discipline in
   this file, which is what lets it win. */
.chip-filter,
.chip-clear {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 42px;
  padding: 0 14px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--pill);
  font: inherit;
  font-size: 0.88rem;
  font-weight: 650;
  text-decoration: none;
  cursor: pointer;
  transition: opacity 0.15s ease, border-color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
.chip-filter { background: var(--bg-elev); color: var(--ink); opacity: 0.7; }
.chip-filter:hover { text-decoration: none; border-color: var(--accent); }
.chip-filter:active,
.chip-clear:active { transform: scale(0.96); }
.chip-filter.is-on {
  opacity: 1;
  background: var(--ecolor-soft, var(--accent-soft));
  border-color: var(--ecolor, var(--accent));
  color: var(--ecolor, var(--accent-chip-ink));
}
/* "Tout" — it clears the filters rather than being one. Hollow and dashed, so a
   thumb running along the row never mistakes it for another thématique. */
.chip-clear {
  background: transparent;
  border-style: dashed;
  color: var(--muted);
}
.chip-clear:hover { text-decoration: none; border-color: var(--accent); color: var(--accent); }

/* --- Photo uploader --------------------------------------------------- */
/* An atelier's photos ARE its instructions — the practitioner shoots the pose
   rather than describing it. Hence a camera button and a gallery button side by
   side, and a grid whose order is the stored order (the first tile is the
   cover, see Atelier.Cover). */
#photo-uploader { display: block; }
.photo-grid { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 10px; }
.photo-grid:not(:empty) { margin-bottom: 12px; }
.photo-preview {
  position: relative;
  width: 88px;
  height: 88px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--bg-elev);
}
.photo-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-remove-btn {
  position: absolute;
  top: 3px;
  right: 3px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 2px solid var(--card);
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  font-size: 0.8rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform 0.1s ease;
}
.photo-remove-btn:active { transform: scale(0.88); }
.photo-remove-btn::after { content: ""; position: absolute; inset: -9px; } /* thumb-sized hit area */
/* Re-crop what is already in the tile. Bottom-right, opposite the ✕ so a thumb
   never hits the wrong one — and stacked, because the tile clips its overflow
   around a full-bleed <img>: without the z-index the button is behind the photo
   and no tap ever reaches it. */
.photo-crop-btn {
  position: absolute;
  right: 3px;
  bottom: 3px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 2px solid var(--card);
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 0.72rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform 0.1s ease;
}
.photo-crop-btn:active { transform: scale(0.88); }
.photo-crop-btn::after { content: ""; position: absolute; inset: -9px; } /* thumb-sized hit area */
/* One editor at a time: app.js disables every ✂ while the cropper is open. */
.photo-crop-btn:disabled { opacity: 0.45; cursor: default; }
/* A picked photo shows a spinner until the browser has downscaled it and its
   preview fills in — the downscale happens here, not on the server. */
.photo-preview.loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-radius: 50%;
  border: 2.5px solid var(--line-strong);
  border-top-color: var(--accent);
  animation: spin 0.7s linear infinite;
}
.photo-buttons { display: flex; gap: 12px; }
.photo-add-btn {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--bg-elev);
  border: 1.5px solid var(--line-strong);
  color: var(--accent);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.photo-add-btn:hover { background: var(--accent-soft); border-color: var(--accent); }
.photo-add-btn:active { transform: scale(0.92); }
.photo-add-btn:focus-within { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* The grid is itself sortable: the order the tiles sit in is the order the
   inputs (and the files) are submitted in, which is the order that gets stored.
   Dragging a tile to the front therefore chooses the cover. */
.sortable-photos .photo-preview { cursor: grab; user-select: none; -webkit-user-drag: none; }
.sortable-photos .photo-preview:active { cursor: grabbing; }
.sortable-photos .photo-preview img { pointer-events: none; } /* the drag must not become an image drag */
.sortable-photos .photo-preview:first-child::before {
  content: "Couverture";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  padding: 2px 4px;
  background: color-mix(in srgb, #0b0d13 62%, transparent);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  text-align: center;
  letter-spacing: 0.02em;
}
.sortable-photos .photo-preview.loading::before { content: none; }

/* Tap a photo to see it whole. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: none;
  place-items: center;
  padding: 18px;
  background: rgba(8, 10, 16, 0.88);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.lightbox:not([hidden]) { display: grid; }
.lightbox.show { opacity: 1; }
.lightbox-img {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}

/* --- Crop editor ------------------------------------------------------ */
/* A photo of a pose is shot in a hurry, off-angle, with half the room in it.
   The cropper is where it becomes a usable illustration — dark, so the photo is
   the only thing lit.

   The shell is a modal <dialog> app.js builds (see buildCropper): showModal()
   is what gives it the stacking, the backdrop, the focus trap and the Escape
   key, which is why nothing here carries a z-index to keep in step with the
   sheet's. What is left to this stylesheet is stripping the UA's dialog chrome.
   Everything INSIDE the stage — the quad, the dimmed outside, the four handles
   — is painted on the canvas rather than marked up, because positioned handles
   would need inline coordinates and style-src has no 'unsafe-inline'. */
.crop-dialog {
  padding: 0;
  border: 0;
  width: 100%;              /* the editor is the whole screen, as it was */
  max-width: 100%;
  max-height: 100dvh;
  background: #0b0d13;
  color: #fff;
  overflow: hidden;
}
/* The flex layout hangs on [open], not on .crop-dialog itself: an author rule
   beats the UA's `dialog:not([open]) { display: none }` whatever its
   specificity, so a bare display:flex here would leave the editor on screen
   while it is closed. */
.crop-dialog[open] { display: flex; flex-direction: column; }
.crop-dialog::backdrop { background: rgba(6, 8, 14, 0.82); }

.crop-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.crop-canvas {
  display: block;
  max-width: 100%;
  max-height: 100%;
}
/* Without this a corner drag is a page scroll on a phone: the browser claims
   the gesture and the pointer stream dies mid-drag on a pointercancel. */
.crop-canvas,
.crop-stage { touch-action: none; }

/* "Traitement…" while the de-warp runs. It holds the main thread for a couple
   of hundred milliseconds, and a still editor saying nothing reads as frozen. */
.crop-busy {
  flex: none;
  margin: 0;
  padding: 10px 14px;
  text-align: center;
  font-weight: 700;
  color: #fff;
  background: #12151d;
}
.crop-busy[hidden] { display: none; }
/* While it works the buttons stop answering: the answer is already on its way. */
.crop-dialog.is-busy .crop-stage { opacity: 0.65; }
.crop-dialog.is-busy .crop-bar { opacity: 0.5; pointer-events: none; }

.crop-bar {
  flex: none;
  display: flex;
  gap: 8px;
  padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
  background: #12151d;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
/* app.js builds these four buttons itself and gives them no .btn class, so they
   carry their own look. The literal colours are deliberate: on the editor's own
   dark ground the theme tokens are those of the page underneath, not of this
   overlay. .crop-rotate / .crop-scan / .crop-cancel are hooks for the script
   alone — only the toggled and the committing state look different. */
.crop-btn {
  flex: 1 1 0;
  min-width: 0;
  min-height: 46px;
  padding: 13px 8px;
  border: 0;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  font: inherit;
  font-weight: 700;
  font-size: 0.92rem;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.1s ease;
}
.crop-btn:hover { background: rgba(255, 255, 255, 0.18); }
.crop-btn:active { transform: scale(0.97); }
/* "Mode scan" is a toggle — it has to look pressed while it is on. */
.crop-btn.is-on { background: var(--accent-2); color: #14101f; }
/* Valider is the one button that commits. */
.crop-ok { background: var(--accent); color: var(--accent-ink); }
.crop-ok:hover { background: var(--accent); filter: brightness(1.06); }

/* --- Spinner ---------------------------------------------------------- */
.spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--line-strong);
  border-top-color: var(--accent);
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --- Focus visibility & in-flight feedback ---------------------------- */
.btn:focus-visible,
.fab:focus-visible,
.icon-btn:focus-visible,
.row:focus-visible,
.row-main:focus-visible,
.tab:focus-visible,
.chip-filter:focus-visible,
.chip-clear:focus-visible,
.btn-link-danger:focus-visible,
.tick:focus-visible,
.tile-link:focus-visible,
.tl-card:focus-visible,
.photo-strip-item:focus-visible,
.row-photo:focus-visible,
.removed-summary:focus-visible,
.drag-handle:focus-visible,
.photo-add-btn:focus-visible,
.photo-remove-btn:focus-visible,
.photo-crop-btn:focus-visible,
.crop-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--card), 0 0 0 4px var(--accent);
}
/* A whole row lighting up for a keyboard is louder than the thing that actually
   took focus, so the inner link keeps the ring inside the row. */
.row-main:focus-visible { box-shadow: none; outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--radius-sm); }

/* htmx tags the element that fired an in-flight request: dim it and swallow
   repeat taps until the response swaps in. */
.htmx-request { opacity: 0.55; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* --- Login ------------------------------------------------------------ */
/* A page on its own: no top bar, no tab bar, nothing to reach from here. The
   card is centred rather than sitting under a header, because the only thing
   this screen asks is a password (see views.LoginPage). */
.login-body {
  display: grid;
  place-items: center;
  padding: 24px 16px calc(24px + env(safe-area-inset-bottom));
}
.login { width: 100%; max-width: 380px; }
.login-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  padding: 26px 22px 24px;
  text-align: center;
}
.login-logo {
  display: grid;
  place-items: center;
  width: 60px;
  height: 60px;
  margin: 0 auto 14px;
  border-radius: var(--radius-md);
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  box-shadow: var(--shadow-sm);
}
.login-logo svg { display: block; }
.login-title {
  margin: 0 0 4px;
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.login-sub { margin: 0 0 20px; color: var(--muted); font-size: 0.92rem; }
/* "Identifiant ou mot de passe incorrect" — said in French, which is the whole
   reason this page exists instead of a basic-auth prompt. */
.login-error {
  margin: 0 0 16px;
  padding: 10px 13px;
  border-radius: var(--radius-sm);
  background: var(--danger-soft);
  color: var(--danger-ink);
  font-size: 0.9rem;
  font-weight: 650;
}
/* The fields read left-aligned even though the card is centred: a label above a
   box is read from the left. */
.login-form { text-align: left; }
.login-form .field:last-of-type { margin-bottom: 20px; }

/* --- Section head ----------------------------------------------------- */
/* A title and the one action that goes with it ("Le déroulé" + "+ Ajouter un
   atelier"). Used both inside a card and bare on the page. */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 2px 0 10px;
}
.section-head h2 { margin: 0; min-width: 0; }
.section-head .btn { flex: none; }
/* The plan and the timeline are bare blocks on the page — the rows are the
   cards — so they owe the next section the gap a card would have given. */
#seance-plan,
#modele-plan,
#timeline { margin-bottom: 16px; }

/* --- Inside a row ----------------------------------------------------- */
/* An hour, at the head of a séance row. Tabular and fixed-width, so a day's
   worth of rows reads as a column. */
.row-time {
  flex: none;
  min-width: 3.1em;
  font-weight: 700;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* The 1, 2, 3 of the plan. Quiet on purpose: the order matters, the number
   itself does not. */
.row-rank {
  flex: none;
  width: 20px;
  text-align: center;
  font-size: 0.8rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
/* A précaution on a person's row — the one line that must be seen before she is
   touched, wherever her name appears. */
.row-warn { color: var(--danger-ink); font-weight: 650; }
/* The chevron of a row that goes somewhere without being a link itself. */
.row-go { flex: none; color: var(--line-strong); font-size: 1.5rem; line-height: 1; }
.row-thumb {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--bg-elev);
}
/* The initial of someone who has no photo — and nobody has a photo. */
.avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-chip-ink);
  font-weight: 800;
  font-size: 0.95rem;
  text-transform: uppercase;
}
/* The photos of an atelier, one tap from the plan: the thumbnail IS the button
   (app.js opens the lightbox from its data-full). */
.row-photo {
  position: relative;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  overflow: hidden;
  cursor: zoom-in;
}
.row-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.row-photo-count {
  position: absolute;
  right: 0;
  bottom: 0;
  padding: 1px 4px;
  background: color-mix(in srgb, #0b0d13 62%, transparent);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  border-top-left-radius: 6px;
}
/* A row inside a card would be a white tile on a white card: lift it onto the
   elevated ground instead and drop the shadow that says "I float". */
.card .row { background: var(--bg-elev); box-shadow: none; }
.card .list { margin-top: 2px; }
.people-list { gap: 8px; }
/* A trailing control (the ✎ of an attendee, the ✕ of a modèle's step) sits
   tight against the edge — the row's own padding is already the gap. */
.person-row > .icon-btn,
.plan-item > .icon-btn { margin-right: -4px; }

/* --- Séance rows ------------------------------------------------------ */
.seance-row .row-title { font-weight: 700; }
/* Annulée: still there, still readable, no longer asking to be prepared. */
.row.is-cancelled { opacity: 0.62; }
.row.is-cancelled .row-title { text-decoration: line-through; }
/* Les réservations sans séance. Dashed, because they are not séances yet — one
   tap is what makes them real. */
.pending { background: var(--bg-elev); }
.pending-row { border-style: dashed; }
/* Le déroulé: a step of a running séance. Tighter than a plain row — a handle,
   a tick, the title, a duration and a photo all have to fit a phone. */
.plan-item { gap: 8px; padding-left: 10px; }
/* Un atelier ajouté en séance, absent du modèle. An inset bar rather than a
   border: a real border would shift every other row by 3px. */
.plan-item.is-added { box-shadow: inset 3px 0 0 var(--accent), var(--shadow-sm); }
.plan-item.is-done { background: var(--bg-elev); }
.plan-item.is-done .row-title { text-decoration: line-through; color: var(--muted); }
/* The tick: what is done, mid-séance, one thumb, without looking. 34px of ink
   with 44px of tap around it (::after), so the row keeps its height. */
.tick {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 2px solid var(--line-strong);
  border-radius: 50%;
  background: var(--card);
  color: transparent;              /* the ✓ only shows once it is checked */
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.tick::after { content: ""; position: absolute; inset: -5px; }
.tick svg { display: block; }
.tick:active { transform: scale(0.9); }
.plan-item.is-done .tick {
  background: var(--success);
  border-color: var(--success);
  color: #fff;
}
/* Ce qu'on a retiré — part of what happened, and one tap from coming back, but
   never in the way of what is left to do. */
.removed-block {
  margin-top: 14px;
  padding: 0 12px;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-md);
}
.removed-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  color: var(--muted);
  font-size: 0.9rem;
  font-weight: 650;
  cursor: pointer;
  list-style: none;
}
.removed-summary::-webkit-details-marker { display: none; }
.removed-summary::after { content: "▾"; margin-left: auto; transition: transform 0.15s ease; }
.removed-block[open] .removed-summary::after { transform: rotate(180deg); }
.removed-list { margin: 2px 0 12px; }
.row-removed { background: transparent; border-style: dashed; box-shadow: none; }
.row-removed .row-title { flex: 1 1 auto; min-width: 0; text-decoration: line-through; color: var(--muted); }
/* A count beside a label ("Retirés 3"). */
.done-count {
  display: inline-flex;
  align-items: center;
  min-width: 20px;
  justify-content: center;
  padding: 1px 6px;
  border-radius: var(--pill);
  background: var(--bg-elev);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* --- Séance header ---------------------------------------------------- */
.seance-head { padding-bottom: 13px; }
.seance-head-row { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-bottom: 13px; }
/* A modèle with no discipline renders the row with nothing in it. */
.seance-head-row:empty { display: none; }
.durbar-wrap { margin-top: 2px; }
/* A modèle whose ateliers overrun the length it targets says so where the
   number is, not only on the bar. */
.badge-min.is-over {
  background: var(--danger-soft);
  border-color: transparent;
  color: var(--danger-ink);
}
/* The count (or the slot) the top bar carries on the right. */
.app-count {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 5px 11px;
  border-radius: var(--pill);
  background: var(--accent-soft);
  color: var(--accent-chip-ink);
  font-size: 0.82rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- Settings rows ---------------------------------------------------- */
/* A thématique or a discipline: a colour, a name, and a handle to arrange them
   in the order the pickers will offer them. */
.setting-row .row-main { gap: 12px; }
.setting-row .row-title { font-weight: 650; }

/* --- Tiles (the atelier library) -------------------------------------- */
/* Photo first: an atelier is recognised by its pose, not by its title. Two
   columns on a phone, which is the widest a thumb can span. */
.tiles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
@media (min-width: 560px) { .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.tile { min-width: 0; }
.tile-link {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  color: inherit;
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
.tile-link:hover { text-decoration: none; border-color: var(--line-strong); box-shadow: var(--shadow-md); }
.tile-link:active { transform: scale(0.985); }
/* Square, always: a grid of ragged photos is unreadable at a glance. */
.tile-media {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  background: var(--bg-elev);
}
.tile-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No photo yet — the atelier is still usable, it just has nothing to show. */
.tile-placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 2.4rem;
  opacity: 0.45;
}
.tile-count {
  position: absolute;
  right: 6px;
  bottom: 6px;
  padding: 2px 7px;
  border-radius: var(--pill);
  background: color-mix(in srgb, #0b0d13 62%, transparent);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.tile-body { display: flex; flex-direction: column; gap: 6px; padding: 10px 11px 12px; min-width: 0; }
/* Two lines at most: past that the tile is taller than the photo it is about. */
.tile-title {
  font-weight: 700;
  line-height: 1.25;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.tile-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }

/* --- Timeline (a person's history) ------------------------------------ */
/* A rail with one dot per entry: séances as cards, loose notes lighter. This is
   the screen read months later, so the DAY is the headline of every entry and
   everything else is subordinate to it. */
.timeline {
  list-style: none;
  margin: 0;
  padding: 0 0 0 28px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.timeline::before {
  content: "";
  position: absolute;
  left: 9px;
  top: 10px;
  bottom: 10px;
  width: 2px;
  border-radius: var(--pill);
  background: var(--line);
}
.tl-entry { position: relative; }
/* Centred on the rail (28px of padding − 25px of offset − half of 14px), with a
   ring of page colour punching a hole in the rail behind it. */
.tl-dot {
  position: absolute;
  left: -25px;
  top: 16px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--line-strong);
  box-shadow: 0 0 0 3px var(--bg);
}
/* A séance is the load-bearing entry of a history — the one that happened, and
   the one that can be opened again. Its dot is the accent one. */
.tl-seance .tl-dot { background: var(--accent); }
/* A note is not a séance: hollow, and in the neutral ink. */
.tl-dot-note { background: var(--bg); border: 3px solid var(--line-strong); }
.tl-card { display: block; margin-bottom: 0; color: inherit; text-decoration: none; }
.tl-card:hover { text-decoration: none; }
a.tl-card { transition: border-color 0.15s ease, box-shadow 0.15s ease; }
a.tl-card:hover { border-color: var(--line-strong); box-shadow: var(--shadow-md); }
.tl-card > p:last-child { margin-bottom: 0; }
/* A loose note reads quieter than a séance — it is context, not an event. */
.tl-note-entry .tl-card { background: var(--bg-elev); box-shadow: none; }
.tl-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
/* The scannable thing on the whole screen. */
.tl-day { font-size: 1.02rem; font-weight: 800; letter-spacing: -0.01em; }
.tl-time { font-size: 0.84rem; font-weight: 650; color: var(--muted); font-variant-numeric: tabular-nums; }
.tl-title { margin: 0 0 8px; font-weight: 650; overflow-wrap: anywhere; }
.tl-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.tl-changes {
  list-style: none;
  margin: 10px 0 0;
  padding: 9px 0 0;
  border-top: 1px dashed var(--line);
  display: flex;
  flex-direction: column;
  gap: 5px;
}
/* Her note of that day, in her own words on the card of the séance it belongs
   to. */
.tl-note {
  margin: 10px 0 0;
  padding-left: 10px;
  border-left: 3px solid var(--line);
  color: var(--muted);
  font-size: 0.9rem;
}

/* --- Changes ---------------------------------------------------------- */
/* What was done to a séance after it was created from its modèle. A record,
   never an alert: only the glyph carries a colour, the sentence stays muted. */
.changes .card-hint { margin-bottom: 12px; }
.change-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.change {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 0.88rem;
  line-height: 1.45;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.tl-changes .change { font-size: 0.82rem; }
.change-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 19px;
  height: 19px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--bg-elev);
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1;
}
.change-added .change-icon { background: var(--success-soft); color: var(--success-ink); }
.change-removed .change-icon { background: var(--danger-soft); color: var(--danger-ink); }
.change-moved .change-icon { background: var(--accent-soft); color: var(--accent-chip-ink); }
.change-retimed .change-icon { background: var(--warn-soft); color: var(--warn-ink); }

/* --- Coverage --------------------------------------------------------- */
/* Declared versus covered, side by side: a filled chip is both, .chip-missing
   is declared and uncovered, .chip-extra is covered and undeclared (both are
   defined with the other chips, above). */
.coverage { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 12px; }
/* The minutes an atelier actually spends on a thématique, inside its chip. */
.chip-min { font-size: 0.7rem; font-weight: 700; opacity: 0.75; font-variant-numeric: tabular-nums; }

/* --- Provenance & status chips ---------------------------------------- */
/* Rattachée à une réservation: a provenance mark, not a state — quiet, hollow,
   the 📅 in the text is the whole of the signal. */
.chip-booking { background: transparent; border-color: var(--line-strong); }
/* A booking calendar wired to no discipline. Not an error: something to finish
   in les réglages, which is what the warn tint says. */
.chip-unmapped {
  background: var(--warn-soft);
  border-color: transparent;
  color: var(--warn-ink);
  border-style: dashed;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  display: inline-block;
}

/* --- Alerts ----------------------------------------------------------- */
/* Les précautions above all — the one thing that must never be scrolled past —
   and the warnings booking sends back when it cannot be reached. The inset bar
   is what makes it a banner rather than a paragraph. */
.alert {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 12px;
  padding: 11px 13px 11px 16px;
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.alert-danger { background: var(--danger-soft); color: var(--danger-ink); box-shadow: inset 3px 0 0 var(--danger); }
.alert-warn { background: var(--warn-soft); color: var(--warn-ink); box-shadow: inset 3px 0 0 var(--warn); }

/* --- Day navigation --------------------------------------------------- */
/* Previous / the day itself / next, in one thumb-row. The middle one is a real
   date input, so three weeks out is one tap rather than twenty-one. */
.daynav { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; }
.daynav .icon-btn { width: 44px; height: 44px; font-size: 1.5rem; font-weight: 700; line-height: 1; }
.daynav-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* --- Search ----------------------------------------------------------- */
/* The margin lives on the field, not on the form: the same input is used bare
   inside the sheet (PeoplePicker), where there is no form around it. */
.search { margin: 0; }
.search-input { margin-bottom: 12px; }

/* --- The picker sheet ------------------------------------------------- */
/* The library seen from inside the sheet. Its filter row runs edge to edge
   rather than stopping at the panel's padding, so the last chip is visibly
   scrollable instead of looking like the end. */
.picker .filters { margin-inline: -18px; padding-inline: 18px; }
.picker-list { margin-bottom: 4px; }
/* Nobody in the list: a walk-in has no reservation to have come from, so she is
   created right here rather than on another screen. */
.picker-create { margin-top: 16px; padding-top: 14px; border-top: 1px dashed var(--line); }

/* --- Forms (continued) ------------------------------------------------ */
/* .field is worn by <fieldset> as well as <label>; the UA's own border and
   padding would frame every group of chips. */
fieldset.field { border: 0; padding: 0; min-width: 0; }
legend.field-label { padding: 0; }
/* The panel's own top padding is 10px — enough for a swap not to jump, not
   enough for a form to start on. */
.sheet-form { display: block; padding-top: 4px; }
/* "min" pinned inside the box: the unit belongs to the number, and a phone
   keyboard is already covering half the screen. */
.input-suffix { position: relative; display: block; }
.input-suffix .input { padding-right: 52px; -moz-appearance: textfield; appearance: textfield; }
.input-suffix .input::-webkit-outer-spin-button,
.input-suffix .input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.suffix {
  position: absolute;
  top: 50%;
  right: 14px;
  transform: translateY(-50%);
  color: var(--muted);
  font-size: 0.86rem;
  font-weight: 650;
  pointer-events: none;
}
/* Les précautions: the field is tinted like the banner it will become. */
.input-warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line-strong)); background: var(--warn-soft); }
.input-warn:focus { border-color: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
/* Archiver, retirer, supprimer: real, reachable, and never mistaken for the
   save button — hence its own row, below the actions, in text. */
.form-danger { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); text-align: center; }
.btn-link-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 10px 16px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--danger);
  font: inherit;
  font-size: 0.92rem;
  font-weight: 650;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.1s ease;
}
.btn-link-danger:hover { background: var(--danger-soft); text-decoration: none; }
.btn-link-danger:active { transform: scale(0.97); }

/* --- Radio rows (le modèle d'une nouvelle séance) --------------------- */
/* A whole row is the target, not the 20px circle in it. */
.radio-row,
.check-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  margin-bottom: 8px;
  padding: 11px 13px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.radio-row input,
.check-row input {
  flex: none;
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
}
.radio-row:has(input:checked),
.check-row:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.radio-row:has(input:focus-visible),
.check-row:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--accent-soft); }
.radio-body,
.check-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.radio-title,
.check-title { font-weight: 700; line-height: 1.3; overflow-wrap: anywhere; }
/* What the modèle is made of, chips included, on the line under its name. */
.radio-hint,
.check-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 0.84rem;
  color: var(--muted);
}
.check-list { display: flex; flex-direction: column; }

/* --- The atelier sheet, mid-séance ------------------------------------ */
/* The photos of the pose, big enough to be read at arm's length, scrolling
   inside themselves rather than pushing the steps off the screen. */
.photo-strip {
  display: flex;
  gap: 10px;
  margin: 0 0 16px;
  padding-bottom: 4px;
  overflow-x: auto;
  scrollbar-width: none;
}
.photo-strip::-webkit-scrollbar { display: none; }
.photo-strip-item {
  flex: none;
  width: 118px;
  height: 118px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  overflow: hidden;
  cursor: zoom-in;
}
.photo-strip-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Les étapes, read out one after the other while the séance runs: bigger than
   body text, and spaced enough that a glance finds its place again. */
.steps { margin: 0 0 18px; padding-left: 1.5rem; display: flex; flex-direction: column; gap: 9px; }
.steps li { font-size: 1.02rem; line-height: 1.5; padding-left: 3px; }
.steps li::marker { color: var(--accent); font-weight: 800; }

/* --- The sheet's scrim ------------------------------------------------ */
/* #sheet is the scrim, and this node is the click surface inside it: absolute,
   so it never becomes a flex item beside the panel. */
.sheet-backdrop { position: absolute; inset: 0; }

/* --- Utilities -------------------------------------------------------- */
/* Free-typed notes keep the line breaks they were typed with. */
.prewrap { white-space: pre-wrap; overflow-wrap: anywhere; }
/* For screen readers only — the label of a colour whose dot is the whole of
   what is seen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (prefers-color-scheme: dark) {
  /* An empty tick is a 2px ring and nothing else: --line-strong is too quiet
     against the dark card for something a thumb has to find without looking. */
  .tick { border-color: var(--muted); }
  /* The rail and the hollow dot of a note both live on --bg here, which is the
     darkest surface of the theme — the note dot needs the stronger edge. */
  .tl-dot-note { border-color: var(--muted); }
}

/* --- The sheet's own chrome -------------------------------------------
   A sheet opened on a phone must be closable with a thumb. The backdrop tap
   works and Escape works, but neither is discoverable — and Escape does not
   exist on a phone at all. The grip is the affordance that this is a panel;
   the ✕ is the way out. */
.sheet-grip {
  width: 38px;
  height: 4px;
  margin: 0 auto 10px;
  border-radius: var(--pill);
  background: var(--line-strong);
}
.sheet-close {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--pill);
  background: var(--card);
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
/* 44px of tap target without 44px of ink, the way the other small buttons do it. */
.sheet-close::after { content: ""; position: absolute; inset: -5px; }
.sheet-close:hover { color: var(--ink); background: var(--bg-elev); }
.sheet-close:active { transform: scale(0.92); }
