/* ==========================================================================
   APPLICATION STYLES — "LA RÉGIE"

   The hero on home.html is the stadium. This is the control room. Same
   vocabulary, but the light becomes WORKING light. Drama in a tool you open
   forty times a day is torture, so none of the showcase machinery is here:
   no canvas, no beams, no parallax, no fade-to-black between views. On the
   whole admin the cost of the effects system is literally zero, because it is
   never mounted.

   WHAT DOES CROSS OVER, and it is the best line in this file:

       .card { border-top-color: var(--edge-gold); }

   One declaration. It says "lit from above", and it ties every panel in the
   dashboard back to the projector in the hero.

   Everything is written in LOGICAL PROPERTIES (`inline-start`, `margin-inline`,
   `border-inline`), so the Arabic RTL layout and the Latin LTR layout fall out
   of one rule set. Arabic is the DEFAULT language, not an afterthought.

   NO COMPONENT IN THIS FILE MAY WRITE A LITERAL `letter-spacing` VALUE.
   Only `var(--track-wide)` / `var(--track-tight)`, which are ZERO in Arabic —
   letter-spacing severs Arabic cursive joins and shatters words into isolated
   letters. See tokens.css, "the six rules".
   ========================================================================== */

@import url('./tokens.css');
@import url('./motion.css');

/* ==========================================================================
   0. RESET & BASE
   ========================================================================== */
* { margin: 0; padding: 0; box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/* The one physical-direction shim in the file.
   CSS gradients take no logical keywords, so the reading direction is bound
   to a variable once, here, and every "veil from the reading edge" gradient
   consumes it. This is why no component below needs `left` or `right`. */
html[dir='rtl'] { --to-inline-end: 270deg; --to-inline-start: 90deg; }
html[dir='ltr'] { --to-inline-end: 90deg;  --to-inline-start: 270deg; }

body {
  font-family: var(--ff-body);
  background: var(--bg);
  color: var(--text);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  min-block-size: 100vh;
  min-block-size: 100dvh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The gold hairline across the very top of the viewport. The light comes
   from above; everything below is lit by it. */
body::before {
  content: '';
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 2px;
  background: var(--grad-gold);
  z-index: 300;
  pointer-events: none;
}

::selection { background: var(--gold-200); color: var(--ink-on-gold); }

h1, h2, h3, .topbar-title, .card-title, .welcome-title, .stat-num {
  font-family: var(--ff-display);
  letter-spacing: var(--track-tight);
  line-height: 1.25;
}
h1 { font-size: var(--fs-xl); font-weight: 900; }
h2 { font-size: var(--fs-lg); font-weight: 700; }
h3 { font-size: var(--fs-md); font-weight: 700; }

/* Emphasis is WEIGHT AND GOLD. `font-style: italic` is banned from the
   system: there is no italic in Arabic, and a synthesised oblique of Cairo is
   an embarrassment. */
i, em, cite, address { font-style: normal; font-weight: 700; }

img { max-inline-size: 100%; block-size: auto; }

a { color: var(--brand-ink); }

/* ==========================================================================
   0b. NUMBERS — a correctness bug, not a matter of taste.

   Any isolated numeric run inside an RTL line (`+24%`, `3 - 1`, `12/03`) is
   REORDERED by the bidi algorithm and rendered WRONG: `%24+`. Isolating it
   fixes the direction without turning the surrounding sentence LTR.

   Every stat figure, every score, every short date, every numeric cell must
   be inside `<bdi class="num">`.
   ========================================================================== */
.num,
bdi,
.schedule-time, .match-score, .match-date,
td.num, .player-num, .stat-num, .trophy-year, .badge-count {
  direction: ltr;
  unicode-bidi: isolate;
  font-variant-numeric: tabular-nums;
}
/* `bdi` inside prose must not be forced LTR — only the `.num` flavour is. */
bdi:not(.num) { direction: inherit; }

/* ==========================================================================
   0c. ICONS — the emojis are dead.

   24×24 grid, fill:none, stroke:currentColor. Because the stroke is
   `currentColor`, an icon INHERITS the gold of an active nav item for free.
   Emoji rendered as empty squares on half the coaches' phones; a flawless
   rail that opens onto tables full of tofu boxes is WORSE than the old
   design, because the promise was made and then broken.
   ========================================================================== */
.ic {
  inline-size: 1.25em;
  block-size: 1.25em;
  flex: none;
  vertical-align: -0.18em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ic--lg { inline-size: 1.5em; block-size: 1.5em; }
.ic--sm { inline-size: 1em;   block-size: 1em; }

/* Directional glyphs (chevrons, logout, undo) must mirror with the text.
   A "next" chevron pointing right in Arabic is pointing BACKWARDS. */
html[dir='rtl'] .ic--flip { transform: scaleX(-1); }

/* ==========================================================================
   0d. THE KHATEM — the punctuation of the site.

   The eight-pointed star of Moroccan zellige: two squares at 45°. Order-8
   symmetry means it is identical under mirroring, so one shape serves Arabic
   and Latin alike.

   Permitted uses, and NO OTHERS: the hero separator, a list bullet, the
   spinner, the empty state, a rail watermark at 3%. Never over a photograph,
   never behind data, never above 12% opacity.
   ========================================================================== */
.khatem {
  display: inline-block;
  inline-size: 8px;
  block-size: 8px;
  flex: none;
  background: var(--gold-400);
  clip-path: var(--khatem-clip);
}
.khatem--lg { inline-size: 14px; block-size: 14px; }

/* The hero separator: rule / three khatems / rule — the three gold stars of
   the crest, quoted back. Markup: <p class="khatem-rule"><i class="khatem">
   ...x3 </p>. The two rules are the ::before / ::after below. */
.khatem-rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
}
.khatem-rule::before,
.khatem-rule::after {
  content: '';
  inline-size: 40px;
  block-size: 1px;
  background: linear-gradient(var(--to-inline-end), var(--gold-600), var(--gold-400));
}

/* ==========================================================================
   0e. ACCESSIBILITY

   On a near-black UI a gold focus ring is genuinely BEAUTIFUL. Here
   accessibility costs nothing — it decorates. It is never removed.

   `outline` rather than `box-shadow`: an outline follows the border radius
   and is never clipped by an `overflow:hidden` ancestor, which is exactly how
   focus rings vanish inside cards and tables.
   ========================================================================== */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}
/* Inside the green rail the ring must be gold in BOTH themes. */
#sidebar :focus-visible { outline-color: var(--gold-300); }

.skip-link {
  position: absolute;
  inset-block-start: -100px;
  inset-inline-start: var(--sp-4);
  z-index: 400;
  padding: var(--sp-3) var(--sp-4);
  background: var(--brand);
  color: var(--text-on-brand);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-weight: 700;
  text-decoration: none;
  transition: inset-block-start var(--t-1) var(--e-out);
}
.skip-link:focus { inset-block-start: 2px; }

.visually-hidden {
  position: absolute !important;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Forced-colors (Windows high contrast): the system's colours win. Do not
   fight it — just make sure nothing disappears. */
@media (forced-colors: active) {
  .card, .btn, .nav-item, input, select, textarea { border: 1px solid CanvasText; }
  .ic { stroke: CanvasText; }
}

/* ==========================================================================
   1. THE RAIL — the rail IS the crest.

   Deep green in BOTH themes (`--rail-bg: #0A2314`). This is the one place
   the brand is unconditional.

   The active item is NOT a filled pill. It is a 3px BAR OF GOLD on the
   reading edge — `border-inline-start`, so it swings to the RIGHT in Arabic,
   for free — plus a radial gradient bleeding inward from that bar.
   We did not colour a button in. We aimed a lamp.
   ========================================================================== */
/* ONE rule, NO [dir] override. `inset-inline-start` is the READING edge: the
   right-hand side in Arabic, the left in French. The rail therefore sits on
   the right in RTL and on the left in LTR without a second declaration — which
   is the whole point of logical properties. (The previous version pinned
   `inset-inline-end` here and then re-pinned the rail to the left in LTR, so
   BOTH directions ended up on the left and the mirror never happened.) */
/* ---- THE RAIL IS A GRID, AND THE RAIL ITSELF NEVER SCROLLS ---------------
   MEASURED, 2026-07-17, before a line was written: 14 entries = 1474px of
   rail in a 900px window. Six items — `طلبات الانتساب` among them — sat below
   the fold, and `overflow: hidden auto` on the RAIL meant the only way to
   reach them was to scroll the crest away first. Nothing on screen said so.
   An administrator does not scroll a thing that does not look scrollable;
   they conclude the feature does not exist.

   Three rows: crest · links · footer.

   The middle row is `minmax(0, max-content)`, NOT `1fr`, and this is the second
   sidebar bug's fix. `1fr` made the nav row swallow ALL the leftover height and
   pinned the © footer to the very bottom of the screen — so when the links did
   not fill the rail (the common case on a tall screen), a dead band of empty
   green opened up between the last entry and the footer. `max-content` sizes the
   nav row to its links and no more, so the footer follows them directly; the
   `min` of 0 lets the row shrink and SCROLL (see `#sidebar nav`, overflow-y:auto)
   when the links genuinely overflow a short screen. `align-content: start` keeps
   the three rows stacked at the top: the space that used to be the dead band is
   now below the footer, filled by `--rail-bg`, where it reads as nothing at all.

   `overflow: hidden` here is deliberate and must stay: the day the rail itself
   scrolls again is the day the original bug comes back. The rail is `position:
   fixed`, so it stays put on page scroll regardless of this grid. */
#sidebar {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;             /* reading edge: right in AR, left in FR/EN */
  inline-size: var(--sidebar-w);
  background: var(--rail-bg);
  color: var(--rail-text);
  z-index: 100;
  display: grid;
  grid-template-rows: auto minmax(0, max-content) auto;   /* crest · nav · footer */
  align-content: start;                     /* leftover height sits BELOW the footer, not as a void above it */
  border-inline-end: 1px solid var(--rail-border);   /* the edge facing the content */
  transition: transform var(--t-2) var(--e-io);
  overflow: hidden;
}

/* The khatem watermark. 3%. Any more and it becomes wallpaper. */
#sidebar::before {
  content: '';
  position: absolute;
  inset-block-start: 42%;
  inset-inline-start: -30px;
  inline-size: 220px;
  block-size: 220px;
  background: var(--gold-400);
  clip-path: var(--khatem-clip);
  opacity: .03;
  pointer-events: none;
}

