/* ===========================================================================
   grid.css — Booking grid. A NEW COMPONENT under design_system.md §22.

   Added by: AI (Claude) | 2026-09-02
   Reason: the system has no component for a two-dimensional availability matrix
   (resources x time slots) with per-cell state and a touch-first form factor.
   Searched §§1-22 first: §13 .datepicker picks a DAY, not a slot; §7 Data Table and
   the ixt-* engine are row-oriented lists; §15 Range Selector selects a continuous
   numeric range, not a discrete cell in a matrix.

   Prefix `sg-` (verified unused in the design system).

   ONE DOM, TWO PRESENTATIONS. The server renders slot-major order once; CSS alone
   chooses which. One keyboard model, one render path, no JS branching.

     MATRIX (the default, every width) — courts are columns, slots are rows.
     STACKED (`.sg--stack`, only below 48em) — rows of wrapped cells, the court name
     moving into each cell.

   WHICH ONE, AND WHY IT IS NOT PURELY A BREAKPOINT: the deciding factor is the COURT
   COUNT, not the viewport. A club with three courts fits a matrix comfortably at
   390px (56px gutter + 3 columns), and the matrix is strictly better — the time reads
   once per row instead of once per cell, and the columns line up. Choosing by width
   alone produced a 2+1 wrap at 390px, with the third court on its own full-width row
   detached from its time. The server sets `.sg--stack` from the court count instead.

   Constraints honoured: tokens only, no hardcoded colours; spacing in multiples of
   4px; type from the existing 11-24px scale; Lucide stroke-2 icons at 14px in cells
   and 16px in the day header; every state carries a word or a glyph, never colour
   alone; all six states plus dark.

   DARK MODE NOTE: .is-past and .is-closed paint var(--surface), which does NOT flip
   in the shipped dark token set. This component therefore DEPENDS ON DEVIATION D9
   (app.css) and will render white cells without it. The .is-closed hatch is drawn
   from var(--border) so it inverts with the theme — never a literal grey.
   =========================================================================== */

.sg-wrap {
  --sg-gutter: 52px;
  --sg-gap: 4px;
  --sg-min: 0;

  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 12px;
}

@media (min-width: 48em) {
  .sg-wrap { --sg-gutter: 104px; --sg-min: 88px; padding: 16px; }
}

/* ---------------------------------------------------------------------------
   Day header: previous, the date, next.
   Ni « aujourd'hui » ni sélecteur de calendrier : le premier se lisait comme une
   étiquette au lieu d'un bouton, le second n'a jamais existé ici. Aucun des deux
   restants n'est pondéré, §2 n'admettant qu'une action primaire par surface, et
   celle de la grille vit dans l'écran qu'ouvre une cellule.
   --------------------------------------------------------------------------- */
/* LA DATE AU CENTRE, LES FLÈCHES AUX DEUX BORDS.
   Les deux commandes encadraient la date d'un même côté, et une règle sous 480 px la
   renvoyait à la ligne au-dessus d'elles : la barre prenait deux hauteurs pour dire un
   seul mot. Cette règle datait de l'époque où la barre portait quatre contrôles ; il
   n'en reste que deux, et tout tient largement sur une ligne.

   Les flèches rétrécissent pour que la date grandisse : c'est elle qu'on lit, elles
   qu'on touche une fois de temps en temps. 36 px reste une cible confortable au pouce,
   et le geste de glissement sur le tableau reste le chemin principal. */
