/* Family Portal — V1 navigation skeleton
   Tablet-first: large touch targets, big text, minimal chrome.
   No child-facing text lives here — copy comes from config/strings/fr.json. */

/* Typography (docs/art/spec_production_visuels_accueil.md item 6, chosen
   2026-09-10): Baloo 2 for titles/accroches (rounded display face, echoes
   the wordmark's bubble letters without duplicating it), Nunito for body
   text (neutral, soft terminals, reads well small for parent-facing
   paragraphs). Self-hosted (web/assets/fonts/) rather than loaded from the
   Google Fonts CDN per the fiche's RGPD note — a CDN call would send every
   visitor's IP/UA to Google on every visit. Only the "latin" subset is
   fetched (covers all of é/è/à/ç/œ — French accents live in the Latin-1
   range U+0000-00FF, œ/Œ is explicitly U+0152-0153, both inside this
   subset), not the other scripts Google Fonts also serves (cyrillic,
   vietnamese, devanagari...), which this project has no use for. Both
   files are variable fonts — one physical file per family covers both
   weights below; the browser picks the right instance from the file
   itself via each @font-face's own font-weight value, same as Google's
   own served CSS does. */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/baloo2-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/baloo2-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/nunito-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/nunito-latin.woff2') format('woff2');
}

:root {
  --color-bg: #fdf6e9;
  --color-surface: #ffffff;
  --color-text: #2c2c2c;
  --color-text-muted: #6b6b6b;
  --color-primary: #3b6fa4;
  --color-primary-dark: #274b73;
  --color-accent: #f2994a;
  --color-border: #d8d0bd;
  --radius-lg: 24px;
  --radius-md: 16px;
  --touch-min: 56px;
  --font-heading: 'Baloo 2', Verdana, "Segoe UI", sans-serif;
  --font-body: 'Nunito', Verdana, "Segoe UI", sans-serif;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--color-bg);
  color: var(--color-text);
  min-height: 100vh;
}

h1, h2, h3,
.app-title,
.section-title,
.btn,
.hub-card-title {
  font-family: var(--font-heading);
}

.page {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 24px 64px;
}

.page-narrow {
  max-width: 480px;
}

.top-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 32px;
}

.top-bar-links {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* dashboard.php: keeps its "Page des enfants" button right next to the H1
   instead of pushed to the far end of .top-bar's own space-between — that
   parent rule still applies, but with only this one grouped child inside
   it now, it just sits at the start. Reused for the "Ajouter un profil
   enfant" heading + Masquer/Afficher button too. */
.top-bar-title-group {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

/* .app-title (the dashboard H1) already has margin:0, which is why that
   first usage centered against its button with no help needed. .section-title
   does not (32px/16px top/bottom by default) — reused here as a flex child,
   that asymmetric margin skewed align-items:center against a near-zero-
   margin button next to it. Neutralized so centering only ever depends on
   the wrapper's own layout; space above the whole group is controlled by
   .top-bar-title-group-spaced on the wrapper instead of the heading's own
   margin, so the two concerns don't fight each other. */
.top-bar-title-group .section-title {
  margin: 0;
}

/* Same spacing value as .section-title-spaced (see exercise_hub.php's "Les
   jeux" heading), applied to the wrapper instead of the heading for the
   reason above — dashboard.php's "Ajouter un profil enfant" row, to clear
   it from the child cards above it. */
.top-bar-title-group-spaced {
  margin-top: 64px;
}

/* web/parent/dashboard.php — same button row, reused at the bottom of the
   page instead of in the top-bar. */
.dashboard-footer-links {
  justify-content: center;
  margin-top: 40px;
}

.app-title {
  font-size: 1.75rem;
  margin: 0;
  color: var(--color-primary-dark);
}

.app-title img {
  height: 56px;
  width: auto;
  display: block;
}

/* web/index.php's child-grid view — 2x the base .app-title img size (the
   landing/marketing header has its own separate scaling, see home.css's
   .landing-menu .app-title img, so this doesn't touch that). Qualified with
   both .app-title and the element type (".app-title img.app-title-logo-lg",
   not just ".app-title-logo-lg") so its specificity actually beats the
   plain ".app-title img" rule above instead of losing to it. */
.app-title img.app-title-logo-lg {
  height: 112px;
  width: auto;
  display: block;
}

.app-logo-link {
  display: flex;
  align-items: center;
}

/* web/parent/dashboard.php's header logo — same 2x size as .app-title-logo-lg
   above, kept as its own class since it isn't nested inside .app-title there. */
.app-title-logo {
  height: 112px;
  width: auto;
  display: block;
}

.section-title {
  font-size: 1.4rem;
  color: var(--color-primary-dark);
  margin: 32px 0 16px;
}

/* exercise_hub.php's "Les jeux" title — extra clearance above it, since the
   curated exercise cards above (see .hub-grid-exercises) already bleed
   past their own bottom edge with the character art. */
.section-title-spaced {
  margin-top: 64px;
}

/* Buttons — 3 flat button-background assets (Notion "FunDrill v2.2 (charte
   graphique)" > Boutons pour toutes les pages), applied as a stretchable
   border-image (fill keyword renders the source's own center fill too, not
   just its edges) rather than a plain background so buttons of any width
   keep the asset's own rounded corners undistorted. border-image-slice is
   an estimate of the asset's own corner size in source pixels (all 3 share
   roughly the same ~270x153 canvas/corner radius) — re-check visually
   against docs/art/UI/button_*.png if a future asset revision changes the
   corner proportions.
   - button_basic.png (cream/tan): the default/neutral button — most of the
     app's buttons (Recommencer/Quitter, Lancer un exercice, nav links,
     dense parent-zone admin rows...).
   - button_green.png: an affirmative/primary action — Valider, Se
     connecter, Créer un compte.
   - button_red.png: a destructive or session-ending action — Se
     déconnecter, supprimer/désactiver (parent zone).
   Text stays dark ink on the cream background (already readable) but must
   switch to white on the green/red fills — flat colors under thin text
   would fail contrast otherwise. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  padding: 12px 28px;
  font-size: 1.1rem;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.1s ease;
  background: none;
  border-style: solid;
  border-width: 18px;
  border-image-repeat: stretch;
  border-image-slice: 44 fill;
  border-image-width: 18px;
  /* Fallback in case the border-image asset fails to load (offline, blocked
     request...) — still reads as a button rather than plain text. */
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  /* Same soft-shadow-under-a-pill-button look already used everywhere in
     the exercise/game mechanics (e.g. web/exercises/type_word/play.html's
     .reset/.validate-btn) — a neutral shadow by default, overridden to a
     shadow tinted to match the button's own fill below. */
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}

.btn:active {
  transform: scale(0.97);
}

/* "In alpha" until a form's required fields are all filled (see
   web/assets/js/form_gate.js) — a plain opacity fade rather than a
   greyed-out fill swap, since buttons use an image border (border-image)
   that doesn't respond to a background-color change. */
.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn:disabled:active {
  transform: none;
}

/* Keyboard-focus ring (FUN-146): the border-image asset paints a plain
   rectangle underneath its own rounded corners, so the browser's default
   focus outline — which traces that rectangle, not the visible pill shape —
   read as the flat square-cornered button from before this border-image
   redesign. An explicit rounded outline (border-radius here does apply to
   outline even though the border-image ignores it) keeps the focus ring
   consistent with the new look. */
.btn:focus-visible {
  outline: 3px solid var(--color-primary-dark);
  outline-offset: 4px;
  border-radius: var(--radius-md);
}

.btn-primary {
  border-image-source: url('../images/ui/button_green.png');
  color: #ffffff;
  box-shadow: 0 6px 14px rgba(60, 174, 112, 0.4);
}

.btn-secondary {
  border-image-source: url('../images/ui/button_basic.png');
  color: var(--color-text);
}

/* A standalone OAuth button (Google sign-in/signup) placed above its
   email/password form alternative — centered on its own line, with room
   below it so it doesn't read as glued to the form that follows. */
.oauth-button-row {
  display: flex;
  justify-content: center;
  margin: 0 0 24px;
}

.btn-danger {
  border-image-source: url('../images/ui/button_red.png');
  color: #ffffff;
  box-shadow: 0 6px 14px rgba(241, 106, 84, 0.4);
}

/* Child picker */

.child-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 24px;
}