/* ---- The crest: a 40px ROW ------------------------------------------------
   Was a centred column — 88px medallion, name, tagline — measuring 199px tall
   at 1920×1080 and 141px at 1440×900. With `.rail-account` (126px) that was
   267px of the 900 a laptop has, 43% of the rail, spent on decor, sitting
   directly above the list of links this console exists to navigate. The nav
   was left 617px for 947px of content, and SIX entries fell off the bottom.

   A row: logo at 28px, the club's name on one line beside it. ~40px. The name
   is still here, the gold is still here, and the full name is on the page
   title of every view. Measured: 141px -> 40px at 1440×900. */
.sidebar-crest {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  border-block-end: 1px solid var(--rail-border);
  position: relative;
  min-block-size: 40px;
}

/* The crest does not float — it is LIT. */
.crest {
  inline-size: 28px;
  block-size: 28px;
  border-radius: 50%;
  background: rgba(227, 178, 78, .06);
  border: 1px solid rgba(227, 178, 78, .35);
  flex: none;
  overflow: hidden;
  position: relative;
  display: grid;
  place-items: center;
  color: var(--gold-300);
  filter: drop-shadow(0 0 10px rgba(227, 178, 78, .22));
}
.crest img { inline-size: 100%; block-size: 100%; object-fit: contain; padding: 2px; }
.crest .ic { inline-size: 16px; block-size: 16px; }

/* One line, ellipsis if the club is ever renamed to something long. */
.club-name {
  font-family: var(--ff-display);
  font-size: var(--fs-sm);
  font-weight: 800;
  line-height: 1.3;
  color: var(--rail-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-inline-size: 0;
}

/* This bare `nav` selector is NOT scoped, and it is left exactly as it was on
   purpose. `/index.html` — the public showcase — loads app.css as well as
   landing.css, and its `<nav class="lp-menu">` and `<nav class="lp-foot__links">`
   inherit both declarations below. `.lp-menu` sets no padding of its own and
   sits in a flex header, so `flex: 1` is currently load-bearing for the
   showcase's header layout. Tightening this selector is very probably right,
   but it is a change to the LANDING and belongs to whoever owns that file —
   not to a sidebar fix. Everything the rail needs is scoped to `#sidebar nav`
   below, where it can leak onto nothing. */
nav { flex: 1; padding: var(--sp-3) var(--sp-3) var(--sp-4); }

/* ---- The links: the ONLY thing in the rail that scrolls -------------------
   Scoped to `#sidebar` deliberately. An earlier draft of this rule was a bare
   `nav {}`, which painted dark-green scroll lids across the public showcase's
   header menu and turned it into a scroll container. app.css is not the
   admin's private stylesheet — `/index.html` loads it too.

   `min-block-size: 0` is load-bearing and not decoration. A `1fr` grid row
   refuses to shrink below its content without it, the row grows to its full
   1069px, the rail overflows again and the bug returns EXACTLY as it was. Do
   not remove it because "it looks like it does nothing".

   The shadows are the affordance, and they ARE the fix: an invisible scroll IS
   the bug. Four backgrounds, no JS, no scroll listener (Roman Komarov's
   technique). The two `local` gradients ride the CONTENT and act as lids; the
   two `scroll` radials are pinned to the nav's own box, so a shadow shows at
   an edge precisely when there is something beyond it and melts away at the
   end of the travel. Layer order matters: the lids are declared FIRST because
   the first background layer paints on top, and a lid that sits under its
   shadow hides nothing. Nothing to keep in sync, nothing to throttle, correct
   at every height on every device.

   THE SHADOWS ARE GOLD, NOT BLACK, AND THAT IS THE WHOLE POINT OF THEM. This
   rail is `--rail-bg: #0A2314` — a near-black green. A black shadow on it
   measured 1.12:1 against its own background: the affordance was written,
   shipped, and INVISIBLE, which is the bug it was supposed to fix. Gold at .55
   measures 3.47:1 (>= 3:1, the non-text minimum). If you retint these, MEASURE
   the result against #0A2314; do not eyeball it on a bright screen.

   `scrollbar-gutter: stable` for the same reason: the thumb is an overlay on
   this platform, so it reserves no space and appears only on hover — it cannot
   signal anything at rest. The gutter makes the styled thumb permanent. */
#sidebar nav {
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;     /* the page behind must not scroll along */
  scrollbar-width: thin;
  scrollbar-gutter: stable;
  scrollbar-color: var(--night-400) transparent;
  background:
    linear-gradient(var(--rail-bg) 30%, transparent) local top / 100% 24px no-repeat,
    linear-gradient(transparent, var(--rail-bg) 70%) local bottom / 100% 24px no-repeat,
    radial-gradient(farthest-side at 50% 0, rgba(227, 178, 78, .55), transparent) scroll top / 100% 12px no-repeat,
    radial-gradient(farthest-side at 50% 100%, rgba(227, 178, 78, .55), transparent) scroll bottom / 100% 12px no-repeat;
}
#sidebar nav::-webkit-scrollbar { inline-size: 8px; }
#sidebar nav::-webkit-scrollbar-thumb { background: var(--night-400); border-radius: var(--r-full); }

/* SIX group titles × 40px = 240px — a quarter of the whole rail — spent
   labelling fifteen links that each already carry an icon AND a word. They are
   not deleted (they do group, and the groups are real), they are demoted to
   what a group break actually needs: a hairline and a small word riding on it.
   ~40px -> ~20px each, ~120px back. The rule is drawn with a border on the
   title's own box, so it costs no extra element and no extra height. */
.nav-group-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  color: var(--rail-text-dim);
  line-height: 1;
}
/* The hairline: it fills whatever the word leaves, and mirrors for free. */
.nav-group-title::after {
  content: '';
  flex: 1;
  block-size: 1px;
  background: var(--rail-border);
}
/* The first group sits under the crest's own border — no double rule. */
.nav-group-title:first-child { padding-block-start: var(--sp-1); }
/* Uppercasing is a LATIN operation. Arabic has no case, and `text-transform`
   on Arabic is at best a no-op and at worst a font-shaping hazard. */
:lang(fr) .nav-group-title,
:lang(en) .nav-group-title { text-transform: uppercase; }
:lang(ar) .nav-group-title { word-spacing: .25em; }

/* Real <a> elements: keyboard-reachable, right-clickable, announced as links.
   Block padding is `--sp-2` (8px), not `--sp-3`: with 15 items, 4px per side
   costs 120px of rail — more than two whole entries below the fold. The row is
   still ~36px tall, comfortably above the 24px target minimum, and the inline
   padding is untouched. */
.nav-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  margin-block-end: 2px;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  border-inline-start: 3px solid transparent;
  color: var(--rail-text-dim);
  text-decoration: none;
  font-size: var(--fs-sm);
  font-weight: 600;
  position: relative;
}
.nav-item:hover {
  background: var(--rail-hover);
  color: var(--rail-text);
}
.nav-item[aria-current='page'] {
  border-inline-start-color: var(--gold-400);
  color: var(--rail-active-text);
  font-weight: 700;
  /* The light bleeding inward from the bar. */
  background:
    radial-gradient(120% 140% at var(--nav-glow-x, 100%) 50%,
                    rgba(227, 178, 78, .16) 0%,
                    rgba(227, 178, 78, .04) 45%,
                    transparent 75%),
    var(--rail-active-bg);
}
html[dir='ltr'] .nav-item { border-radius: var(--r-sm) 0 0 var(--r-sm); }
html[dir='ltr'] .nav-item[aria-current='page'] { --nav-glow-x: 0%; }

.nav-item .nav-icon {
  display: inline-flex;
  inline-size: var(--ic);
  block-size: var(--ic);
  flex: none;
  color: currentColor;   /* the icon takes the gold with the label */
}
.nav-item .nav-icon .ic { inline-size: 100%; block-size: 100%; }

.badge-count {
  margin-inline-start: auto;
  background: var(--gold-400);
  color: var(--ink-on-gold);
  font-size: var(--fs-xs);
  font-weight: 700;
  min-inline-size: 22px;
  padding: 1px 7px;
  border-radius: var(--r-full);
  text-align: center;
}

/* ---- Who you are, and the way out — NOW IN THE TOP BAR -------------------
   This block spent 126px at the foot of the rail, and on every laptop the club
   owns that foot was below the fold: the "always visible" way out was the one
   thing you had to scroll a hidden scroller to find. It is a top-bar menu now,
   which is where every console puts it and where it is visible on every view.

   A native `<details>`: open/close, keyboard and screen-reader semantics all
   come from the element. app.js adds only Escape and click-outside. */
.user-menu {
  position: relative;
}
.user-menu__btn {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
  list-style: none;                 /* kill the disclosure triangle … */
  min-block-size: 38px;
  transition: background var(--t-1) var(--e-ui), border-color var(--t-1) var(--e-ui);
}
.user-menu__btn::-webkit-details-marker { display: none; }   /* … and in Safari */
.user-menu__btn:hover { background: var(--surface-hover); border-color: var(--border-strong); }
.user-menu__name {
  max-inline-size: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The caret, drawn — there is no chevron glyph in the sprite. A rotated
   square's two borders: no icon dependency, no extra request. */
.user-menu__btn::after {
  content: '';
  inline-size: 6px;
  block-size: 6px;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--t-1) var(--e-ui);
  flex: none;
}
.user-menu[open] .user-menu__btn::after { transform: translateY(1px) rotate(225deg); }

.user-menu__pop {
  position: absolute;
  inset-block-start: calc(100% + 6px);
  inset-inline-end: 0;              /* mirrors for free in RTL */
  min-inline-size: 200px;
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md, 0 8px 24px rgba(0,0,0,.18));
  z-index: 60;                      /* above the topbar's own z-50 */
}
.user-menu__role {
  font-size: var(--fs-xs);
  color: var(--text-subtle);
  margin-block-end: var(--sp-3);
}
/* Ghost by default; on hover the ink and the border go to `--danger`, because
   signing out is destructive and the interface should say so BEFORE the click.
   Retinted from the `--rail-*` tokens to the page tokens when this moved out of
   the dark rail: `--rail-text-dim` on `--surface` measured as good as invisible. */