.sg-dayhead {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.sg-dayhead-date {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  font-size: 18px;
  font-weight: 700;
  line-height: 24px;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  /* La date reste ce qui cède si la ligne devient étroite : les flèches, elles, ne
     rétrécissent ni ne passent à la ligne. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sg-dayhead .btn {
  flex: 0 0 auto;
  width: 36px;
  min-width: 0;
  height: 36px;
  padding: 0;
  justify-content: center;
}
.sg-dayhead .btn svg { width: 18px; height: 18px; }

@media (min-width: 30em) {
  .sg-dayhead-date { font-size: 19px; line-height: 26px; }
}

/* Court filter — shown above 6 courts, where a row stops being scannable. */
.sg-courtfilter {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
  overflow-x: auto;
  padding-bottom: 4px;
  scrollbar-width: none;
}
.sg-courtfilter::-webkit-scrollbar { display: none; }
.sg-courtchip {
  flex: 0 0 auto;
  min-height: 32px;
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 550;
  white-space: nowrap;
  background: var(--fill-tertiary);
  border: 0;
  border-radius: var(--radius);
  color: var(--foreground);
  cursor: pointer;
}
.sg-courtchip[aria-pressed="true"] { background: var(--foreground); color: var(--background); }

/* ---------------------------------------------------------------------------
   MATRIX — the default presentation at every width.

   subgrid is the load-bearing feature: it lets .sg-slotrow stay a real element (which
   is what makes the stacked mode possible from the same DOM) while its cells align to
   the parent's columns. The documented fallback is display:contents, which loses the
   row box but keeps the alignment.
   --------------------------------------------------------------------------- */
.sg {
  display: grid;
  grid-template-columns: var(--sg-gutter) repeat(var(--sg-n, 3), minmax(var(--sg-min), 1fr));
  gap: var(--sg-gap);
}

.sg-headrow,
.sg-slotrow {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  gap: var(--sg-gap);
}
@supports not (grid-template-columns: subgrid) {
  .sg-headrow, .sg-slotrow { display: contents; }
}

.sg-corner { display: block; }

.sg-courthead {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: var(--sg-sticky-top, 0);
  z-index: 2;
  padding: 6px 8px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  font-weight: 600;
  line-height: 16px;
  /* A court name is short; a long one clips rather than widening its column. */
  overflow: hidden;
}
.sg-courthead-sub {
  font-size: 11px;
  font-weight: 450;
  color: var(--foreground-subdued);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (min-width: 48em) {
  .sg-courthead { padding: 8px; font-size: 13px; line-height: 18px; }
}

.sg-time {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 6px;
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--card);
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  color: var(--foreground-subdued);
  font-variant-numeric: tabular-nums;
  /* La plage sur DEUX lignes au téléphone (début au-dessus, fin en dessous, plus
     discrète) : la gouttière reste étroite et les cases gardent leur largeur. */
  flex-direction: column;
  align-items: flex-end;
  line-height: 15px;
}
.sg-time-sep { display: none; }
.sg-time-end { font-weight: 400; opacity: .7; }
@media (min-width: 48em) {
  /* Sur une ligne dès qu'il y a la place : « 08:00 – 09:30 ». */
  .sg-time { flex-direction: row; align-items: center; gap: 4px; }
  .sg-time-sep { display: inline; opacity: .5; }
}

.sg-cell {
  min-width: 0;
  min-height: 44px;      /* deviation D1: a thumb, not a cursor */
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  padding: 6px;
  font-family: var(--font);
  font-size: inherit;
  text-align: left;
  /* A free cell is an <a> so the flow works without JavaScript — but it is a CELL, not
     a link in prose, and the inherited underline made every free slot look like body
     text with a rule under it. */
  text-decoration: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--foreground);
  cursor: pointer;

  /* Removes the 300ms double-tap delay without disabling zoom on the page. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
@media (min-width: 48em) {
  .sg-cell { min-height: 48px; padding: 8px; }
}

/* In the matrix the court is a column header, so the cell's second line is free for
   the closure reason instead. The stacked presentation turns it back on. */
.sg-cell-court { display: none; }

.sg-cell-t {
  font-size: 12.5px;
  font-weight: 550;
  line-height: 16px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.sg-cell-s {
  font-size: 11px;
  font-weight: 450;
  line-height: 14px;
  color: var(--foreground-subdued);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sg-cell-s:empty { display: none; }
.sg-cell svg { width: 14px; height: 14px; flex: 0 0 auto; }

@media (min-width: 48em) {
  .sg-cell-t { font-size: 13px; }
}

/* The pressed recipe copies the design system's own: a 1px content nudge plus a
   darkening inset. NOT transform: scale() — §3 forbids reaching a size by a
   transform, and it blurs the cell's border. */
.sg-cell:not([aria-disabled="true"]):active {
  box-shadow: 0 2px 0 0 rgba(0, 0, 0, .06) inset;
  padding-top: 7px;
  padding-bottom: 5px;
}

.sg-cell:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  z-index: 3;
}

/* ---------------------------------------------------------------------------
   STACKED — narrow screens with enough courts that a matrix row stops being
   readable. `.sg--stack` is set by the server from the court count.
   --------------------------------------------------------------------------- */
@media (max-width: 47.99em) {
  .sg--stack { display: flex; flex-direction: column; }

  /* Hide the ROW, not just its children: an empty grid row would still collect a gap
     of blank space at the top. */
  .sg--stack .sg-headrow { display: none; }

  .sg--stack .sg-slotrow {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: var(--sg-gap);
  }

  .sg--stack .sg-time {
    flex: 0 0 var(--sg-gutter);
    position: static;
    justify-content: center;
    align-items: flex-start;
  }

  /* Two per line under the time label; four per line once there are many courts. */
  .sg--stack .sg-cell { flex: 1 1 calc(50% - var(--sg-gutter) / 2 - var(--sg-gap) * 2); }
  .sg--stack.sg--many .sg-cell { flex-basis: calc(25% - var(--sg-gutter) / 4 - var(--sg-gap) * 2); }

  /* The column header is gone here, so each cell names its own court. */
  .sg--stack .sg-cell-court { display: inline; }
}

/* ---------------------------------------------------------------------------
   States. Every one carries a WORD or a GLYPH as well as a colour.

   Doctrine check (§19 Shared): `taken` is NEUTRAL, because "settled states wear the
   neutral badge; tint is reserved for states that want something from someone".
   `mine` is success — a positive outcome the reader owns. `pending` is warning
   because it genuinely does want something (money, on the day). `closed` is neutral,
   not destructive: a closed court is not an error.
   --------------------------------------------------------------------------- */
.sg-cell.is-free {
  background: var(--card);
  border-color: var(--border);
}
.sg-cell.is-free:hover { border-color: var(--foreground-subdued); }

.sg-cell.is-mine {
  background: var(--success-surface);
  border-color: var(--success-border);
  color: var(--success-foreground);
}
.sg-cell.is-mine .sg-cell-s { color: var(--success-foreground); opacity: .85; }

.sg-cell.is-pending {
  background: var(--warning-surface);
  border-color: var(--warning-border);
  color: var(--warning-foreground);
}
.sg-cell.is-pending .sg-cell-s { color: var(--warning-foreground); opacity: .85; }

.sg-cell.is-taken {
  background: var(--fill-tertiary);
  border-color: var(--border-soft);
  color: var(--foreground-subdued);
  cursor: default;
}

.sg-cell.is-closed {
  background: var(--surface);
  border-color: var(--border-soft);
  color: var(--foreground-subdued);
  cursor: default;
  /* Hatch drawn from a token so it inverts in dark mode. A literal grey here would
     disappear against #0f0f0f — §21. */
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 3px,
    var(--border) 3px 4px
  );
}

.sg-cell.is-past {
  background: var(--surface);
  border-color: transparent;
  color: var(--foreground-disabled);
  cursor: default;
}

.sg-cell.is-beyond {
  background: var(--surface);
  border: 1px dashed var(--border-soft);
  color: var(--foreground-disabled);
  cursor: default;
}

/* Status dot, borrowed from the index table's shapes so the vocabulary matches. */
.sg-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: baseline;
}
.sg-dot.full { background: currentColor; }
.sg-dot.open { background: transparent; border: 1.5px solid currentColor; }

/* An off-grid booking: drawn at its REAL time, spanning the cells it covers, rather
   than rounded to the grid. Showing a member a time that is not the real time is
   worse than showing them an odd-looking row. */
.sg-span {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--sg-gap);
  padding: 8px 10px;
  font-size: 12px;
  line-height: 16px;
  background: var(--fill-tertiary);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  color: var(--foreground-subdued);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   Legend. MANDATORY, not optional chrome: a colour-blind member must be able to read
   the grid, and so must a printed copy.
   --------------------------------------------------------------------------- */
.sg-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
  font-size: 12px;
  color: var(--foreground-subdued);
}
.sg-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.sg-swatch {
  width: 14px; height: 14px;
  border-radius: 4px;
  border: 1px solid var(--border);
  flex: 0 0 auto;
}
.sg-swatch.is-free    { background: var(--card); }
.sg-swatch.is-mine    { background: var(--success-surface); border-color: var(--success-border); }
.sg-swatch.is-pending { background: var(--warning-surface); border-color: var(--warning-border); }
.sg-swatch.is-taken   { background: var(--fill-tertiary); border-color: var(--border-soft); }
.sg-swatch.is-closed  {
  background: var(--surface);
  background-image: repeating-linear-gradient(45deg, transparent 0 3px, var(--border) 3px 4px);
}
.sg-swatch.is-past    { background: var(--surface); border-color: var(--border-soft); }