/* .child-grid's own grid-template-columns (minmax(160px, 1fr)) always
   divides the full row width evenly across however many cards there are —
   fine for the 120px avatar it was tuned for, but with the doubled
   .child-avatar-xl (240px) that even share can come out narrower than the
   avatar itself (e.g. 3 cards in the page's 960px max-width), and the
   overflowing avatar then renders hard against the card's left edge
   instead of centered. Fixed-width flex cards that wrap and center
   (per CLAUDE.md's "wrapped item rows" rule) sidestep this instead of
   trying to out-guess the grid's column math. */
.child-grid-spaced {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin: 32px 0 48px;
}

.child-grid-spaced .child-card {
  width: 340px;
}

/* web/index.php's no-children-yet state (family logged in, zero child
   profiles created): a random schoolkid character (tranche B) centered
   above the "come create a profile" hint. */
.home-empty-state {
  text-align: center;
  margin: 32px 0 48px;
}

.home-empty-state img {
  max-width: 180px;
  height: auto;
}

.child-card {
  padding: 24px 16px;
  text-align: center;
  text-decoration: none;
  color: var(--color-text);
  transition: transform 0.15s ease;
  background: none;
  border-style: solid;
  border-width: 20px;
  border-image-source: url('../images/ui/button_basic.png');
  border-image-slice: 44 fill;
  border-image-width: 20px;
  border-image-repeat: stretch;
  border-radius: var(--radius-lg);
  background-color: var(--color-surface);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}

.child-card:active {
  transform: scale(0.96);
}

.child-card-inactive {
  opacity: 0.55;
}

.child-avatar {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--color-border);
  font-size: 2.5rem;
  font-weight: 700;
  color: #ffffff;
  margin: 0 auto 12px;
}

.child-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Home page (web/index.php) only — doubles the shared .child-avatar size for
   the child picker's own cards, without touching the base size still used by
   parent/dashboard.php. */
.child-avatar-xl {
  width: 240px;
  height: 240px;
  font-size: 5rem;
}

.child-avatar-sm {
  width: 56px;
  height: 56px;
  font-size: 1.4rem;
  margin: 0;
}

/* exercise_hub.php, and (2026-09-16 user request: "doubler la taille de la
   photo") child_detail.php's own header — 2x .child-avatar-sm. Not applied
   to all_exercises.php/change_photo.php's own .child-avatar-sm usage. */
.child-avatar-lg {
  width: 112px;
  height: 112px;
  font-size: 2.8rem;
  margin: 0;
}

.child-header {
  display: flex;
  align-items: center;
  gap: 12px;
}

.child-name {
  font-size: 1.3rem;
  font-weight: 700;
  display: block;
}

/* exercise_hub.php only — wraps .child-name plus the small access-control
   reminder caption below it (Notion "Étape v2.3" #5), so the caption
   stacks under the name instead of sitting beside it in .child-header's
   own flex row. */
.child-name-group {
  display: flex;
  flex-direction: column;
}

.child-access-caption {
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* exercise_hub.php's blocked-access state (outside allowed hours / daily
   time cap reached) — replaces the exercises/games grids with a single
   centered message, same idea as .no_exercises/.no_games's plain <p> but
   with its own "Retour aux enfants" button since there's nothing else to
   click on this page while blocked. */
.access-blocked {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  padding: 48px 16px;
}

.access-blocked-message {
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--color-text-muted);
}

.child-level {
  font-size: 0.95rem;
  color: var(--color-text-muted);
  display: block;
}

.child-last-activity {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  display: block;
}

.token-badge {
  background: var(--color-accent);
  color: #ffffff;
  padding: 10px 20px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 1.1rem;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Colored shadow (tinted to the badge's own fill, same pattern as
     .btn-primary/.btn-danger above) rather than the neutral black shadow
     used elsewhere — makes the star counter pop more. */
  box-shadow: 0 6px 14px rgba(242, 153, 74, 0.45);
}

/* Replaces the "jetons" text label on the token balance badge — a young
   non-reading child recognizes the star icon faster than the word. */
.token-badge-star {
  width: 22px;
  height: 22px;
  /* Also used plain inline (child_detail.php's balance line, not inside
     .token-badge's own flex/align-items) — keeps it level with the digits
     there too. */
  vertical-align: middle;
}

/* Forms */

.form-card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: 24px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}

/* parent/dashboard.php's "Créer un profil enfant" form only — half the
   width of the page's own 960px max-width, rather than stretching to fill
   it like every other .form-card on this page. */
.form-card-narrow {
  max-width: 480px;
}

/* parent/dashboard.php's "Créer un profil enfant" form — small breathing
   room from the heading/Masquer row right above it. */
.form-card-top-spaced {
  margin-top: 16px;
}

/* parent/dashboard.php's referral link widget (Notion "Etape v2.5
   (parrainage)" Ordre 2) — deliberately NOT a .form-card: sits directly on
   the page's standard background rather than a white surface, separated
   from the "Créer un profil enfant" section above it by a rule instead of
   a card boundary (2026-09-15, moved here from above the children section
   per user feedback). */
.referral-card {
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--color-border);
}

.referral-link-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.referral-link-row input[type="text"] {
  flex: 1 1 260px;
  min-height: 36px;
  padding: 4px 8px;
  font-size: 0.9rem;
  border-radius: 8px;
  border: 2px solid var(--color-border);
  font-family: inherit;
}