.rail-logout {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  inline-size: 100%;
  padding: var(--sp-2) var(--sp-3);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
  text-align: start;
  transition: color var(--t-1) var(--e-ui), border-color var(--t-1) var(--e-ui), background var(--t-1) var(--e-ui);
}
.rail-logout:hover {
  color: var(--danger);
  border-color: var(--danger);
  background: rgba(179, 38, 30, .10);
}

.sidebar-footer {
  padding: var(--sp-4);
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--rail-text-dim);
  border-block-start: 1px solid var(--rail-border);
}

/* ---- SHORT SCREENS: the last few pixels ----------------------------------
   THE PREVIOUS COMMENT HERE WAS FALSE, and it is worth saying why rather than
   just deleting it. It claimed "at 1440×900 and 1280×800 all fourteen entries
   fit with zero travel, measured, not assumed". Measured again with fifteen
   entries: SEVEN were out of the nav's box at 1440×900, NINE at 1366×768, and
   SIX at 1920×1080 — where this media query does not even apply. The comment
   was true for a rail that no longer existed by the time it was read, and a
   comment that lies is worse than no comment: it makes a live bug look fixed
   and gets it closed twice.

   The cause was never the padding this block trims. It was structural: 267px
   of a 900px rail — `.sidebar-crest` 141px + `.rail-account` 126px — went to
   decor, leaving the nav 617px for 947px of links. Every page added pushed one
   more entry out. That is fixed ABOVE now (crest is a 40px row, the account
   block moved to the top bar), and it is fixed at every height, not just under
   960px.

   What is left here is genuinely a short-screen trim: the row goes 44px -> 34px
   under 960px of height. WCAG 2.5.8 sets the target minimum at 24px; 34px is
   still well above it, and these are mouse-and-trackpad rows on a laptop, not
   thumb targets (the mobile drawer keeps the full 44px — see the max-width
   block). DO NOT take this below 32px.

   32px, the floor itself, since the "Catégories" work: the rail grew from 17
   links to 18 (categories under اللاعبات, beside players). At 34px the eighteenth
   entry — إعدادات النادي — fell ~20px out of the nav's box at 1366×768, the very
   failure the layout test guards. 32px buys it back with ~16px to spare and is
   exactly the floor this comment has always promised — the layout test asserts
   `rowHeight >= 32`, so this is the last row of headroom the density has: a
   nineteenth entry cannot be paid for from here, it must come from structure
   (a group merged, an entry retired) as the crest and account block once were. */
@media (max-height: 960px) {
  /* `#sidebar nav`, never a bare `nav`: the showcase loads this file too. */
  #sidebar nav { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  /* 1px, not 2px: six group titles pay for this six times over, and the rule
     they now carry is what reads as the break — not the air around the word. */
  .nav-group-title { padding: 1px var(--sp-3); }
  /* `min-block-size`, NOT a padding sum. Padding + whatever line box the font
     happens to produce is how this drifted to 31px — under the 32px floor —
     once, while the comment above it lied and claimed 36px. The row is 32px
     because it is told to be 32px, in one number, on the property that decides
     it. `margin-block-end: 1px` (2px on desktop) shaves another row of air
     across eighteen entries. WCAG 2.5.8 asks 24px; this is 32px, the floor.
     DO NOT go below 32px here — the layout test fails at 31px, by design. */
  .nav-item { padding-block: 0; padding-inline: var(--sp-4); min-block-size: 32px; margin-block-end: 1px; }
  .nav-item .nav-icon { inline-size: 18px; block-size: 18px; }

  /* A © line that repeats the club name printed two centimetres above it, in
     the one place where 50px is the difference between reaching a link and
     not. It returns in full above 960px. */
  .sidebar-footer { display: none; }
}

/* ==========================================================================
   2. MAIN & TOPBAR
   ========================================================================== */
/* The content is pushed off the rail's edge — the same logical edge the rail is
   pinned to, so this mirrors for free too. No [dir] override. */
#main {
  margin-inline-start: var(--sidebar-w);
  min-block-size: 100vh;
}

/* NO `backdrop-filter` here. Only two surfaces on the whole site are blurred
   (the hero's fact band and the lightbox backdrop) — never a sticky element
   that a long table scrolls beneath. That is the single most expensive thing
   you can do to a mid-range Android. */
.topbar {
  position: sticky;
  /* `0`, not `2px`. At 2px the bar stuck two pixels down and left a permanent
     slot the content scrolled through, on every page. It was copied from
     `.skip-link:focus` (l.207), where 2px is deliberate. */
  inset-block-start: 0;
  z-index: 50;
  block-size: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding-inline: var(--sp-8);
  background: var(--bg);
  border-block-end: 1px solid var(--border);
}
.topbar-title {
  font-size: var(--fs-xl);
  font-weight: 900;
  letter-spacing: var(--track-tight);
}
.topbar-right { margin-inline-start: auto; display: flex; align-items: center; gap: var(--sp-3); }
.topbar-date { font-size: var(--fs-sm); color: var(--text-subtle); }

.hamburger {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text);
  padding: var(--sp-2);
  line-height: 0;
}
.hamburger .ic { inline-size: 24px; block-size: 24px; }

.content { padding: var(--sp-8); max-inline-size: var(--maxw); }

/* ---- Save-state pill ---------------------------------------------------- */
#saveState {
  font-size: var(--fs-xs);
  font-weight: 700;
  padding: 4px 10px;
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  color: var(--text-subtle);
}
#saveState[data-state='saving']  { color: var(--text-subtle); }
#saveState[data-state='saved']   { color: var(--success); border-color: color-mix(in srgb, var(--success) 45%, transparent); }
#saveState[data-state='offline'] { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 45%, transparent); }
#saveState[data-state='error']   { color: var(--danger);  border-color: color-mix(in srgb, var(--danger) 45%, transparent); }

/* ---- Language & theme switches ------------------------------------------ */
.switch-group {
  display: flex;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  padding: 2px;
}
.switch-btn {
  border: none;
  background: none;
  cursor: pointer;
  padding: 5px 11px;
  border-radius: var(--r-full);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--text-subtle);
  transition: background var(--t-1) var(--e-ui), color var(--t-1) var(--e-ui);
}
.switch-btn:hover { color: var(--text); }
.switch-btn[aria-pressed='true'] { background: var(--brand); color: var(--text-on-brand); }

.icon-btn {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  inline-size: 38px;
  block-size: 38px;
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background var(--t-1) var(--e-ui), color var(--t-1) var(--e-ui), border-color var(--t-1) var(--e-ui);
}
.icon-btn:hover { background: var(--surface-hover); color: var(--text); border-color: var(--border-strong); }
.icon-btn .ic { inline-size: 20px; block-size: 20px; }

/* ==========================================================================
   3. PRIMITIVES
   ========================================================================== */

/* THE GOLD EDGE. One declaration, and every panel in the admin is tied back
   to the projector in the hero. Radius drops to 8px: an institution does not
   have 18px corners. */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start-color: var(--edge-gold);
  border-radius: var(--r-md);
  padding: var(--sp-6);
  box-shadow: none;
  margin-block-end: var(--sp-6);
}
.card:hover { box-shadow: var(--shadow-sm); }

/* The gold top edge means "this is the club". A card that can erase the club
   does not get to wear it. Used once: the backup-restore block in settings. */
.card--danger {
  border-color: color-mix(in srgb, var(--danger) 35%, var(--border));
  border-block-start-color: var(--danger);
}

.card-header {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-block-end: var(--sp-5);
}
.card-title { font-size: var(--fs-lg); font-weight: 700; }
.card-header > .spacer { margin-inline-start: auto; }

/* ---- Buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 11px 20px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  font: inherit;
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.2;
  cursor: pointer;
  /* Buttons are `<a>` on the showcase. Without this the UA underline paints
     straight through the label — including inside the filled gold CTA, which
     is the single most visible element on the site. `.nav-item` remembered
     this (l.410); `.btn` did not. */
  text-decoration: none;
}
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn .ic { inline-size: 1.15em; block-size: 1.15em; }

/* Light: deep green, white ink (12.81:1). Dark: gold, green-black ink
   (8.39:1). Both directions come free from `--brand` / `--text-on-brand`. */
.btn--primary {
  background: var(--brand);
  color: var(--text-on-brand);
  box-shadow: var(--shadow-brand);
}
.btn--primary:hover:not(:disabled) { background: var(--brand-hover); }

.btn--outline {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border-strong);
}
.btn--outline:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--brand); }

.btn--danger { background: var(--danger); color: var(--surface); }
.btn--danger:hover:not(:disabled) { filter: brightness(1.08); }

/* GOLD BUTTON: light gold, green-black ink. NEVER THE INVERSE. Gold type on a
   dark ground is 1.84:1 and unreadable; this is the whole reason the gold
   ramp exists. */
.btn--gold {
  background: var(--gold-400);
  color: var(--ink-on-gold);
  font-weight: 700;
}
.btn--gold:hover:not(:disabled) {
  box-shadow: 0 0 0 1px var(--gold-300), 0 12px 40px -8px rgba(227, 178, 78, .55);
}

/* The rim button: transparent, gold edge. For secondary actions on dark. */
.btn--rim {
  background: transparent;
  border-color: var(--gold-600);
  color: var(--accent-ink);
}
.btn--rim:hover:not(:disabled) { border-color: var(--gold-400); background: var(--brand-wash); }

.btn--ghost { background: transparent; color: var(--text-subtle); }
.btn--ghost:hover:not(:disabled) { background: var(--surface-hover); color: var(--text); }

.btn--sm { padding: 7px 12px; font-size: var(--fs-xs); }
.btn--block { inline-size: 100%; }

/* ---- Badges ------------------------------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 700;
  border: 1px solid var(--border);
  background: var(--surface-sunken);
  color: var(--text-muted);
}
.badge .ic { inline-size: 1em; block-size: 1em; }
.badge--success { color: var(--success);    border-color: color-mix(in srgb, var(--success) 40%, transparent); background: color-mix(in srgb, var(--success) 12%, transparent); }
.badge--danger  { color: var(--danger);     border-color: color-mix(in srgb, var(--danger)  40%, transparent); background: color-mix(in srgb, var(--danger)  12%, transparent); }
.badge--warning { color: var(--accent-ink); border-color: color-mix(in srgb, var(--warning) 45%, transparent); background: color-mix(in srgb, var(--warning) 12%, transparent); }
.badge--info    { color: var(--info);       border-color: color-mix(in srgb, var(--info)    40%, transparent); background: color-mix(in srgb, var(--info)    12%, transparent); }
.badge--brand   { color: var(--brand-ink);  border-color: color-mix(in srgb, var(--brand)   40%, transparent); background: var(--brand-wash); }

/* ---- Forms -------------------------------------------------------------- */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-4); }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-group.full { grid-column: 1 / -1; }