.sg-empty { padding: 32px 16px; text-align: center; color: var(--foreground-subdued); font-size: 14px; }

/* Eight or more courts: tighten rather than widen. Above ~12 the matrix stops being
   scannable at all and the answer is a filtered list, not a wider grid — see the
   usage rules. */
.sg--dense { --sg-min: 64px; }
.sg--dense .sg-cell-s { font-size: 11px; }

@media (prefers-reduced-motion: reduce) {
  .sg-cell:active { padding-top: 6px; padding-bottom: 6px; box-shadow: none; }
}

/* ---------------------------------------------------------------------------
   Print — clubs pin the day to a corkboard.
   --------------------------------------------------------------------------- */
@media print {
  .sg--stack { display: grid; }
  .sg--stack .sg-headrow { display: grid; }
  .sg-courthead { position: static; }
  .sg-slotrow   { break-inside: avoid; }
  .sg-cell      { break-inside: avoid; }
  .sg-courtfilter, .sg-dayhead .btn { display: none; }
  .sg-legend    { break-inside: avoid; }
}

/* ---------------------------------------------------------------------------
   Le mouvement du changement de jour.

   Deux journées vides se ressemblent trait pour trait : sans mouvement, on glisse, la
   page se recharge à l'identique et on croit que le geste n'a pas pris. L'animation
   n'est pas un ornement, c'est la seule chose qui dise « il s'est passé quelque chose ».

   Elle raconte aussi la DIRECTION : le tableau sort du côté vers lequel on pousse et le
   suivant entre par l'autre bord, comme une page qu'on tourne. Un simple fondu dirait
   « ça a changé » sans dire « vers demain ».

   Les distances sont courtes et les durées sous 200 ms : au-delà, on attend le tableau
   au lieu de le lire.
   --------------------------------------------------------------------------- */