.referral-copy-feedback {
  display: inline-block;
  margin-top: 6px;
  font-size: 0.85rem;
  color: var(--color-success, #2e7d32);
}

/* Without this, the unconditional display:inline-block above beats the
   [hidden] attribute's UA display:none (author styles always outrank UA
   defaults), so the "Lien copié !" feedback showed permanently instead of
   only after a real copy click. Found 2026-09-15 while screenshotting the
   referral widget's new position. */
.referral-copy-feedback[hidden] {
  display: none;
}

/* Referral scene widget (Ordre 10) — replaces the old flat-text
   .referral-potential block with the iso world-map track (docs/art/
   fiche_cc_scene_parrainage_iso.md §8/§9, validated v6 mockup). Ported
   from the mockup's own CSS custom properties where they don't already
   have an equivalent above (--rs-path-color etc.). */
.referral-scene {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--color-border);
  --rs-path-color: var(--color-border);
  --rs-green: #5f9e52;
  --rs-green-dark: #3f7a35;
}

.referral-scene-intro {
  margin: 0 0 4px;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

.referral-scene-intro-coupon {
  height: 1.1em;
  width: auto;
  vertical-align: text-bottom;
}

.referral-scene-caption {
  margin: 0 0 6px;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

.referral-scene-strip {
  position: relative;
  overflow-x: auto;
  overflow-y: visible;
  padding: 34px 10px 24px;
}

/* width:100% + justify-content:space-between makes the track fill the
   card's available width whenever the stops fit within it (min-width
   still forces it to its natural content width, and thus a horizontal
   scroll, once they don't) — see feedback that the scene read as cramped
   into a narrow strip instead of using the card's full width. */
.referral-scene-track {
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
  width: 100%;
  min-width: max-content;
  /* Tall enough for the current landmark at its +50% size (diorama up to
     ~340px, plus shadow/label/badges below and the crown overlay's own
     -30px overflow above it) plus the sine-wave winding offset — .strip's
     overflow-x:auto forces overflow-y to clip too (the CSS paired-overflow-
     axis rule, already caught once before on decor pieces), so anything
     taller than this gets silently cropped instead of just spilling over. */
  min-height: 600px;
}

svg.rs-path-line {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.rs-stop {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  flex: 0 0 auto;
  margin: 0 3px;
  /* Baseline stacking order for everything ON the path (dots, marker,
     landmarks) — decor's own inline z-index (see .rs-decor below) sits
     below or above this depending on whether it's meant to read as
     further back or closer to the viewer, iso-painter's-algorithm style. */
  z-index: 50;
}

/* Ambient decor — a backdrop AROUND the path, not stops ON it (per a
   mockup the user drew after the first version put decor icons directly
   inline on the dashed line). .rs-decor itself is a near-zero-width flex
   anchor (barely affects the track's own layout width even with 10-14 of
   them); the actual prop art escapes via position:absolute so it can float
   well above or below the line (per-item 'footY', computed in PHP, always
   at least 45px from the centerline so a clear corridor stays open right
   around the path) without needing its own layout space. z-index is set
   inline per item (see the template), overriding this rule's own baseline.
   Full opacity — no longer needs to stay muted to avoid competing with the
   real dots, since it no longer shares their line. */
.rs-decor {
  width: 2px;
  position: relative;
}

/* Anchored at the BOTTOM (via the `bottom` property, not `top`) so every
   prop "stands" on its own point on an implied ground line regardless of
   its own height — the taller ones (trees) grow upward from the same
   footpoint a short one (a rock) would use, exactly like a 2D iso game's
   sprite anchor convention, per direct feedback. Sized by height (fixed
   per source image, see the asset bank's own `size` field), not width —
   these are standing props, height is what should read as consistent
   scale, width just follows the art's own aspect ratio. */
.rs-decor-img {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: auto;
  display: block;
}

/* filleul rank dots: the referred child's own art, shown only once that
   rank is paid (pos <= paid — "avec l'étoile ou avant") — a signed-up-but-
   unpaid rank shows its green dot alone, no portrait, per direct feedback.
   Sized down from the first pass so the current tier's landmark (much
   bigger now, see .rs-landmark-current below) reads as the clear focal
   point rather than competing with these for visual weight. */
.rs-filleul-img {
  height: 22px;
  width: auto;
  display: block;
}

/* Bigger, oval rather than circular — matches the iso art's own ground-
   plane perspective (a flat circle on the ground reads as an ellipse from
   that camera angle), per direct feedback. Doubled again (2026-09-17) on
   top of that, per further direct feedback. */
.rs-track-dot {
  width: 40px;
  height: 22px;
  border-radius: 50%;
  margin-top: 8px;
  background: var(--color-border);
}

.rs-track-dot.rs-dot-green {
  background: var(--rs-green);
}

.rs-track-dot.rs-dot-accent {
  background: var(--color-accent);
  width: 52px;
  height: 28px;
}

/* "Stage zero" (départ, 0 paid): a real dot occupying the same layout slot
   as any other rank's — so the connecting path actually anchors at the
   path's true start and the star sits flush with the left edge — but
   invisible, since rank 0 isn't a real filleul. visibility:hidden (not
   display:none) keeps it measurable via getBoundingClientRect for
   referral_scene.js's own path-anchor scan. */
.rs-track-dot.rs-dot-hidden {
  visibility: hidden;
}

/* "vous êtes ici" — last confirmed-paid rank, idle on the path. Made much
   more prominent per direct feedback ("l'étoile doit être bien plus
   visible") — bigger art plus a soft glow halo behind it (::before on the
   stop) so it reads as the one special element on an otherwise flat dotted
   trail, not just another dot. */
.rs-marker {
  position: relative;
}

.rs-marker::before {
  content: '';
  position: absolute;
  top: -10px;
  left: 50%;
  transform: translateX(-50%);
  width: 76px;
  height: 76px;
  background: radial-gradient(circle, rgba(242, 153, 74, 0.38) 0%, rgba(242, 153, 74, 0) 70%);
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
}

.rs-marker-star {
  width: 56px;
  height: 56px;
  object-fit: contain;
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.3));
}

/* paliers: one pre-composed diorama image per gabarit (2026-09-17 real art
   batch, replacing the earlier platform+pieces composition — see
   config/asset_banks/referral_scene_iso.json's own note), sized per
   past/current/upcoming via the template's inline width/height. */
.rs-diorama {
  position: relative;
}

.rs-diorama-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Summit crown overlay (couronne.png) — the lifetime-free tier has no
   dedicated diorama of its own, it reuses its pool gabarit's image (see
   fp_build_referral_scene()'s own comment) with this perched on top. Sized
   to match the star mascot (.rs-marker-star, 56×56) exactly, per direct
   feedback, rather than scaling with the (now much bigger) diorama box.
   Raised further (2026-09-17, "remonte aussi la couronne") once the coupon
   row moved above the diorama too (see .rs-landmark-coupons) — the two
   never actually coexist in the current tier config (the lifetime-free
   tier awards 0 coupons of its own) but this stays clear of it either way. */
.rs-diorama-crown {
  position: absolute;
  width: 56px;
  height: 56px;
  object-fit: contain;
  top: -70px;
  left: 50%;
  transform: translateX(-50%);
  filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.3));
}