label { font-size: var(--fs-sm); font-weight: 700; color: var(--text-muted); }
.req { color: var(--danger); margin-inline-start: 2px; }

input, select, textarea {
  font: inherit;
  font-size: var(--fs-sm);
  padding: 11px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  inline-size: 100%;
  transition: border-color var(--t-1) var(--e-ui), box-shadow var(--t-1) var(--e-ui);
}
input:hover, select:hover, textarea:hover { border-color: var(--brand); }
input:focus, select:focus, textarea:focus { border-color: var(--brand); }
input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-sunken);
  color: var(--text-subtle);
  border-color: var(--border);
  cursor: not-allowed;
}
textarea { min-block-size: 104px; resize: vertical; line-height: var(--lh-body); }
input[type='checkbox'] { inline-size: 18px; block-size: 18px; accent-color: var(--brand); }
input[type='date'], input[type='time'], input[type='number'] {
  font-variant-numeric: tabular-nums;
}
select {
  /* No emoji arrow, no background-image: the caret is drawn from a token. */
  appearance: none;
  padding-inline-end: var(--sp-8);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: calc(100% - 18px) calc(50% + 1px), calc(100% - 13px) calc(50% + 1px);
  background-repeat: no-repeat;
}
html[dir='rtl'] select {
  background-position: 13px calc(50% + 1px), 18px calc(50% + 1px);
}

.field-error { color: var(--danger); font-size: var(--fs-xs); font-weight: 700; }
input[aria-invalid='true'], select[aria-invalid='true'], textarea[aria-invalid='true'] { border-color: var(--danger); }

.checkbox-row { display: flex; align-items: flex-start; gap: var(--sp-3); }
.checkbox-row label { font-weight: 500; color: var(--text-muted); font-size: var(--fs-sm); }
.checkbox-row input[type='checkbox'] { margin-block-start: 3px; }

.toolbar {
  display: flex;
  gap: var(--sp-3);
  flex-wrap: wrap;
  align-items: center;
  margin-block-end: var(--sp-4);
}
.toolbar input[type='search'], .toolbar select, .toolbar input[type='date'] {
  inline-size: auto;
  min-inline-size: 160px;
}

/* ---- Empty & loading ----------------------------------------------------
   The khatem at 6% behind a single line of `--text-subtle`. Never an emoji,
   never a cute illustration. This club has won a league. */
.empty-state {
  grid-column: 1 / -1;
  position: relative;
  text-align: center;
  padding: var(--sp-10) var(--sp-4);
  color: var(--text-subtle);
  font-size: var(--fs-md);
  isolation: isolate;
}
.empty-state::before {
  content: '';
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  inline-size: 96px;
  block-size: 96px;
  background: currentColor;
  clip-path: var(--khatem-clip);
  opacity: .06;
  z-index: -1;
}
.empty-state__icon {
  display: block;
  margin-inline: auto;
  margin-block-end: var(--sp-3);
  color: var(--text-subtle);
  opacity: .85;
}
.empty-state__icon .ic { inline-size: 34px; block-size: 34px; stroke-width: 1.25; }

/* The way out of the empty state. Nine of the fifteen views are empty on a
   brand-new club, so this button is a large part of what the console IS on the
   first day — the text said "start by adding one!" and then offered nothing to
   press. */
.empty-state__cta { margin-block-start: var(--sp-5); }

.skeleton { display: flex; flex-direction: column; gap: var(--sp-3); }
.skeleton__row {
  block-size: 56px;
  border-radius: var(--r-md);
  background: linear-gradient(var(--to-inline-end),
              var(--surface-sunken) 25%, var(--surface-hover) 37%, var(--surface-sunken) 63%);
  background-size: 400% 100%;
}

/* ==========================================================================
   4. DASHBOARD
   ========================================================================== */

/* THE ONE PHOTOGRAPH IN THE WHOLE ADMINISTRATION. It is enough to bind the
   two halves of the product together. The veil is on the reading edge, so the
   text always lands on the dark side of it — in Arabic and in French alike. */
.welcome-banner {
  position: relative;
  overflow: hidden;
  min-block-size: 180px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  border-block-start-color: var(--edge-gold);
  padding: var(--sp-8) var(--sp-6);
  margin-block-end: var(--sp-5);
  color: var(--on-grad-brand);
  background-color: var(--night-100);
  background-image:
    linear-gradient(var(--to-inline-end),
                    rgba(6, 26, 15, .96) 0%,
                    rgba(6, 26, 15, .82) 42%,
                    rgba(6, 26, 15, .35) 100%),
    url('/assets/photos/club-16.jpg');
  background-size: cover, cover;
  background-position: center, 50% 40%;   /* no face, no foot, no ball is cut */
  background-repeat: no-repeat;
}
.welcome-title {
  position: relative;
  font-size: var(--fs-2xl);
  font-weight: 900;
  letter-spacing: var(--track-tight);
}
.welcome-sub {
  position: relative;
  color: rgba(242, 245, 238, .82);
  margin-block-start: var(--sp-2);
  font-size: var(--fs-md);
}

/* ---- Stat cards ---------------------------------------------------------
   THE FIGURE IS THE HERO. Cairo 900, `--fs-d1`, tabular, and it must be
   wrapped in `<bdi class="num">` by the view. The label sits ABOVE it, small
   and quiet — you read the number first and the caption second, which is the
   order in which a coach actually needs them.

   DOM order from views/dashboard.js is icon → num → label. `order` puts the
   label back above the figure without the view having to change. */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--sp-4);
  margin-block-end: var(--sp-5);
}
.stat-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start-color: var(--edge-gold);
  border-radius: var(--r-md);
  padding: var(--sp-5) var(--sp-5) var(--sp-6);
  overflow: hidden;
  text-align: start;
}
/* The 2px rule of gold that DRAWS ITSELF. `transform-origin: inline-start`
   means it grows from the RIGHT in Arabic. For free. (motion.css) */
.stat-card::after {
  content: '';
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: 2px;
  background: var(--grad-gold);
}
.stat-icon {
  order: 1;
  color: var(--accent-ink);
  margin-block-end: var(--sp-2);
  line-height: 0;
}
.stat-icon .ic { inline-size: 22px; block-size: 22px; }
.stat-label {
  order: 2;
  font-size: var(--fs-xs);
  color: var(--text-subtle);
  font-weight: 700;
  letter-spacing: var(--track-wide);
}
:lang(fr) .stat-label, :lang(en) .stat-label { text-transform: uppercase; }
.stat-num {
  order: 3;
  font-family: var(--ff-display);
  font-size: var(--fs-d1);
  font-weight: 900;
  color: var(--brand-ink);
  line-height: 1.05;
  letter-spacing: var(--track-tight);
  font-variant-numeric: tabular-nums;
}

.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--sp-5); }

.progress-bar {
  block-size: 8px;
  background: var(--surface-sunken);
  border-radius: var(--r-full);
  overflow: hidden;
}
.progress-fill {
  block-size: 100%;
  background: var(--chart-1);
  border-radius: var(--r-full);
  transition: inline-size var(--t-2) var(--e-out);
}

.bar-list { display: flex; flex-direction: column; gap: var(--sp-1); }

.legend-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  padding-block: 6px;
}
.legend-row strong { font-variant-numeric: tabular-nums; }
.color-dot {
  display: inline-block;
  inline-size: 9px;
  block-size: 9px;
  border-radius: 50%;
  margin-inline-end: 6px;
}

.activity-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-block-end: 1px solid var(--border);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.activity-item:last-child { border-block-end: none; }
.activity-item .ic { color: var(--text-subtle); }

/* ---- Charts (core/chart.js — inline SVG, no library) --------------------
   TWO SERIES, AND NOTHING ELSE. Series 1 is the GOLD — the light bleeding
   down onto the pitch. Series 2 is the green. A palette dies the instant you
   bolt a five-colour categorical set onto it; when one is truly unavoidable,
   use --cat-1..5, which is a single light source at five distances. */
.chart { inline-size: 100%; block-size: 180px; overflow: visible; }
.chart__grid  { stroke: var(--chart-grid); stroke-width: 1; }
.chart__area  { fill: var(--chart-area); }
.chart__line  { fill: none; stroke: var(--chart-1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart__dot   { fill: var(--chart-1); }
.chart__label { font-size: 10px; fill: var(--chart-axis); font-variant-numeric: tabular-nums; }
.chart-bar    { fill: var(--chart-1); }
.chart-bar--muted { fill: var(--chart-2); }

/* Second series, when a view draws one. */
.chart__line--2 { stroke: var(--chart-2); }
.chart__dot--2  { fill: var(--chart-2); }

/* ==========================================================================
   5. PLAYERS
   ========================================================================== */
.players-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--sp-4); }

.player-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start-color: var(--edge-gold);
  border-radius: var(--r-md);
  padding: var(--sp-5) var(--sp-4) var(--sp-4);
  text-align: center;
  position: relative;
  box-shadow: none;
}
/* The same verb as the gallery tile: RISE INTO THE LIGHT. */
.player-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--border-strong);
  border-block-start-color: var(--gold-400);
}

.player-num {
  position: absolute;
  inset-block-start: var(--sp-3);
  inset-inline-start: var(--sp-3);
  background: var(--grad-brand);
  color: var(--on-grad-brand);
  inline-size: 32px;
  block-size: 32px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: var(--fs-xs);
  font-weight: 800;
  border: 1px solid var(--edge-gold);
}
.player-avatar, .player-avatar--empty {
  inline-size: 76px;
  block-size: 76px;
  border-radius: 50%;
  margin: 0 auto var(--sp-3);
  object-fit: cover;
  border: 2px solid var(--border);
  background: var(--surface-sunken);
  display: grid;
  place-items: center;
  font-family: var(--ff-display);
  font-size: 1.7rem;
  font-weight: 800;
  color: var(--text-subtle);
}
.player-name { font-weight: 700; font-size: var(--fs-md); }
.player-pos { font-size: var(--fs-sm); color: var(--text-subtle); margin-block: 4px var(--sp-2); }
.player-actions { display: flex; gap: 6px; justify-content: center; margin-block-start: var(--sp-3); flex-wrap: wrap; }