@keyframes sgEnterFromRight { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
@keyframes sgEnterFromLeft  { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: none; } }

.sg-wrap.sg-enter-next { animation: sgEnterFromRight .18s ease-out both; }
.sg-wrap.sg-enter-prev { animation: sgEnterFromLeft  .18s ease-out both; }

/* La sortie : un accusé de réception immédiat, avant même que la page suivante arrive.
   C'est ce qui fait la différence entre « ça a marché » et « j'ai raté mon geste ». */
.sg-wrap.sg-leave-next { transform: translateX(-18px); opacity: .35; transition: transform .13s ease-in, opacity .13s ease-in; }
.sg-wrap.sg-leave-prev { transform: translateX(18px);  opacity: .35; transition: transform .13s ease-in, opacity .13s ease-in; }

/* Pour qui a demandé moins de mouvement, il reste le fondu : l'information « la page a
   changé » ne doit pas disparaître, seule sa mise en scène. */
@media (prefers-reduced-motion: reduce) {
  @keyframes sgEnterFromRight { from { opacity: 0; } to { opacity: 1; } }
  @keyframes sgEnterFromLeft  { from { opacity: 0; } to { opacity: 1; } }
  .sg-wrap.sg-leave-next,
  .sg-wrap.sg-leave-prev { transform: none; transition: opacity .1s linear; }
}

/* ---------------------------------------------------------------------------
   L'espace au-dessus de la grille, sur téléphone.

   Le titre de page, sa marge, puis le cadre de la carte empilaient trois respirations
   avant la première ligne d'horaires. Sur un écran de 844 px, chacune coûte une demi
   rangée de créneaux, c'est-à-dire une information que l'on va chercher en défilant
   alors qu'elle pourrait être là.

   On resserre les marges, pas la grille : les cellules gardent leur taille et leurs
   cibles tactiles.
   --------------------------------------------------------------------------- */
@media (max-width: 47.99em) {
  .sg-wrap { padding: 10px; }

  /* Le titre « Réserver un court » et la date se répètent l'un l'autre : deux blocs
     pour dire où l'on est. On rapproche le second du premier plutôt que d'en supprimer
     un, parce que le titre sert aussi de repère au lecteur d'écran. */
  .tc-main > .tc-header { margin-bottom: 10px; min-height: 0; }
  .tc-main > .tc-header h1 { font-size: 20px; }
}