/* Arrival mascot (celebrate.png / proud.png) — made much more prominent
   per direct feedback ("l'étoile doit être bien plus visible"): bigger,
   perched at the top-right corner rather than low/overlapping the art, with
   a stronger drop shadow so it reads clearly as a distinct arrival signal. */
.rs-diorama-mascot {
  position: absolute;
  height: 62%;
  width: auto;
  right: -10%;
  top: -12%;
  filter: drop-shadow(0 4px 5px rgba(20, 14, 4, 0.35));
}

.rs-landmark-past .rs-diorama {
  opacity: 0.55;
  filter: saturate(0.6);
}

.rs-landmark-shadow {
  height: 6px;
  width: 55%;
  border-radius: 50%;
  background: rgba(20, 14, 4, 0.15);
  filter: blur(1px);
  margin-top: -4px;
}

.rs-landmark-label {
  margin-top: 4px;
  font-size: 0.62rem;
  font-weight: 700;
  color: var(--color-text-muted);
  text-align: center;
  white-space: nowrap;
}

.rs-landmark-current .rs-landmark-label {
  font-size: 1rem;
  color: var(--color-text);
}

.rs-landmark-badge {
  margin-top: 2px;
  font-size: 0.62rem;
  font-weight: 700;
  color: var(--color-primary-dark);
  background: rgba(59, 111, 164, 0.12);
  padding: 2px 7px;
  border-radius: 999px;
  white-space: nowrap;
}

.rs-landmark-current .rs-landmark-badge {
  font-size: 0.76rem;
  padding: 3px 10px;
}

/* Coupon icons ABOVE each landmark (moved up from below, 2026-09-17, per
   direct feedback) — how many that tier awards ("affiche des coupons sous
   chaque palier pour indiquer le montant qu'on va gagner", the original
   ask that placed these; the "above, and bigger" follow-up kept the same
   per-tier-count logic). Sits before .rs-diorama in the markup so normal
   flex-column flow puts it above for free, no absolute positioning
   needed. Reuses the same coupon.png as .referral-wallet, capped visually
   at 6 (fp_build_referral_scene() caps the loop itself, this is just
   belt-and-suspenders wrapping for a stray config). */
.rs-landmark-coupons {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px;
  margin-bottom: 6px;
  max-width: 160px;
}

.rs-landmark-coupon-icon {
  width: 28px;
  height: auto;
  display: block;
}

.rs-landmark-current .rs-landmark-coupon-icon {
  width: 40px;
}

.rs-collapse-pill {
  font-weight: 800;
  font-size: 0.68rem;
  background: var(--color-bg);
  color: var(--color-text-muted);
  border-radius: 999px;
  padding: 4px 10px;
  border: 1px dashed var(--color-border);
  white-space: nowrap;
}

/* Coupon wallet (Ordre 7) — real, credited coupons, so unlike
   .referral-potential above this is NOT muted: it's an actual balance the
   parent can act on. */
.referral-wallet {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--color-border);
}

.referral-wallet-flash {
  font-weight: 700;
  margin: 0 0 6px;
}

.referral-wallet-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 6px;
}

.referral-wallet-actions form {
  margin: 0;
}

/* Referral scene "cheat" — dev-environment families only (see the PHP-side
   gate), so deliberately styled as an obvious tool rather than a normal
   product action: dashed border, muted background, small print. */
.referral-cheat {
  margin-top: 10px;
  padding: 8px 10px;
  border: 1px dashed var(--color-border);
  border-radius: 8px;
  background: var(--color-bg);
}

.referral-cheat-label {
  margin: 0 0 6px;
  font-size: 0.76rem;
  color: var(--color-text-muted);
}

.referral-cheat-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.referral-cheat-form {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0;
}

.referral-cheat-form input[type="number"] {
  width: 3.5em;
  min-height: 30px;
  padding: 2px 6px;
  border-radius: 6px;
  border: 2px solid var(--color-border);
  font-family: inherit;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 20px;
}

.form-field label {
  font-size: 1.05rem;
  font-weight: 700;
}

.form-field input,
.form-field select,
.form-field textarea {
  min-height: var(--touch-min);
  padding: 12px 14px;
  font-size: 1.05rem;
  border-radius: var(--radius-md);
  border: 2px solid var(--color-border);
  background: #ffffff;
  font-family: inherit;
}

.form-field textarea {
  font-family: monospace;
  resize: vertical;
}

.form-field span {
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

.form-field-checkbox label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 0.95rem;
  font-weight: 400;
}

.form-field-checkbox input[type="checkbox"] {
  min-height: unset;
  width: 22px;
  height: 22px;
  flex: none;
  margin-top: 2px;
}

.form-error {
  color: #b3261e;
  font-weight: 700;
  margin-bottom: 16px;
}

/* Password show/hide toggle (FUN v2.2 item 14) — web/assets/js/password_toggle.js
   wraps every input[type=password] in this div at runtime (no markup change
   needed on the pages themselves) and appends the eye/eye-slash button
   inside it, absolutely positioned over the input's own right padding. */
.password-field {
  position: relative;
}

.password-field input {
  width: 100%;
  padding-right: 48px;
}

.password-toggle {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--color-text-muted);
  cursor: pointer;
}

.password-toggle:active {
  transform: translateY(-50%) scale(0.9);
}

.password-toggle svg {
  width: 22px;
  height: 22px;
}

/* Exercise / game hub */

.hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
}

/* Games section only (web/child/exercise_hub.php) — fixed at 3 cards per
   row regardless of viewport width, wrapping after that rather than
   auto-fit-ing to however many 200px cards happen to fit. Flexbox (not
   grid — see CLAUDE.md's "wrapped item rows" rule) so a partial last row
   (e.g. 4 games -> 3 + 1) centers instead of sitting flush-left. */
.hub-grid-games {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 20px;
}

.hub-grid-games .hub-card-game {
  width: calc((100% - 40px) / 3);
}

/* "Voir tous les exercices" under the curated hub grid (see
   Family\Exercise\ExerciseCurator) — a low-key escape hatch, not another
   button competing with the exercise cards themselves. */
.link-discreet {
  display: inline-block;
  margin-top: 12px;
  font-size: 0.9rem;
  color: var(--color-text-muted);
  text-decoration: underline;
}

/* Dev hub sort/filter bar — client-side only (see web/dev/index.php's inline
   script), so it never touches the server-rendered card list itself, just
   reorders/hides the existing .hub-card elements. */
.dev-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  margin-bottom: 20px;
  padding: 12px 16px;
  background: var(--color-surface);
  border-radius: var(--radius-md);
}

.dev-filters label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

.dev-filters select {
  font-size: 0.9rem;
  padding: 4px 6px;
}

.hub-card {
  position: relative;
  padding: 24px 20px 24px 26px;
  min-height: 100px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  text-decoration: none;
  color: var(--color-text);
  background: none;
  border-style: solid;
  border-width: 16px;
  border-image-source: url('../images/ui/button_basic.png');
  border-image-slice: 44 fill;
  border-image-width: 16px;
  border-image-repeat: stretch;
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
  transition: transform 0.15s ease;
}