/* ==========================================================================
   6. ATTENDANCE

   The statuses KEEP THEIR SEMANTIC COLOURS. A dashboard that repaints its
   statuses in the club's colours is a dashboard nobody can read.
   ========================================================================== */
.attendance-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--sp-3); }
.attendance-card {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--border-strong);
  border-radius: var(--r-md);
  padding: var(--sp-3);
}
.attendance-card:hover { background: var(--bg-tint); }
.attendance-card[data-status='present'] { border-inline-start-color: var(--status-present); }
.attendance-card[data-status='absent']  { border-inline-start-color: var(--status-absent); }
.attendance-card[data-status='sick']    { border-inline-start-color: var(--status-sick); }
.attendance-card[data-status='late']    { border-inline-start-color: var(--status-late); }
.attendance-card[data-status='excused'] { border-inline-start-color: var(--status-excused); }

.att-avatar {
  inline-size: 42px;
  block-size: 42px;
  border-radius: 50%;
  background: var(--surface-sunken);
  display: grid;
  place-items: center;
  font-weight: 800;
  color: var(--text-subtle);
  overflow: hidden;
  flex: none;
}
.att-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.att-name { font-weight: 700; font-size: var(--fs-md); }
.att-btns { display: flex; gap: 4px; margin-block-start: 6px; flex-wrap: wrap; }
.att-btn {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  border-radius: var(--r-sm);
  padding: 4px 8px;
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-1) var(--e-ui), color var(--t-1) var(--e-ui);
}
.att-btn:hover { background: var(--surface-hover); }
.att-btn[aria-pressed='true'] { border-color: transparent; font-weight: 700; }
.att-btn[aria-pressed='true'][data-status='present'] { background: var(--status-present); color: var(--ink-on-gold); }
.att-btn[aria-pressed='true'][data-status='absent']  { background: var(--status-absent);  color: var(--night-000); }
.att-btn[aria-pressed='true'][data-status='sick']    { background: var(--status-sick);    color: var(--night-000); }
.att-btn[aria-pressed='true'][data-status='late']    { background: var(--status-late);    color: var(--night-000); }
.att-btn[aria-pressed='true'][data-status='excused'] { background: var(--status-excused); color: var(--night-000); }
/* In the light theme these five are dark inks, so the label goes light. */
:root[data-theme='light'] .att-btn[aria-pressed='true'] { color: #FFFFFF; }

/* ==========================================================================
   7. TABLES

   NO ZEBRA STRIPES. On near-black they turn to mud; on paper they are vulgar.
   Separation is a 1px hairline. On hover the row RISES INTO THE LIGHT —
   exactly the same gesture as a gallery tile. The system has one verb.
   ========================================================================== */
/* `overflow-x: auto` forces the used value of `overflow-y` to `auto` too — the
   two are not independent in CSS. That silently made THIS box the scroll
   container the sticky `thead` resolves against, instead of the page. And with
   no `max-block-size` the box never scrolls: it just grows. So the headers were
   stuck to a container that never moves, the page scrolled underneath them, and
   they left with it — measured at `top: -503px` after an 800px scroll. Sticky
   headers that have never once stuck.

   Giving the box a real height makes it a real scroller and the stickiness
   resolves against something that actually travels. The subtraction keeps the
   table clear of the top bar and leaves room for the card's own chrome. */
.table-wrap {
  overflow-x: auto;
  max-block-size: calc(100dvh - var(--topbar-h) - 8rem);
  border: 1px solid var(--border);
  border-block-start-color: var(--edge-gold);
  border-radius: var(--r-md);
  background: var(--surface);
}
table { inline-size: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
th, td {
  padding: 13px 16px;
  text-align: start;
  border-block-end: 1px solid var(--border);
  white-space: nowrap;
}
thead th {
  background: var(--surface-sunken);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--track-wide);
  color: var(--text-subtle);
  position: sticky;
  inset-block-start: 0;
  /* 2, not 1: a `td` with its own positioned content (the booking rectangles,
     the row actions) would otherwise paint over the header it scrolls under. */
  z-index: 2;
}
:lang(fr) thead th, :lang(en) thead th { text-transform: uppercase; }
tbody tr { transition: background var(--t-1) var(--e-ui); }
tbody tr:hover { background: var(--bg-tint); }
tbody tr:last-child td { border-block-end: none; }
td.num { text-align: end; font-variant-numeric: tabular-nums; }
/* A numeric HEADER must sit over its numeric column. `th`/`td` both default to
   `text-align: start`, but `td.num` flips the CELLS to `end` — so a header with
   no class stayed left while its figures went right, and every count column read
   as offset by one (the "Assiduité par joueuse" table, ratings, stats). The view
   marks its numeric `<th>` with `class="num"`; this aligns them to match. One
   rule repairs every numeric table at once. */
th.num { text-align: end; }

/* A BOOKING. A yellow card is a SOLID RECTANGLE, not a pictogram — the sprite
   would be wrong here and the emoji was tofu. It is drawn, so it costs nothing
   and cannot fail to render. The real column name rides alongside in
   `.visually-hidden` text, so a screen reader announces "Yellows", not silence.
   Statuses keep their semantic colours: a dashboard that repaints its cards in
   the club's colours is a dashboard nobody can read. */
/* Le petit rectangle de couleur qui sert d'en-tete de colonne dans le tableau
   des statistiques. Il s'appelait `.card-chip` — le MEME nom que la pastille de
   la page Cartons, definie plus bas dans ce fichier. La seconde definition,
   venant apres, ecrasait celle-ci : le rectangle plein de 11x15 px devenait une
   gelule bordee et arrondie, dans un en-tete de colonne ou il n'y a pas de
   texte. Deux objets differents ne peuvent pas porter le meme nom de classe.
   Celui-ci est une PASTILLE DE LEGENDE, pas une puce : le nom le dit desormais. */
.card-swatch {
  display: inline-block;
  inline-size: 11px;
  block-size: 15px;
  border-radius: var(--r-xs);
  vertical-align: -2px;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .25);
}
.card-swatch--yellow { background: var(--booking-yellow); }
.card-swatch--red { background: var(--booking-red); }

/* The sort caret is DRAWN, not typed: no glyph, no font dependency, no
   accidental emoji. */
th[aria-sort] { cursor: pointer; user-select: none; }
th[aria-sort]::after {
  content: '';
  display: inline-block;
  inline-size: 8px;
  block-size: 5px;
  margin-inline-start: 6px;
  background: currentColor;
  opacity: 0;
}
th[aria-sort='ascending']::after {
  opacity: 1;
  color: var(--accent-ink);
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
th[aria-sort='descending']::after {
  opacity: 1;
  color: var(--accent-ink);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}

/* ==========================================================================
   8. SESSIONS / SCHEDULE
   ========================================================================== */
.schedule-item {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--brand);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  margin-block-end: var(--sp-3);
}
.schedule-item:hover { background: var(--bg-tint); }
.schedule-time {
  font-family: var(--ff-display);
  font-weight: 800;
  color: var(--brand-ink);
  font-variant-numeric: tabular-nums;
  flex: none;
}
.schedule-info { flex: 1; min-inline-size: 0; }
.schedule-name { font-weight: 700; }
.schedule-meta { font-size: var(--fs-sm); color: var(--text-subtle); }

/* ==========================================================================
   9. ANNOUNCEMENTS
   ========================================================================== */
.announcement-item {
  background: var(--grad-brand);
  color: var(--on-grad-brand);
  border: 1px solid var(--night-400);
  border-block-start-color: var(--edge-gold);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  margin-block-end: var(--sp-3);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.announcement-title { font-weight: 800; font-family: var(--ff-display); }
.announcement-date {
  font-size: var(--fs-xs);
  color: rgba(242, 245, 238, .70);
  margin-block-start: 6px;
}
/* La vignette n'est même pas émise quand l'annonce n'a pas de photo (voir
   views/training.js) : un carré vide réservé « au cas où » décalait le titre de
   toutes les annonces sans photo, alors qu'elles sont la majorité.
   `flex: none` + taille figée sont obligatoires ici : l'article est un flex
   dont les enfants s'étirent par défaut, donc une photo portrait 3/4 tirait la
   carte sur toute sa hauteur. `object-fit: cover` recadre au lieu d'écraser les
   visages. Rayon `--r-xs` comme la galerie : une photographie n'est pas un
   bouton. Le fond sombre évite le flash blanc entre le layout et le décodage. */
.announcement-thumb {
  flex: none;
  inline-size: 72px;
  block-size: 72px;
  object-fit: cover;
  border-radius: var(--r-xs);
  border: 1px solid var(--edge-gold);
  background: rgba(0, 0, 0, .18);
}
/* Le bloc texte mange la place restante. `min-inline-size: 0` parce qu'un mot
   insécable dans le corps (une URL collée) refusait de se réduire sous sa
   largeur naturelle et poussait les boutons d'action hors de la carte ;
   la base `240px` le renvoie sous la vignette sur mobile plutôt que de le
   réduire à une colonne de deux caractères. */
.announcement-main { flex: 1 1 240px; min-inline-size: 0; }

/* ==========================================================================
   10. GALLERY (admin) — the showcase gallery lives in home.css.

   The tile is IN SHADOW and lights up when looked at. Same gesture as the
   showcase, dialled down: this is a management grid, not a museum wall.
   ========================================================================== */
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--sp-3); }
.gallery-card {
  position: relative;
  border-radius: var(--r-xs);   /* photographs get 2px. They are not buttons. */
  overflow: hidden;
  background: var(--surface-sunken);
  aspect-ratio: 4 / 3;
  border: 1px solid var(--border);
  margin: 0;
}
/* The image is its own button; the delete button is a sibling, not a child. */
.gallery-open {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
}
.gallery-card img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--t-3) var(--e-out), filter var(--t-3) var(--e-out);
}
/* WE NEVER TOUCH THE PHOTOGRAPHS. No hue shift, no colour filter, no
   geometric mask over a face. Brightness only, and only downward at rest.
   These are real teenagers. */
.gallery-card:hover img { transform: scale(1.03); }
.gallery-card:hover { box-shadow: 0 0 0 1px var(--gold-400), var(--shadow-md); }

.gallery-caption {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  background: linear-gradient(transparent, rgba(4, 18, 11, .82));
  color: #F2F5EE;
  padding: var(--sp-3);
  font-size: var(--fs-xs);
  text-align: start;
  pointer-events: none;   /* must not intercept the image button */
}
.gallery-delete {
  position: absolute;
  inset-block-start: 6px;
  inset-inline-end: 6px;
  z-index: 1;
  background: rgba(4, 18, 11, .72);
  color: #F2F5EE;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  padding: 4px 7px;
  cursor: pointer;
  line-height: 0;
  opacity: 0;
  transition: opacity var(--t-1) var(--e-ui), background var(--t-1) var(--e-ui);
}
.gallery-delete:hover { background: var(--danger); }
/* Always visible to keyboard users, who cannot hover. */
.gallery-card:hover .gallery-delete,
.gallery-delete:focus-visible { opacity: 1; }
@media (hover: none) { .gallery-delete { opacity: 1; } }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 250;
  background: rgba(4, 18, 11, .96);
  display: none;
  place-items: center;
  padding: var(--sp-6);
}
.lightbox[open], .lightbox.is-open { display: grid; }
.lightbox img { max-inline-size: 92vw; max-block-size: 82vh; border-radius: var(--r-xs); }
.lightbox-caption { color: #F2F5EE; text-align: center; margin-block-start: var(--sp-4); }
.lightbox-close {
  position: absolute;
  inset-block-start: var(--sp-5);
  inset-inline-end: var(--sp-5);
  background: rgba(255, 255, 255, .10);
  color: #F2F5EE;
  border: 1px solid rgba(227, 178, 78, .35);
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  cursor: pointer;
}

/* ==========================================================================
   11. TROPHIES
   ========================================================================== */
.trophies-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--sp-4); }
.trophy-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start-color: var(--edge-gold);
  border-radius: var(--r-md);
  overflow: hidden;
  box-shadow: none;
}
.trophy-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }

.trophy-banner {
  block-size: 110px;
  display: grid;
  place-items: center;
  position: relative;
  color: var(--night-000);
}
.trophy-banner .ic { inline-size: 42px; block-size: 42px; stroke-width: 1.25; }
.trophy-banner[data-rank='gold']   { background: var(--grad-gold); }
.trophy-banner[data-rank='silver'] { background: linear-gradient(135deg, #6E7F71, #B9C6B7); }
.trophy-banner[data-rank='bronze'] { background: linear-gradient(135deg, #8A5A00, #D19A5B); }
.trophy-banner[data-rank='other']  { background: var(--grad-brand); color: var(--on-grad-brand); }
.trophy-banner img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: .55;
}
.trophy-banner span, .trophy-banner svg { position: relative; }
.trophy-rank {
  text-align: center;
  font-size: var(--fs-xs);
  font-weight: 700;
  padding: 6px;
  background: var(--surface-sunken);
  color: var(--text-muted);
  letter-spacing: var(--track-wide);
}
.trophy-body { padding: var(--sp-4); }
.trophy-title { font-family: var(--ff-display); font-weight: 800; }
.trophy-year { font-size: var(--fs-xs); color: var(--accent-ink); font-weight: 700; }
.trophy-desc { font-size: var(--fs-sm); color: var(--text-subtle); margin-block-start: var(--sp-2); }
.trophy-actions { display: flex; gap: 6px; margin-block-start: var(--sp-3); flex-wrap: wrap; }

/* ==========================================================================
   12. REGISTRATIONS
   ========================================================================== */
.submission-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  margin-block-end: var(--sp-3);
  background: var(--surface);
  flex-wrap: wrap;
}
.submission-item:hover { background: var(--bg-tint); }
.submission-avatar {
  inline-size: 46px;
  block-size: 46px;
  border-radius: 50%;
  background: var(--surface-sunken);
  display: grid;
  place-items: center;
  font-weight: 800;
  color: var(--text-subtle);
  overflow: hidden;
  flex: none;
}
.submission-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.submission-info { flex: 1; min-inline-size: 180px; }
.submission-name { font-weight: 700; }
.submission-meta { font-size: var(--fs-xs); color: var(--text-subtle); }
.submission-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }

/* ==========================================================================
   13. MATCHES
   ========================================================================== */
.match-row {
  display: grid;
  grid-template-columns: 92px 1fr auto auto;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  margin-block-end: var(--sp-2);
  background: var(--surface);
}
.match-row:hover { background: var(--bg-tint); }
.match-date { font-size: var(--fs-xs); color: var(--text-subtle); font-variant-numeric: tabular-nums; }
.match-score {
  font-family: var(--ff-display);
  font-weight: 900;
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}
.match-result { inline-size: 6px; block-size: 34px; border-radius: var(--r-full); }
.match-result[data-r='win']  { background: var(--success); }
.match-result[data-r='draw'] { background: var(--text-subtle); }
.match-result[data-r='loss'] { background: var(--danger); }

/* --------------------------------------------------------------------------
   13b. CARTONS (#/cartons)

   Jaune et rouge ne se distinguent JAMAIS par la seule couleur : le jeton
   porte le libellé écrit en toutes lettres, et l'icône du rouge est un
   rectangle PENCHÉ là où celle du jaune est droite (core/icons.js). En
   niveaux de gris, sur une capture imprimée ou pour une deutéranope, les deux
   restent lisibles. Les teintes viennent des jetons existants — `--warning`
   (l'ambre de la palette or) et `--danger` — aucune nouvelle couleur, aucun
   vert.
   -------------------------------------------------------------------------- */
.card-give {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  margin-block-start: var(--sp-2);
}
.card-minute {
  inline-size: 9ch;
  font-variant-numeric: tabular-nums;
}

.btn--card {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  font-weight: 700;
}
.btn--card .ic { flex: none; }
/* `--warning` et NON `--accent-ink` : en thème sombre `--accent-ink` vaut
   #FF8A80, c'est-à-dire exactement `--danger` — un carton jaune peint en rouge.
   `--warning` est ambre dans les deux thèmes (5,59:1 clair, 12,85:1 sombre). */
.btn--card[data-card='yellow'] {
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 55%, transparent);
}
.btn--card[data-card='yellow']:hover:not(:disabled) { background: color-mix(in srgb, var(--warning) 14%, transparent); }
.btn--card[data-card='red'] {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}
.btn--card[data-card='red']:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 12%, transparent); }

.card-list {
  display: flex;
  flex-wrap: wrap;
  /* 12 px ENTRE LES LIGNES, 6 px entre les pastilles d'une même ligne. Les
     cibles tactiles des boutons débordent de 5 px en haut comme en bas
     (.card-chip__act::after) : avec l'ancien 6 px uniforme, deux pastilles
     superposées auraient vu leurs zones se chevaucher sur 4 px et un doigt posé
     entre deux lignes aurait supprimé le carton de la ligne du dessus. */
  gap: 12px 6px;
  align-items: center;
  margin-block-start: var(--sp-2);
}
.card-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 6px;
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
}
.card-chip bdi { font-variant-numeric: tabular-nums; }
.card-chip__nomin { font-weight: 400; opacity: .75; }
.card-chip[data-card='yellow'] {
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
  background: color-mix(in srgb, var(--warning) 12%, transparent);
}
.card-chip[data-card='red'] {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}
/* Le compteur de cumul : même jeton, sans le libellé écrit — il vit à côté du
   nom, où la place manque. L'`aria-label` et le `title` portent la phrase
   entière ; l'icône penchée/droite porte la distinction visuelle. */
.card-chip--count { padding-inline: 5px; }

/* --------------------------------------------------------------------------
   CORRIGER ET RETIRER UN CARTON — la partie « CRUD » de la pastille.

   L'utilisateur a signalé qu'il n'y avait « pas de CRUD pour supprimer un
   carton » alors que le bouton existait : `.card-chip__x` était une croix de
   14 px, sans contour, à 70 % d'opacité, posée sur un fond déjà coloré. Elle
   était là, elle ne se VOYAIT pas — et au doigt on visait à côté.

   Trois décisions, chacune payée par un défaut constaté :
    · UN CONTOUR ET UN FOND. Sans eux rien n'annonce « ceci est cliquable » :
      une icône nue au milieu d'une étiquette se lit comme une décoration.
    · 34 px visibles, 44 px tactiles. WCAG 2.5.8 demande 24 px, la règle de la
      maison est 44. On ne peint PAS 44 px : deux boutons de 44 px feraient une
      pastille plus haute qu'une ligne de tableau. Le pseudo-élément `::after`
      étend la cible SANS toucher à la mise en page.
    · gap de 10 px entre les deux boutons = exactement les 2 × 5 px que les
      `::after` débordent. Les zones se touchent sans JAMAIS se recouvrir : si
      elles se chevauchaient, le bouton peint en dernier — SUPPRIMER — volerait
      les clics destinés à MODIFIER. Sur une suppression, c'est inacceptable.

   Aucune couleur nouvelle : `--danger` et les teintes déjà portées par la
   pastille. Aucun vert.
   -------------------------------------------------------------------------- */
.card-chip--given {
  gap: 6px;
  padding: 3px 4px 3px 8px;
}
.card-chip__acts {
  display: inline-flex;
  align-items: center;
  gap: 10px;                 /* 10px = 2 × le débord des ::after ci-dessous */
  margin-inline-start: 2px;
}
.card-chip__act {
  position: relative;        /* ancre du ::after qui porte la cible tactile */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 34px;
  block-size: 34px;
  flex: none;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-full);
  background: var(--surface);
  /* `--text` et NON `inherit` : sur une pastille ROUGE, un crayon hérité serait
     rouge lui aussi et les deux boutons ne se distingueraient plus que par la
     forme de leur icône. Le neutre pour « corriger », le rouge pour « retirer » :
     l'action destructrice est la seule des deux à être peinte en danger. */
  color: var(--text);
  cursor: pointer;
  transition: background var(--t-1) var(--e-out), border-color var(--t-1) var(--e-out);
}
.card-chip__act::after {
  content: '';
  position: absolute;
  inset: -5px;               /* 34 + 2×5 = 44px de cible, sans un pixel de plus à l'écran */
}
.card-chip__act .ic { inline-size: 16px; block-size: 16px; }
.card-chip__act:hover { background: color-mix(in srgb, currentColor 16%, var(--surface)); }
.card-chip__act:focus-visible { z-index: 1; }   /* l'anneau de focus passe au-dessus du voisin */
.card-chip__act--danger {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
}