/* Liseré : la couleur du sujet (--card-accent, posée inline par PHP —
   web/dev/index.php, web/child/exercise_hub.php/all_exercises.php) ou,
   pour un jeu (pas de sujet), --color-accent — remplace l'ancien
   border-left plat maintenant qu'un border-image commun occupe les 4
   côtés (border-image, une fois actif, couvre tout le cadre, on ne peut
   plus garder un côté en couleur unie via border-left-color). */
.hub-card::before {
  content: '';
  position: absolute;
  left: 6px;
  top: 10px;
  bottom: 10px;
  width: 6px;
  border-radius: 3px;
  background: var(--card-accent, var(--color-primary));
}

.hub-card-game::before {
  background: var(--color-accent);
}

.hub-card:active {
  transform: scale(0.97);
}

.hub-card-title {
  font-size: 1.15rem;
  font-weight: 700;
  /* Leaves room so long titles don't run under the subject badge, which is
     absolutely positioned over the top-right corner (see .hub-card-subject
     below) rather than laid out in flow — the badge needs to sit at a fixed
     corner regardless of how tall the title/status text stack gets. */
  padding-right: 64px;
}

.hub-card-level {
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

.hub-card-level-star {
  width: 16px;
  height: 16px;
  vertical-align: -3px;
  margin-left: 3px;
}

/* Subject badge (top-right) + colored left border ("liseré") — see
   docs/family_technical.md "Theme system": this is the `subject` metadata
   field (math/French/English...), a different concept from the cosmetic
   `theme` system (vehicles/farm_animals/toys), so it gets its own config
   (config/subjects.json) and its own class names here rather than reusing
   "theme" anywhere in code. Color comes from that config via a plain inline
   `style="border-left-color: ..."` set per card in PHP, not a fixed set of
   CSS classes, since new subjects can be added without a matching CSS rule.
   Added 2026-08-13 at the user's request, closing the "exercise theme/
   subject tagging" item that docs/family_design.md had deliberately listed
   as a later addition. */

.hub-card-subject {
  position: absolute;
  top: 12px;
  right: 16px;
  font-size: 0.75rem;
  font-style: italic;
  color: var(--color-text-muted);
  text-align: right;
}

/* Subject icon (web/assets/images/subjects/<code>.png) — a faint background
   illustration on the card's right side, purely decorative (the color
   liseré + name label above already carry the actual subject information,
   this is just a visual hint for non-reading children). Rendered as the
   *first* child in the card markup so later elements (title/level/subject
   spans) simply paint over it in normal DOM order, no z-index needed.
   Added 2026-08-16 at the user's request, child hub only — not shown on
   web/dev/index.php's cards. */
.hub-card-subject-icon {
  position: absolute;
  top: 8px;
  right: 8px;
  bottom: 8px;
  width: 60%;
  background-repeat: no-repeat;
  background-position: right center;
  background-size: contain;
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

/* Game card only (scoped past .hub-card-subject-icon's shared base rule,
   which all_exercises.php's plain .hub-card cards also use): the icon used
   to be absolutely positioned over the card (top/bottom:8px, 60% width)
   with the title/level/age text simply painting over it in normal flow —
   relying on .hub-card-title's own padding-right to dodge it was fragile
   (only the title got that padding; nothing guaranteed the icon's vertical
   reach never crossed into the level/age lines below it for every possible
   title length/wrap). Restructured as a real 2-column flex row instead —
   text stack on the left (.hub-card-game-text), the icon as a normal (not
   absolutely-positioned) flex item on the right sized by its own
   width/height — so overlap is impossible by construction, not by
   arithmetic. */
.hub-card-game {
  flex-direction: row;
  justify-content: space-between;
  gap: 10px;
}

/* Relies on .hub-card-game's own align-items:stretch (the flex default —
   deliberately NOT overridden to flex-start here, which was tried first and
   broke this: a top-aligned text column is only as tall as its own content,
   so justify-content:space-between below had no extra space to distribute
   and .hub-card-age never actually reached the card's bottom edge). Stretch
   makes this column match the row's height (set by the taller icon), so
   the title lands at the very top and .hub-card-age at the very bottom —
   level with the accent bar's own top/bottom (.hub-card::before). */
.hub-card-game-text {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* Without this a flex child's default min-width:auto keeps it as wide as
     its longest unbreakable word, which could push the icon column out
     past the card's own edge instead of wrapping the title text first. */
  min-width: 0;
}

.hub-card-game-main {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.hub-card-game .hub-card-title {
  padding-right: 0;
}

/* No longer inside the <a> in normal flow like .hub-card-title/-level —
   overrides the shared .hub-card-subject-icon rule's absolute positioning
   entirely (top/right/bottom/width/opacity). Sized/rounded per the user's
   own mockup (2026-09-14): a real thumbnail-sized image with rounded
   corners, not a small flat icon — background-size:cover since these
   game-icon assets are already filled edge-to-edge scene art (sand/grass/
   walls behind the vehicle), not a transparent glyph that needs
   letterboxing via :contain. */
.hub-card-game .hub-card-subject-icon {
  position: static;
  flex: 0 0 auto;
  width: 110px;
  height: 90px;
  border-radius: var(--radius-md);
  background-size: cover;
  background-position: center;
}

/* Restores the shared .hub-card-subject-icon rule's default-faded/
   hover-to-full-opacity behavior, dropped by mistake when this icon was
   restructured into a static-layout thumbnail above (opacity:1 was
   hardcoded there, which both flattened the default fade and made the
   hover transition a no-op since it was already at full opacity). */
.hub-card-game:hover .hub-card-subject-icon,
.hub-card-game:focus-visible .hub-card-subject-icon {
  opacity: 1;
}

/* Small on purpose ("vraiment en petit" — user request) — sits inside the
   button as the text stack's last line, pinned to the bottom via
   .hub-card-game-text's own justify-content:space-between, never touching
   the icon column since that's a separate flex item entirely (see above),
   not an absolutely-positioned layer text could ever cross. */
.hub-card-age {
  font-size: 0.65rem;
  color: var(--color-text-muted);
}

/* Exercise card v5 (2026-09-10): 2/3-width rows, alternating left/right
   (every other row flush against its own side, no shared gutter), with a
   costumed character at a fixed true size (250px wide, see
   .hub-card-character) — its own box height is independent of the card's,
   so shrinking the card never shrinks the character (v4's mistake: tying
   the character's box height to the card's made it shrink along with a
   shorter card, since background-size:contain was then height-limited).
   The character bleeds upward past the card's top edge as needed and its
   feet poke a little past the bottom edge; horizontally it stays flush
   with the card's own left edge, never bleeding into the page margin. At
   the opposite end, the subject icon (flag/calculator/globe/...) at 40%
   opacity with its subject name printed underneath as a caption.
   Deliberately its own class set rather than variants of .hub-grid/
   .hub-card — those stay exactly as they are for the games section,
   web/child/all_exercises.php and web/dev/index.php, none of which this
   redesign covers. */
.hub-grid-exercises {
  display: flex;
  flex-direction: column;
  /* Wider than a plain card gap (was 24px) — the character art (see
     .hub-card-character) bleeds above/below each card, so a tighter gap
     let neighbouring rows crowd each other. */
  gap: 48px;
  /* Clearance so the first row's character (taller than its own card, see
     .hub-card-character) doesn't bleed up into the section title above it. */
  margin-top: 60px;
}

.hub-card-exercise {
  position: relative;
  display: flex;
  align-items: center;
  width: 66%;
  height: 134px;
  text-decoration: none;
  color: var(--color-text);
  background: none;
  border-style: solid;
  border-width: 16px;
  border-image-source: url('../images/ui/button_basic.png');
  border-image-slice: 44 fill;
  border-image-width: 16px;
  border-image-repeat: stretch;
  border-radius: var(--radius-md);
  background-color: var(--color-bg);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
  /* Visible (not hidden) so the character (see .hub-card-character) can
     bleed past the card's top edge and its feet past the bottom edge. */
  overflow: visible;
  transition: transform 0.15s ease;
}

/* Liseré : voir la même note sur .hub-card::before — --card-accent (posé
   inline par web/child/exercise_hub.php) remplace l'ancien border-left
   plat. Offset un peu plus généreux ici (border-image plus épais que sur
   .hub-card) pour rester visuellement à l'intérieur du cadre. */
.hub-card-exercise::before {
  content: '';
  position: absolute;
  left: 8px;
  top: 14px;
  bottom: 14px;
  width: 6px;
  border-radius: 3px;
  background: var(--card-accent, var(--color-primary));
}

/* "Calés ou à gauche ou à droite" — odd rows flush against the container's
   left edge, even rows flush against its right edge, every other row. */
.hub-grid-exercises .hub-card-exercise:nth-child(odd) {
  align-self: flex-start;
}

.hub-grid-exercises .hub-card-exercise:nth-child(even) {
  align-self: flex-end;
}

.hub-card-exercise:active {
  transform: scale(0.97);
}

/* Fixed box size, deliberately NOT derived from the card's own height —
   tying it to the card (top:0/bottom:-20px with height:auto) squashed the
   character down every time the card got shorter, since
   background-size:contain is limited by whichever box dimension runs out
   first (found the hard way: shrinking the card by a third visibly shrank
   the character too, which wasn't the intent). 220px is the box height
   that read right against a 250px-wide character during design — kept as
   a fixed constant here so a future card-height tweak never resizes the
   character again; it bleeds upward past the card's top edge as needed
   (into the row above, not literally full natural size — a truly
   unclipped 250px-wide character needs ~420px of box height given these
   costumes' aspect ratios, which bled 2+ rows up and was worse) plus the
   feet poking past the bottom. */
.hub-card-character {
  position: absolute;
  left: 0;
  /* -20px shifted to -50px (30px further down) per request. */
  bottom: -50px;
  width: 250px;
  height: 220px;
  background-repeat: no-repeat;
  background-position: bottom center;
  background-size: contain;
  pointer-events: none;
}

.hub-card-exercise-body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  width: 100%;
  height: 100%;
  padding: 10px 118px 10px 260px;
  overflow: hidden;
}

.hub-card-exercise-body .hub-card-title {
  padding-right: 0;
  line-height: 1.2;
}

/* "avec <enfant>" caption, printed under the exercise title — names the
   character randomly drawn per-card (see exercise_hub.php's per-exercise
   $character draw), later reused as the same character shown top-left on
   that exercise's own play.html. */
.hub-card-character-caption {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-text-muted);
}

/* Hidden here — the subject name now lives in .hub-card-subject-caption
   instead (under the endcap icon), not as a second line under the title. */
.hub-card-exercise-body .hub-card-subject {
  display: none;
}

/* Right-end cluster: the subject's own icon (flag/calculator/globe/...)
   at 40% opacity with its name printed underneath as a plain caption —
   one uniform treatment for every subject, language ones included (their
   "icon" already is the flag, so no separate opaque badge is needed on
   top of it any more). */
.hub-card-subject-end {
  position: absolute;
  top: 50%;
  right: 16px;
  transform: translateY(-50%);
  width: 90px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  pointer-events: none;
}

.hub-card-subject-end-icon {
  width: 85px;
  height: 85px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  opacity: 0.4;
  transition: opacity 0.15s ease;
}

/* Same hover-to-full-opacity treatment as .hub-card-game's own subject icon
   (see that rule above) — on hover/focus the faded icon goes to full
   opacity instead of staying dimmed. */
.hub-card-exercise:hover .hub-card-subject-end-icon,
.hub-card-exercise:focus-visible .hub-card-subject-end-icon {
  opacity: 1;
}

.hub-card-subject-caption {
  font-size: 0.75rem;
  font-style: italic;
  color: var(--color-text-muted);
  text-align: center;
}

/* Shown on a game card when web/assets/js/exercise_launch.js's log_start.php
   call is rejected (insufficient balance, unknown/unpublished game) — unlike
   an exercise, a failed game launch must actually block navigation (see that
   file's comment), so the child needs some feedback instead of just nothing
   happening on tap. */
.hub-card-blocked-toast {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 10px;
  background: rgba(58, 46, 42, 0.9);
  color: #fff;
  font-size: 0.95rem;
  font-weight: 600;
  border-radius: var(--radius-md);
}

.hub-card-blocked-toast-star {
  width: 18px;
  height: 18px;
  vertical-align: -3px;
  margin: 0 2px;
}

/* Interaction/mechanic type — dev hub only, never shown to parents/children
   (see CLAUDE.md "Language convention": this is an internal/code-facing
   label, not runtime content, so it stays in English and only appears where
   web/dev/index.php renders it). */
.hub-card-interaction {
  position: absolute;
  bottom: 10px;
  right: 16px;
  font-size: 0.7rem;
  font-style: italic;
  color: var(--color-text-muted);
  opacity: 0.65;
}

/* Per-exercise token-gain multiplier badge ("xN", FUN-32) — dev-only ledger
   in config/exercise_multipliers.json (see web/dev/multipliers.php), only
   rendered on the card when set above the implicit default of 1. Visual is
   deliberately plain per the issue ("on ajustera le visuel plus tard") —
   black text in a yellow circle, bottom-right corner. On the dev hub, where
   .hub-card-interaction also sits bottom-right, that label is shifted left
   inline (see web/dev/index.php) when both are present on the same card. */
.hub-card-multiplier {
  position: absolute;
  bottom: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #ffd60a;
  color: #000;
  font-size: 0.75rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Admin/parent-dashboard tables — plain and functional on purpose, see
   parental_controls_brief.md ("not meant to be polished"). */

.admin-table-wrap {
  overflow-x: auto;
  margin-bottom: 16px;
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--color-surface);
  border-radius: var(--radius-md);
  overflow: hidden;
  font-size: 0.95rem;
}

.admin-table th,
.admin-table td {
  padding: 10px 12px;
  text-align: left;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}

.admin-table th {
  background: var(--color-bg);
  font-weight: 700;
  color: var(--color-primary-dark);
}

.admin-table tr:last-child td {
  border-bottom: none;
}

.inline-form {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 2px 0;
}

.admin-table td.stat-cell {
  text-align: center;
}

/* Spacer column between success/abandon rate in the subject-stats table
   (FUN-40) — reads as a divider strip running the full height of the
   table, its border-bottom left un-overridden so each row's divider line
   still runs through it instead of leaving a gap. The header cell keeps
   the header row's own background like every other <th>; the body cells
   are filled plain white instead (2026-09-16 user request) rather than
   also carrying that background, so the divider reads as a clean gap
   against the row's own rateBarStyle() fill. */
.admin-table th.stat-spacer {
  padding: 0;
  width: 24px;
}

.admin-table td.stat-spacer {
  padding: 0;
  width: 24px;
  background: var(--color-surface);
}

/* Activity-history chart (FUN-42): period/subject filter controls above a
   hand-rolled SVG line chart (no charting library — see
   renderHistoryChart() in child_detail.php). Same green/red as the
   subject-stats table's rateBarStyle() rgb() values, kept as CSS classes
   here instead since these are strokes/fills, not background bars. */
.history-chart-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 12px;
}