/* La légende du haut de page : les MÊMES boutons, hors contexte, à côté de la
   phrase qui dit ce qu'ils font. Ce sont des dessins, pas des commandes — d'où
   `pointer-events:none` (un clic dessus ne doit rien promettre) et pas de
   `::after`, une cible tactile sur un décor ne servirait qu'à gêner. */
.cards-hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-2) 0 0;
  color: var(--text-subtle);
  font-size: var(--fs-sm);
}
.card-chip__act--demo {
  pointer-events: none;
  inline-size: 28px;
  block-size: 28px;
}
.card-chip__act--demo::after { content: none; }

/* ==========================================================================
   14. IMAGE PICKER
   ========================================================================== */
.image-picker {
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
  padding: var(--sp-5);
  text-align: center;
  cursor: pointer;
  background: var(--surface-sunken);
  color: var(--text-subtle);
  transition: border-color var(--t-1) var(--e-ui), color var(--t-1) var(--e-ui);
}
.image-picker:hover, .image-picker:focus-within { border-color: var(--brand); color: var(--brand-ink); }
.image-picker img { max-block-size: 120px; border-radius: var(--r-xs); }
.image-picker input[type='file'] { display: none; }

/* ==========================================================================
   15. MODAL, TOAST, LIVE REGION
   ========================================================================== */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--scrim);
  display: grid;
  place-items: center;
  padding: var(--sp-4);
  overflow-y: auto;
}
body.has-modal { overflow: hidden; }

.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start: 2px solid var(--gold-400);
  border-radius: var(--r-md);
  inline-size: min(640px, 100%);
  max-block-size: 88vh;
  overflow-y: auto;
  padding: var(--sp-6);
  box-shadow: var(--shadow-lg);
}
.modal--sm { inline-size: min(420px, 100%); }
.modal--lg { inline-size: min(900px, 100%); }
.modal-header { display: flex; align-items: center; gap: var(--sp-3); margin-block-end: var(--sp-5); }
.modal-title { font-size: var(--fs-lg); font-weight: 800; flex: 1; }
.modal-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-subtle);
  padding: var(--sp-2);
  line-height: 0;
  border-radius: var(--r-sm);
}
.modal-close:hover { color: var(--text); background: var(--surface-hover); }
.modal-actions {
  display: flex;
  gap: var(--sp-3);
  justify-content: flex-end;
  margin-block-start: var(--sp-6);
  flex-wrap: wrap;
}
.dialog-text { color: var(--text-muted); line-height: var(--lh-body); }

/* The toast is always NIGHT, in both themes: it is a message from the system,
   not a surface of the page. */
.toast {
  position: fixed;
  inset-block-end: var(--sp-6);
  inset-inline-start: 50%;
  transform: translate(-50%, 120%);
  z-index: 400;
  background: var(--night-100);
  color: #F2F5EE;
  border: 1px solid var(--night-400);
  border-block-start-color: var(--edge-gold);
  padding: 12px 20px;
  border-radius: var(--r-md);
  font-size: var(--fs-md);
  font-weight: 600;
  box-shadow: var(--shadow-lg);
  transition: transform var(--t-2) var(--e-out);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  max-inline-size: min(90vw, 520px);
}
.toast.is-visible { transform: translate(-50%, 0); }
.toast[data-tone='success'] { border-block-start-color: var(--citron); }
.toast[data-tone='error']   { border-block-start-color: var(--danger); }
.toast-undo {
  background: rgba(255, 255, 255, .12);
  color: #F2F5EE;
  border: 1px solid rgba(227, 178, 78, .35);
  border-radius: var(--r-sm);
  padding: 4px 10px;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
}
.toast-undo:hover { background: var(--gold-400); color: var(--ink-on-gold); }

/* ==========================================================================
   16. PUBLIC PAGES (login, registration)
   ========================================================================== */
.centered-page {
  min-block-size: 100vh;
  min-block-size: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--sp-4);
}
.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start: 2px solid var(--gold-400);
  border-radius: var(--r-md);
  padding: var(--sp-8);
  inline-size: min(420px, 100%);
  box-shadow: var(--shadow-lg);
  text-align: center;
}
.auth-card .crest { margin-block-end: var(--sp-4); border-color: var(--edge-gold); }
.auth-card form { margin-block-start: var(--sp-5); text-align: start; }

.reg-shell { max-inline-size: 760px; margin: var(--sp-6) auto; padding-inline: var(--sp-4); }
.reg-header {
  background: var(--grad-brand);
  color: var(--on-grad-brand);
  border-block-start: 2px solid var(--gold-400);
  border-radius: var(--r-md) var(--r-md) 0 0;
  padding: var(--sp-8) var(--sp-6);
  text-align: center;
}
.reg-body {
  background: var(--surface);
  border: 1px solid var(--border);
  border-block-start: none;
  border-radius: 0 0 var(--r-md) var(--r-md);
  padding: var(--sp-6);
}
.reg-section-title {
  font-family: var(--ff-display);
  font-weight: 800;
  margin-block: var(--sp-6) var(--sp-4);
  padding-block-end: var(--sp-2);
  border-block-end: 1px solid var(--border);
  border-image: linear-gradient(var(--to-inline-end), var(--gold-400), transparent) 1;
}
.reg-section-title:first-child { margin-block-start: 0; }
.privacy-note {
  font-size: var(--fs-xs);
  color: var(--text-subtle);
  background: var(--surface-sunken);
  border-inline-start: 3px solid var(--info);
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  margin-block-start: var(--sp-4);
}
.honeypot { position: absolute; inset-inline-start: -9999px; opacity: 0; pointer-events: none; }

/* ==========================================================================
   17. RESPONSIVE
   ========================================================================== */
#scrim {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-2) var(--e-io);
}
#scrim.is-visible { opacity: 1; pointer-events: auto; }

/* Above the breakpoint the rail is permanent, so the scrim must never appear. */
@media (min-width: 981px) { #scrim { display: none; } }

@media (max-width: 980px) {
  /* Le tiroir fermé vit HORS de l'écran (translateX 100%). Rien ne le clippait,
     donc ses ~268px hors-champ gonflaient scrollWidth : toutes les pages
     défilaient latéralement de +100px vers du vide. `overflow-x: clip` coupe ce
     dépassement sans toucher à la logique du tiroir (déjà `inert` fermé), et
     n'empêche pas le défilement VERTICAL, contrairement à `hidden`. */
  html, body { overflow-x: clip; }

  /* `transform` has no logical form, so this is one of the two sanctioned
     physical exceptions: the drawer slides out past the edge it is pinned to —
     the right in Arabic, the left in French. */
  #sidebar { transform: translateX(100%); }
  html[dir='ltr'] #sidebar { transform: translateX(-100%); }
  #sidebar.is-open { transform: translateX(0); }
  #main { margin-inline: 0; }
  .hamburger { display: block; }
  .content { padding: var(--sp-4); }
  .topbar { padding-inline: var(--sp-4); }
  .match-row { grid-template-columns: 1fr auto; }
  .topbar-date { display: none; }
  .welcome-banner { min-block-size: 150px; padding: var(--sp-6) var(--sp-5); }

  /* ---- TOUCH TARGETS -----------------------------------------------------
     Measured on the live site: `.hamburger` 40×40, `.switch-btn` 31×38,
     `.icon-btn` 38×38 — five controls out of five under the 44px that WCAG
     2.5.5 (and every platform guideline) asks for on a touch screen. The
     smallest of them was `ع`: the button for the DEFAULT language of this
     site, the one a Moroccan visitor is most likely to reach for.

     A phone is short as well as narrow, so the `max-height: 960px` block above
     is also in force here and had squeezed the rail's rows to 36px. That trade
     buys nothing on a phone — the drawer is 100% of a scrolling screen, it has
     no fold to fight — and it costs the most on the device where fingers, not
     a trackpad, do the pointing. The full 44px comes back. */
  .nav-item { padding: var(--sp-2) var(--sp-4); min-block-size: 44px; }
  .nav-item .nav-icon { inline-size: var(--ic); block-size: var(--ic); }
  .hamburger,
  .icon-btn,
  .user-menu__btn { min-inline-size: 44px; min-block-size: 44px; }
  .switch-btn { min-inline-size: 44px; min-block-size: 44px; padding-inline: var(--sp-4); }
}

@media (max-width: 560px) {
  .players-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  .welcome-title { font-size: var(--fs-xl); }
  .stat-num { font-size: var(--fs-2xl); }
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
}

/* ==========================================================================
   19. LE RHABILLAGE — the poster skin of the console

   The showcase got the charter: Bebas Neue on the heads, the eyebrow with its
   wide tracking, cream paper with grain in the fibre, the gold hairline, the
   0.75rem corner, deep green and gold. This section brings that same language
   into the control room WITHOUT touching a single behaviour: not one rule
   below changes a display mode, a grid, a size that a script measures, or the
   geometry of the rail. It changes faces, tracking, corners, shadows and
   texture. Every screen keeps working exactly as it did.

   ⚠️ EVERYTHING HERE IS SCOPED TO `body.app-shell`, AND THAT IS NOT STYLE.
   `/index.html` (the showcase) and `/login.html` + `/register.html` load this
   very same stylesheet — `.card`, `.btn`, `.badge`, `table` are SHARED with
   them. An unscoped `.card { border-radius }` here would silently redraw the
   public site and the sign-in card, two files this change is forbidden to
   touch. `app-shell` is on the <body> of app.html and of nothing else, so the
   blast radius of this section is exactly the console. Do not "simplify" the
   scope away.

   TWO RULES OF THE CHARTER ARE OBEYED LITERALLY HERE, and they are the two
   that break Arabic when they are not:
     · no literal `letter-spacing` — only `--track-*`, which are ZERO in
       Arabic, because tracking severs the cursive joins;
     · poster type never hard-codes its weight — `--fw-poster` is 400 for
       Bebas and 800 for Cairo, which is what makes the Arabic head weigh the
       same as the Latin one.
   ========================================================================== */

/* --- 19a. THE PAPER -------------------------------------------------------
   The showcase is printed on paper; the console was printed on a screen. One
   fixed grain layer over the whole working surface closes that gap for one
   painted rectangle and zero DOM.

   `position: fixed` so it never repaints on scroll, and `z-index: 0`: a
   positioned element at 0 paints ABOVE normal-flow content, so `.content` is
   lifted to 1 below — otherwise the noise would sit on top of every table.
   `pointer-events: none` keeps it out of every click in the application. It
   is behind the rail (z-100), the top bar (z-50), the drawer scrim (z-90) and
   the modals (z-200) already. */
.app-shell #main { position: relative; background: var(--bg); }
.app-shell #main::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: overlay;
  background-image: var(--grain-url);
}
.app-shell .content { position: relative; z-index: 1; }
/* On the night the same noise at the same opacity disappears — the token
   exists for exactly this. */