.history-chart-wrap {
  background: var(--color-surface);
  border-radius: var(--radius-md);
  padding: 12px;
  margin-bottom: 16px;
}

.history-chart {
  width: 100%;
  height: auto;
  display: block;
}

.history-chart-grid {
  stroke: var(--color-border);
  stroke-width: 1;
}

.history-chart-axis-label {
  fill: var(--color-text-muted, #8a8378);
  font-size: 10px;
}

.history-chart-line-success {
  fill: none;
  stroke: rgb(76, 175, 80);
  stroke-width: 2;
}

.history-chart-line-abandon {
  fill: none;
  stroke: rgb(244, 67, 54);
  stroke-width: 2;
}

.history-chart-line-success-dot {
  fill: rgb(76, 175, 80);
}

.history-chart-line-abandon-dot {
  fill: rgb(244, 67, 54);
}

.history-chart-legend {
  display: flex;
  gap: 16px;
  margin-top: 4px;
  font-size: 0.85rem;
}

.history-chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.history-chart-legend-swatch {
  display: inline-block;
  width: 14px;
  height: 3px;
  border-radius: 2px;
}

/* .history-chart-line-success/-abandon set SVG fill/stroke for the chart's
   own <polyline> elements, which have no effect on a plain <span> — the
   legend swatch needs its own background-color instead. */
.history-chart-legend-swatch.history-chart-line-success {
  background: rgb(76, 175, 80);
}

.history-chart-legend-swatch.history-chart-line-abandon {
  background: rgb(244, 67, 54);
}

.history-day {
  background: var(--color-surface);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  margin-bottom: 8px;
}

.history-day summary {
  cursor: pointer;
  font-size: 0.95rem;
}

.history-day summary::marker {
  color: var(--color-primary-dark);
}

.history-day[open] summary {
  margin-bottom: 8px;
}

/* child_detail.php's per-day "Supprimer la journée" button, above that
   day's own entries table. */
.history-day-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 8px;
}