@media (prefers-color-scheme: dark) {
  html:not([data-theme='light']) .app-shell #main::before { opacity: var(--grain-opacity-night); }
}
html[data-theme='dark'] .app-shell #main::before { opacity: var(--grain-opacity-night); }

/* --- 19b. THE TOP BAR — the console's poster line -------------------------
   The page title was 24px of Cairo 900. It is now the POSTER VOICE, the same
   one that carries the showcase: Bebas in French and English, Cairo 800 in
   Arabic (`--fw-poster` switches for us). `--poster-tail` reserves the descent
   of the ي / ج / ع, which any `overflow: hidden` ancestor would otherwise
   slice off — it is 0 in Latin, so it costs FR/EN nothing.

   The bar keeps its 76px (`--topbar-h`) and its sticky behaviour untouched:
   `.table-wrap` subtracts that exact token from the viewport height, so a bar
   that grew would shorten every table in the product. */
.app-shell .topbar {
  position: sticky;                 /* restated for clarity — unchanged */
  background: var(--bg);
}
/* The gold hairline under the bar: the light falls from above onto everything
   below it, which is the one idea the whole charter is built on. It fades out
   toward the far edge, so it reads as light falling off a rule and not as a
   border that stopped. It mirrors with the language for free. */
.app-shell .topbar::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: -1px;
  block-size: 1px;
  background-image: linear-gradient(var(--to-inline-end),
    var(--gold-400) 0%, rgba(227, 178, 78, .35) 55%, transparent 100%);
  pointer-events: none;
}
.app-shell .topbar-title {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--ff-poster);
  font-weight: var(--fw-poster);
  font-size: var(--fs-2xl);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-poster);
  text-transform: uppercase;        /* a no-op in Arabic, which has no case */
  padding-block-end: var(--poster-tail);
  min-inline-size: 0;
}
/* The khatem in front of the title: the punctuation of this site, quoted at
   the head of every view. Order-8 symmetry, so it is identical mirrored. */
.app-shell .topbar-title::before {
  content: '';
  inline-size: 9px;
  block-size: 9px;
  flex: none;
  background: var(--gold-400);
  clip-path: var(--khatem-clip);
}

/* --- 19c. THE CORNER ------------------------------------------------------
   0.75rem is the radius of the reference design and of the showcase. The
   console sat at 8px. Everything that is a PANEL moves to 12px; everything
   that is a CONTROL (button, input, chip) stops at 8px, because a 12px corner
   on a 32px button reads as a toy. The rail's own items are NOT in this list:
   their `0 4px 4px 0` shape is what makes the active gold bar read as a bar. */
.app-shell .card,
.app-shell .table-wrap,
.app-shell .stat-card,
.app-shell .player-card,
.app-shell .welcome-banner,
.app-shell .schedule-item,
.app-shell .announcement-item,
.app-shell .attendance-card,
.app-shell .modal,
.app-shell .skeleton__row { border-radius: var(--r-lg); }

/* The panel now carries a resting shadow. It had none, so a card on cream was
   an outline and nothing else; the showcase's panels sit ON the paper. */
.app-shell .card { box-shadow: var(--shadow-sm); }
.app-shell .card:hover { box-shadow: var(--shadow-md); }

/* --- 19d. THE HEADS -------------------------------------------------------
   Card titles take the display voice properly (`--fw-display`, `--track-display`)
   instead of a hard-coded 700, and the header gets the gold hairline under it. */
.app-shell .card-title,
.app-shell .modal-title,
.app-shell .announcement-title,
.app-shell .welcome-title {
  font-family: var(--ff-display);
  font-weight: var(--fw-display);
  letter-spacing: var(--track-display);
  line-height: var(--lh-tight);
}
.app-shell .card-header {
  position: relative;
  padding-block-end: var(--sp-3);
}
.app-shell .card-header::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 1px;
  background-image: linear-gradient(var(--to-inline-end),
    rgba(227, 178, 78, .55) 0%, rgba(227, 178, 78, .18) 45%, transparent 100%);
  pointer-events: none;
}

/* The ONE photograph in the administration deserves the poster treatment: the
   club's name in poured gold on the night, which is the exact gesture of the
   hero. `.u-gold-text` is legible ONLY on the night — this band is the night,
   and it is the only place in the console where this is allowed.
   `color` is declared BEFORE the clip so a browser without `background-clip:
   text` renders solid gold rather than an invisible headline. */
.app-shell .welcome-title {
  font-family: var(--ff-poster);
  font-weight: var(--fw-poster);
  font-size: var(--fs-3xl);
  letter-spacing: var(--track-poster);
  text-transform: uppercase;
  padding-block-end: var(--poster-tail);
  color: var(--gold-300);
  background-image: var(--grad-gold-text);
  -webkit-background-clip: text;
  background-clip: text;
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .app-shell .welcome-title { color: transparent; }
}
/* Forced colours throws the gradient away and would leave transparent text —
   an empty headline for exactly the users who need it most. */
@media (forced-colors: active) {
  .app-shell .welcome-title { color: CanvasText; background-image: none; }
}
.app-shell .welcome-banner { box-shadow: var(--shadow-premium); }

/* --- 19e. THE EYEBROW -----------------------------------------------------
   `--track-label` is the wide tracking of the reference's `— MATCHDAY`, and it
   is ZERO in Arabic, where an eyebrow is spaced with WORDS instead. Applied to
   the three places the console already had a small tracked label: the rail's
   group breaks, the stat caption, and the table head.

   `white-space: nowrap` on the group titles is LOAD-BEARING: at .3em of
   tracking a French group name could wrap to a second line, every group break
   would grow ~14px, and the last entries of the rail would fall out of the nav
   box again — the exact bug tests/layout.test.mjs guards. The hairline after
   the word is `flex: 1` and absorbs the width instead. */
.app-shell .nav-group-title {
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--track-label);
  white-space: nowrap;
}
.app-shell .stat-label,
.app-shell thead th {
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-label);
}
/* A tracked head must not be allowed to widen a numeric column into a scroll. */
.app-shell thead th { font-weight: 700; }

/* THE FIGURE IS THE HERO — now in the poster face. Bebas numerals at
   `--fs-d1` are the single most charter-faithful thing in the console; in
   Arabic `--ff-poster` falls back to Cairo at `--fw-poster` (800) and the
   figure keeps its optical weight. Size, colour, tabular alignment and the
   `<bdi class="num">` isolation are all untouched. */
.app-shell .stat-num {
  font-family: var(--ff-poster);
  font-weight: var(--fw-poster);
  letter-spacing: var(--track-poster);
}

/* --- 19f. THE CONTROLS ----------------------------------------------------
   8px corners (a control, not a panel) and the label in the small-caps voice
   of the design. `text-transform` is applied ONLY in French and English:
   Arabic has no case, and `uppercase` on Arabic is at best a no-op and at
   worst a shaping hazard. */
.app-shell .btn,
.app-shell .icon-btn,
.app-shell input,
.app-shell select,
.app-shell textarea,
.app-shell .rail-logout { border-radius: var(--r-md); }

.app-shell .btn { letter-spacing: var(--track-wide); }
:lang(fr) .app-shell .btn,
:lang(en) .app-shell .btn { text-transform: uppercase; }

/* The primary action carries the club's own light. */
.app-shell .btn--primary:hover:not(:disabled) { box-shadow: var(--shadow-glow-green); }
.app-shell .btn--gold:hover:not(:disabled)    { box-shadow: var(--shadow-glow-gold); }

/* A row of the table lights up in the club's wash rather than a grey tint —
   the same "rise into the light" verb as a card and a gallery tile. */
.app-shell tbody tr:hover { background: var(--brand-wash); }

/* --- 19g. THE RAIL --------------------------------------------------------
   The rail is the one piece of the console that was ALREADY the charter: deep
   green in both themes, the gold bar of the active item, the khatem watermark.
   NOTHING structural is touched here — not a padding, not a `min-block-size`,
   not the grid — because its geometry is under test at 1440×900 and 1366×768
   and it has been broken three times before. Two cosmetic lines only. */
.app-shell .club-name {
  font-family: var(--ff-display);
  font-weight: var(--fw-display);
  letter-spacing: var(--track-display);
}
.app-shell .nav-item { border-radius: 0 var(--r-md) var(--r-md) 0; }
html[dir='ltr'] .app-shell .nav-item { border-radius: var(--r-md) 0 0 var(--r-md); }

/* ==========================================================================
   18. PRINT — roll-call sheets and player cards
   ========================================================================== */
@media print {
  #sidebar, .topbar, .toast, .btn, .toolbar, .hamburger, .modal-overlay, #scrim { display: none !important; }
  body::before { display: none; }
  #main { margin: 0; }
  .content { padding: 0; max-inline-size: none; }
  .card { border: none; box-shadow: none; page-break-inside: avoid; }
  body { background: #fff; color: #000; }
  table { font-size: 11pt; }
  thead { display: table-header-group; }
  .print-only { display: block !important; }

  /* Section 19 is scoped one class deeper than these rules, so on specificity
     ALONE it would win inside `@media print` and put a drop shadow and a sheet
     of grain onto a roll-call sheet. The skin is undone here explicitly. */
  .app-shell #main::before { display: none; }
  .app-shell .card { border: none; box-shadow: none; }
  .app-shell .card-header::after,
  .app-shell .topbar::after { display: none; }
  /* Poured gold prints as nothing at all on white paper. */
  .app-shell .welcome-title { color: #000; background-image: none; }
}
.print-only { display: none; }