.inline-form input[type="number"],
.inline-form input[type="text"],
.inline-form select {
  min-height: 36px;
  padding: 4px 8px;
  font-size: 0.9rem;
  border-radius: 8px;
  border: 2px solid var(--color-border);
  font-family: inherit;
  width: 110px;
}

/* Wider than the plain 110px input width above — a period/subject select's
   own option text ("Dernière semaine", "Depuis le début"...) was cramped at
   that width, unlike the numeric +/- amount field it was sized for. */
.inline-form select {
  width: 210px;
}

/* child_detail.php's dropdown options sat flush against the widget's own
   edges by default — a little padding gives the label text room to
   breathe (user request 2026-09-16). */
.inline-form select option {
  padding: 6px 10px;
}

.btn-sm {
  min-height: 36px;
  padding: 4px 12px;
  font-size: 0.9rem;
  border-radius: 8px;
  /* Smaller than .btn's default 18px — at full size the border-image eats
     most of a button this small (dense admin rows: +100/+1000/réinitialiser). */
  border-width: 9px;
  border-image-width: 9px;
}

.badge-inactive {
  color: var(--color-text-muted);
  font-style: italic;
}

.subject-factor-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}

.subject-factor-row label {
  width: 110px;
  font-weight: 700;
}

.subject-factor-row output {
  width: 36px;
  text-align: right;
  font-weight: 700;
}

/* Legal pages (web/legal/cgu.php, confidentialite.php) — plain long-form
   text, not a touch-target UI like the rest of the app. */

.legal-wip-banner {
  background: #fff3cd;
  border: 2px solid #f2994a;
  border-radius: var(--radius-md);
  padding: 12px 16px;
  font-weight: 700;
  color: #7a4a12;
  margin-bottom: 24px;
}

.legal-content {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: 32px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
  line-height: 1.6;
}

.legal-article-heading {
  font-size: 1.1rem;
  color: var(--color-primary-dark);
  margin: 28px 0 8px;
}

.legal-article-heading:first-child {
  margin-top: 0;
}

.legal-content blockquote {
  margin: 8px 0;
  padding: 8px 16px;
  border-left: 4px solid var(--color-border);
  color: var(--color-text-muted);
  font-style: italic;
}

/* Legal-links bar — kept visually minimal (small text, muted) so it doesn't
   compete with the "top-bar" it sits next to. Most pages place it above the
   app title; the home page (web/index.php) places it at the bottom instead. */
.legal-links-bar {
  display: flex;
  justify-content: center;
  gap: 16px;
  font-size: 0.8rem;
  padding: 6px 0;
  margin-top: 32px;
  margin-bottom: 12px;
}

.legal-links-bar a {
  color: var(--color-text-muted);
  text-decoration: underline;
}

/* avatar_picker.php's gallery tiles: in mode=change each tile is its own
   <form> (a real POST straight to change_photo.php, no JS needed) wrapping
   a .child-card-styled <button> — display:contents drops the form from the
   box tree so the button itself is the .child-grid item, same as the plain
   <a class="child-card"> used everywhere else this grid appears. */
.avatar-picker-tile-form {
  display: contents;
}

/* avatar_picker.php's own grid: the default .child-grid/.child-avatar sizing
   (120px avatar, 160px min column) is tuned for a handful of child-picker
   cards, not a dense 19-20-tile-per-group gallery — at real window widths
   this crowded tiles enough to read as overlapping (2026-09-13 user report).
   Smaller avatar + tighter card padding/gap/min column fixes the density
   without touching .child-grid/.child-card anywhere else they're used.
   Went through 3 sizing passes the same day: 72px avatar (still too tight)
   -> 56px (turned out to be judged against a browser-cached stylesheet,
   so untrustworthy — see docs/implementation_log.md) -> the real rendered
   56px read as genuinely too small once the cache was cleared, so scaled
   every dimension here back up by ~1/3 to the current values. Border-image
   still thinned from .child-card's default 20px (same trick as .btn-sm),
   just less aggressively than the 56px pass needed. */
/* Overrides .child-grid's own display:grid — CSS Grid always fills a
   partial last row starting from the first column (see CLAUDE.md's
   "Wrapped item rows must center every row" rule), which is exactly the
   "avatars pas centrés" regression a user hit here (2026-09-16): this page
   was never migrated to the flex/justify-content:center fix already
   applied everywhere else (FUN-13) when it was first built. Tiles need a
   fixed width now instead of minmax(...,1fr)'s auto-stretch — 140px comfortably
   fits the 94px avatar plus this rule's own padding/border (136px), with a
   little slack. */
.avatar-picker-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 11px;
}

.avatar-picker-tile {
  font: inherit;
  width: 140px;
  padding: 11px 8px;
  border-width: 13px;
  border-image-width: 13px;
}

.avatar-picker-tile .child-avatar {
  /* Bigger than the 75px this started at (user request) — kept just under
     the tile's own content budget (112px column min minus this rule's
     16px horizontal padding) so the tile box itself doesn't grow. */
  width: 94px;
  height: 94px;
  margin: 0 auto 8px;
}

.avatar-picker-tile .child-name {
  font-size: 1rem;
  line-height: 1.2;
}

/* Referral-driven avatar unlock ladder (Notion "Etape v2.5 (parrainage)"
   Ordre 7.5) — a <div>, not a <button>/<form> like the unlocked tiles
   above, since a locked avatar must never be selectable. */
.avatar-picker-tile-locked {
  filter: grayscale(1);
  opacity: 0.55;
  cursor: default;
}

.avatar-picker-unlock-hint {
  display: block;
  margin-top: 4px;
  font-size: 0.7rem;
  line-height: 1.2;
  color: var(--color-text-muted);
}

/* avatar_picker.php's tranche switcher (2026-09-17 simplification): plain
   GET links (?tranche=...), not JS, so the page reload stays the single
   source of truth for which tranche's tiles are shown/lockable. */
.avatar-picker-tranche-switch {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 4px 0 20px;
}

.avatar-picker-tranche-current {
  opacity: 1;
  font-weight: 700;
  text-decoration: underline;
}

/* change_photo.php's "anciennes photos" section: a handful of past-photo
   thumbnails (capped at 8, see ChildProfileRepository::MAX_PHOTO_HISTORY),
   each its own <button class="child-avatar">. Flex + centered wrap, not
   .child-grid's CSS grid, per CLAUDE.md's "wrapped item rows" rule — with a
   count this small an uneven last row is common and must still center. */
.photo-history-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 16px;
  margin: 16px 0;
}

.photo-history-tile {
  font: inherit;
  padding: 0;
  border: 3px solid transparent;
  cursor: pointer;
  transition: transform 0.15s ease, border-color 0.15s ease;
}

.photo-history-tile:hover,
.photo-history-tile:focus-visible {
  border-color: var(--color-accent);
}

.photo-history-tile:active {
  transform: scale(0.94);
}

/* addchild.php / dashboard.php's create-child form: the catalog-avatar
   preview shown after picking one in the popup (hidden until then). */
.selected-avatar-preview {
  align-items: center;
  gap: 12px;
  margin-top: 8px;
}

/* An author rule always beats the UA [hidden]{display:none} regardless of
   specificity (origin outranks specificity in the cascade) — so `display`
   only gets set here, scoped to :not([hidden]), instead of unconditionally
   above, or toggling the hidden attribute would visibly do nothing. */
.selected-avatar-preview:not([hidden]) {
  display: flex;
}

.selected-avatar-preview img {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
}

/* curriculum_acquisition.php's page subtitle — cites the official Bulletin
   officiel texts (data/curriculum/_sources.json) the programme data below
   was extracted from, with their publication dates (user request
   2026-09-16). Small/muted, same treatment as .curriculum-codes. */
.curriculum-sources {
  margin: -20px 0 32px;
  font-size: 0.8rem;
  color: var(--color-text-muted, #8a8378);
}

/* curriculum_acquisition.php — read-only "acquisition du programme
   officiel" view: matière > domaine > sous-domaine > exercice, each with a
   small icon timeline of the child's own attempts. --subject-color is set
   inline per <section> from config/subjects.json (same color used
   elsewhere for that subject's badges). */
.curriculum-subject {
  background: var(--color-surface);
  border-radius: var(--radius-md);
  border-left: 6px solid var(--subject-color, var(--color-border));
  padding: 12px 16px;
  margin-top: 24px;
  margin-bottom: 16px;
}

.curriculum-subject-title {
  margin: 0 0 8px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 1.7rem;
  color: var(--subject-color, var(--color-primary-dark));
}

.curriculum-subject-icon {
  width: 36px;
  height: 36px;
  object-fit: contain;
}

.curriculum-domain {
  margin: 12px 0;
}

.curriculum-domain-title {
  margin: 0 0 6px;
  color: var(--color-primary-dark);
}

.curriculum-subdomain {
  margin: 8px 0 8px 12px;
  padding: 8px 12px;
  background: var(--color-bg);
  border-radius: var(--radius-sm, 8px);
}

.curriculum-subdomain-title {
  margin: 0 0 4px;
  font-size: 1.15rem;
}

/* Raw Notion curriculum-object codes, shown temporarily until the
   programme-officiel pipeline no longer needs them surfaced here (see
   child_detail.php's link-in comment). */
.curriculum-codes {
  margin: 0 0 8px;
  font-size: 0.78rem;
  color: var(--color-text-muted, #8a8378);
  font-family: monospace;
}

/* History icons sit right after the exercise name, not pushed to the far
   right (user request 2026-09-16) — plain flex-start with a small gap
   instead of justify-content:space-between. */
.curriculum-exercise-row {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  padding: 4px 0;
  flex-wrap: wrap;
}

.curriculum-exercise-label {
  font-weight: 600;
}

/* Placeholder row for a "gap" (⚠️ no exercise built yet) or
   "hors_perimetre" (deliberately excluded) sous-domaine — no history to
   show, so it's a single label-only row, visually muted since it's
   informational rather than actionable. */
.curriculum-placeholder-row .curriculum-exercise-label {
  font-weight: 400;
  font-style: italic;
  color: var(--color-text-muted, #8a8378);
}

.curriculum-hors-perimetre-row {
  cursor: help;
}

.curriculum-hors-perimetre-row .curriculum-exercise-label {
  text-transform: uppercase;
  font-size: 0.8rem;
  letter-spacing: 0.03em;
}

.curriculum-history {
  display: flex;
  flex-wrap: nowrap;
  gap: 1px;
}

/* Plain colored emoji (✅ ❌ ✖️) rather than a hand-built colored badge —
   user request 2026-09-16 ("on n'a pas d'emoji de couleur ?"). Tightened
   further the same day so a long attempt history still fits on the
   exercise's own line instead of wrapping. */
.curriculum-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 14px;
  height: 16px;
  font-size: 0.75rem;
  cursor: default;
}
