/*
   PredictionPoolCreator theme — "measures".

   The visual language borrows from measuring instruments: tick marks, tabular figures,
   layered grounds. That structure has survived four palettes. The first went monochrome and
   read as gloomy; the second gave it real chroma and read as loud; the third softened the
   second; this one changes something the first three never did — see THE LEAD COLOUR IS THE
   INK, below the tokens. It is a rule about roles, not about hue, and it is the rule that
   stops the next palette from repeating the same fault in another colour.

   ⚠️ Bootstrap is no longer linked. This file now carries its own reset — section 1b, right
   after the tokens — and the reason for the change is not weight but authority: Bootstrap
   sized every heading this file did not explicitly claim, so `--step-*` was never the law.
   It was measured doing real damage: on the leaderboard, an unclassed `h3` came out at 28 px
   over its own `h2` at 24 px. A stylesheet that quietly outranks the theme is worse than a
   heavy one.
*/

/* ==========================================================================
   0. DISPLAY TYPEFACE — third-party, and the licence travels with it.

   ⚠️ THIRD-PARTY FONT SOFTWARE, NOT WRITTEN HERE.
   Bricolage Grotesque, © 2022 The Bricolage Grotesque Project Authors
   (https://github.com/ateliertriay/bricolage), licensed under the SIL Open Font License
   version 1.1. Taken from the @fontsource-variable/bricolage-grotesque npm package pinned at
   5.3.0. The full licence sits beside the files at wwwroot/fonts/LICENSE-bricolage-grotesque.txt
   — the OFL requires it to travel with the font, so that file is not optional and must not be
   tidied away. The OFL also forbids selling the font on its own and requires any modified version to
   be renamed; neither applies to using it as it is.

   Self-hosted rather than pulled from a CDN: no third-party request at runtime, nobody else
   learning who visits this site, and nothing that breaks when someone else's domain does.

   HEADINGS ONLY. Bricolage Grotesque has real character — that is why it was chosen — and a
   face with character is tiring as running text at 15px. It sets the headings, the wordmark
   and the large figures; body text stays on the system UI stack. An earlier attempt used one
   neutral grotesque for both, which was right for a neutral face and is wrong for this one:
   a display face wants a quiet partner, not a twin.

   TWO SUBSETS, split by unicode-range. Latin (41 kB) covers French including œ, which lives
   at U+0152-0153 and is inside the latin subset rather than latin-ext. Latin Extended
   (18 kB) is downloaded ONLY if a character needs it — a participant named Grzegorz or
   Ľubica — so it costs nothing for the pools that never do. Verified: on every current page
   the second face reports `unloaded` and transfers nothing.

   ⚠️ If you go to verify that yourself, do NOT measure with canvas `measureText`: naming the
   family there makes the browser load every face in it, latin-ext included, and the download
   you then observe is the one your own probe caused. Read `document.fonts` and the resource
   timeline instead. This cost a confused half hour once.

   The weight axis runs 200 to 800; this theme uses 600 and 700. Ranges come from the
   package's own CSS rather than being guessed.

   ⚠️ format('woff2'), not format('woff2-variations') as the upstream CSS ships. The second is
   a transitional spelling from before variable fonts were in the spec: a browser that does not
   recognise the format string skips the whole src and falls back silently, which is the worst
   way for this to fail. The axis is declared by `font-weight: 200 800` below, never by that
   word.
   ========================================================================== */
@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('fonts/bricolage-latin.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('fonts/bricolage-latin-ext.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   THE DATA FACE — the second third-party font, and the one this theme could
   least afford to leave to chance.

   ⚠️ THIRD-PARTY FONT SOFTWARE, NOT WRITTEN HERE.
   JetBrains Mono, © 2020 The JetBrains Mono Project Authors
   (https://github.com/JetBrains/JetBrainsMono), licensed under the SIL Open Font License
   version 1.1 — the same licence as the display face, not the Apache 2.0 the project carried
   before version 2.0. Taken from @fontsource-variable/jetbrains-mono pinned at 5.3.0. The full
   licence sits beside the files at wwwroot/fonts/LICENSE-jetbrains-mono.txt.

   ⚠️ The Bricolage licence was renamed from LICENSE.txt to LICENSE-bricolage-grotesque.txt in
   the same change. With one font a generic name was unambiguous; with two it named neither.
   Both files are required to travel with their font and neither is tidying to be undone.

   WHY AT ALL. --font-data was `ui-monospace, 'Cascadia Mono', 'SF Mono', Menlo, Consolas,
   monospace`: a different face on Windows, on macOS, on Android, each with its own advance
   width. On a theme whose entire premise is aligned figures, the one family that carries every
   figure on the site was the only thing not being chosen. It already cost a layout — see the
   note on .factlist--prose, where a row fitted with zero slack on the machine it was measured
   on and could not be trusted anywhere else.

   WHY THIS ONE RATHER THAN IBM PLEX MONO, which was the first choice: Plex Mono ships only as
   static weights, and this theme uses three of them (400 for figures, 600 for the countdown and
   the fact values, 700 where .num sits inside .board__rank). That is three files per subset.
   JetBrains Mono has a variable axis, so it is ONE file per subset covering all three — the
   same mechanism already proven for the display face, rather than a second one beside it.

   Same two subsets, same split. The unicode-ranges below are byte-for-byte those of Bricolage
   above, because both packages subset on the same boundaries: latin (40 kB) and latin-ext
   (15 kB), the second downloaded only if a name needs it.

   ⚠️ format('woff2'), not the format('woff2-variations') the upstream CSS ships — see the note
   on the display face. The axis is declared by `font-weight: 100 800`, never by that word.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'JetBrains Mono';
  src: url('fonts/jetbrains-mono-latin.woff2') format('woff2');
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ==========================================================================
   1. TOKENS — the palette and type scale live here and nowhere else.

   ONE THEME, IMPOSED. There is deliberately no dark variant.

   Committing to one look halves the surface where a contrast bug can hide — the first
   attempt shipped two of them, one per theme. ⚠️ The *reason* has changed even though the
   rule has not: the old justification was that a dark version contradicts a cream-paper
   metaphor. The ground is no longer cream, so that argument is gone and a dark variant is
   now merely unbuilt rather than wrong. Building one means re-measuring every pair below
   against a second set of grounds; it is a project, not a media query.

   `color-scheme: light only` is what makes it stick: without it the browser would still
   auto-darken date pickers, selects and scrollbars on a machine set to dark mode, leaving
   dark controls sitting on a light page.

   ---------------------------------------------------------------------------
   CARDS LIFT OFF THE PAGE. WHAT DOES THE LIFTING HAS CHANGED ONCE — READ THIS
   BEFORE TOUCHING --paper.

   Two palettes ago the raised surface and the page were both near-white: 1.06 between them,
   which is to say the two planes were one plane, with cards held together by a border
   measured at 1.23 against their own fill. The page had no structure. Tinting the ground
   fixed it; then --shadow arrived and did the job on its own, so the ground was lightened
   back to a near-white and the gap fell to 1.03 — carried entirely by the shadow.

   The chart paper takes that gap back to 1.17 without spending anything: every text token
   below is measured against --paper-sunk, which is darker than --paper, so lightening or
   darkening the page inside that range moves no ratio at all.

   ⚠️ The rule to keep is still not "the ground must be tinted". It is: A PANEL MUST BE
   SEPARATED BY SOMETHING. It is now separated twice over — the shadow, and 1.17 of real
   luminance — which is the point: remove --shadow from .card-p and the page keeps its
   structure, where before it went straight back to washed out with nothing in the contrast
   numbers to warn you. Plane separation carries no WCAG threshold, so it has to be held by
   something a measurement can see.

   ---------------------------------------------------------------------------
   THREE ROLES PER HUE. This extends the two-role split the theme already had, because a
   soft palette is exactly where two roles stop being enough: one gentle tone used for
   surfaces, marks and text at once is illegible in all three places.

     --*-wash    pastel   surfaces only. Never a mark, never behind text.
     --*         medium   data marks and control boundaries. Measured >= 3.0 (WCAG 1.4.11).
     --*-deep    deep     anything carrying text. Measured >= 4.5.

   ⚠️ The 3.0 threshold applies to control boundaries and to marks that carry data — an
   input border, a chart bar. It does NOT apply to two adjacent surfaces, nor to a purely
   decorative divider. Forcing it there would turn the page into a chessboard, so --rule
   stays deliberately soft and is never the only thing delineating a control.

   ---------------------------------------------------------------------------
   THE LEAD COLOUR IS THE INK. This is the change that separates this palette from the three
   before it, and it is worth stating as a rule because it is a rule about ROLES.

   The palette before this one had a green lead colour, and --accent-deep appeared nineteen
   times in this file: every eyebrow, every card title and its band, the countdown figures,
   the fact-list values, the table headers, the rank and total in the leaderboard, the podium
   score, the selected chip, the sort arrow, the quote rule, the gate. When the accent carries
   half the non-body text on the page, it has stopped accenting anything. Nothing stood out
   because everything did.

   So there is no accent hue any more. The brand IS the ink: #0A0F2C, and everything is
   engraved in it — text, headings, filled buttons, the wordmark. Exactly one warm colour
   exists on the site, and it is called --signal because that is all it is allowed to do.

   ⚠️ --signal AND --signal-deep MAY APPEAR ONLY WHERE SOMETHING MEASURES OR CHANGES:
     · the marker sliding along the countdown's rule;
     · the edge of a chosen answer, and of the reader's own row in the leaderboard;
     · the :focus-visible ring;
     · the reference-date mark on a chart, and the "not yet known" state in the admin.
   They may not be a heading, a section ground, or the default colour of a figure. This is
   checkable in review, which is the whole reason for phrasing it as a list.

   --signal-wash is exempt, and deliberately so: a pale tint carries no emphasis, so it is
   free to mark rows that still need attention. It is the saturated pair that is rationed.

   ⚠️ THE SIGNAL IS MAGENTA, AND THE CHOICE IS NOT DECORATIVE. On a printed nautical chart,
   magenta is the one overprinted colour, reserved for lights, cautions and special marks —
   everything the reader must not miss on a sheet otherwise engraved in blue-black on pale
   paper. That is the exact job it has here, on exactly that ground.

   It also measures better than the warm alternative it replaced: 4.15 on --paper-sunk against
   an orange's 3.36. The focus ring is drawn in this token, so the extra headroom is not spare.

   Red stays free for what is actually a fault. --crit #A5261D sits at hue 4°, the signal at
   332° — brick against pink, at different saturations, and the two never occupy the same role:
   red is text and destructive buttons, the signal is a marker, an edge and a ring. "Still to
   score" and "this failed" must never read as the same thing.

   ---------------------------------------------------------------------------
   NEUTRALS FOLLOW THE GROUND'S TEMPERATURE. HUES THAT MEAN SOMETHING KEEP THEIRS.

   The ground is cool, around hue 215°. Every neutral leans the same way: the inks, the rules,
   --paper-sunk and the shadow tint all sit between 213° and 224°. The inks are the deep end
   of the brand hue itself rather than a grey that happens to lean blue, which is what makes
   the page read as one material.

   What does NOT follow: --signal and its family, the six data hues, --ok/--warn/--crit, and
   the medals. Gold is the clearest case — pulled toward the page's blue it stops reading as
   gold, and it is there to say "first", not to decorate. The medals are therefore the only
   values in this file that survived the repaint untouched, and they get away with it because
   they are measured against the white of the podium step rather than against the page.

   ⚠️ No data hue may come near the signal, and this is a constraint rather than a taste: a
   chart series painted in the colour that means "look here" turns an ordinary category into
   the thing being pointed at. --data-clay and --data-sand are the two warm series, both muted
   and both far from 332°. The hue the signal occupies is closed to the data range.

   Every value below was measured against every ground it can land on before it was painted,
   --paper-sunk included: it is the darkest ground that carries text, so it is the one that
   decides how dark a text token has to be. Re-measured from the file after writing, and both
   washes were checked as grounds in their own right — they are lighter than --paper-sunk, so
   it keeps its status as the binding one. 30 pairs, 0 failures.
   ========================================================================== */
:root {
    color-scheme: light only;
    /* Chart paper: a pale blue-grey, hue 215°. 1.17 from a white card, so the two planes are
     told apart by luminance as well as by the shadow. */
    --paper: #E9EEF6;
    --paper-raised: #FFFFFF;
    /* Wash behind zebra rows and track fills. It carries text, and it is the darkest ground
     in the theme, so it is what the text tokens are measured against. Both washes further
     down were checked against this claim and are lighter, so it still holds. */
    --paper-sunk: #DCE3EF;
    /* ⚠️ Not #0A0F2C, which is where this palette started and is still the bottom of its ramp
     (it tints the shadow, below). At that luminance — l11 — the hue is no longer perceptible:
     a heading set in it reads as black, and so does a filled button. Lifting the ink to l19
     keeps 12.68 on the binding ground, which is far more headroom than anything needs, and
     buys back the only thing that made it worth choosing a blue. */
    --ink: #131B4A; /* 14.05 on --paper, 16.36 on a card, 12.68 on --paper-sunk */
    --ink-soft: #414A66; /* 7.52 / 8.76 / 6.79 */
    --ink-faint: #525B75; /* 5.79 / 6.75 / 5.23 */

    --rule: #C8D2E2; /* decorative dividers and card edges. No threshold applies. */
    /* Control boundaries and chart axes. 3.58 on --paper, 4.17 on a card, 3.24 on
     --paper-sunk — that last one is the binding measurement, because an input is not
     guaranteed to sit inside a card and --paper-sunk is the darkest ground in the theme. */
    --rule-strong: #6E7D95;
    /* THE BRAND, i.e. everything that is painted rather than written: filled buttons, the
     chosen chip, the figures that used to be green. Same hue as the ink, four steps up the
     ramp — the ink is what a thing is written in, the brand is what it is painted in, and
     the two are the same colour at two luminances rather than two colours.

     ONE token, where the green needed three. At 6.32 on the binding ground it already clears
     the 4.5 text threshold, so there is nothing for a -deep variant to fix. If a future value
     is chosen lighter than this, the split comes back — that is what the three-role table
     above is for. */
    --brand: #2B4A9E; /* 7.00 on --paper, 8.15 on a card, 6.32 on --paper-sunk. White on it: 8.15 */
    --brand-strong: #22398A; /* hover of a filled surface — white on it, 10.39 */
    --brand-wash: #DCE5F3; /* card bands and neutral row hovers. Surfaces only. */
    --on-solid: #FFFFFF; /* the label on any filled surface — worst case 6.40, on --signal-deep */
    /* THE NEEDLE. Read the rationed-use list above before adding a usage.
     ⚠️ --signal is measured against --paper-sunk (3.36), not against --paper (3.73) or a card
     (4.34). It draws the :focus-visible ring, which appears around whatever the visitor
     happens to be tabbing through, so it has to clear the threshold on the darkest ground in
     the theme — and that is --paper-sunk. The focus ring is the one indicator that must never
     be marginal.
     It takes the signal rather than the brand on purpose: a ring in the brand blue, drawn
     around a control that is itself blue, reads as part of the control. */
    --signal: #C42A72; /* marks and the focus ring — 4.60 / 5.36 / 4.15 */
    --signal-deep: #AD1F5F; /* text — 5.74 on --paper, 6.69 on a card, 5.19 on --paper-sunk */
    --signal-strong: #8C1149; /* hover of a filled surface — white on it, 9.16 */
    --signal-wash: #FADEEA; /* exempt from the rationing — see the note above. */
    /* Data hues. Distinct enough to tell six categories apart, and each measured over 3.0 on a
     card so a bar is still a bar to someone who cannot separate the hues.
     ⚠️ Marks, and ONE text case: the figure in a stat tile, which .tile__value colours per
     column. That is legitimate only because 3.0 happens to be both the non-text threshold
     and the large-text one — and .tile__value is --step-2, exactly 24px, exactly the size at
     which large-text applies. Shrink that step and these figures silently need 4.5 instead.
     Anywhere smaller, use the -deep tokens. */
    --data-blue: #4C7DB8; /* 3.30 */
    /* ⚠️ Was --data-rose #B5567F, and it had to go: at hue 334° it sat two degrees from the
     signal. A category that happens to be the fourth one would then have been painted the
     colour that means "look here". Clay takes the warm end of the range the signal vacated. */
    --data-clay: #B0603A; /* 3.55 */
    --data-sand: #97722A; /* 3.42 */
    --data-violet: #7A6BC4; /* 3.44 */
    --data-teal: #2E8080; /* 3.60 */
    --data-slate: #64788F; /* 3.52 */
    /* Semantic, and text-bearing, so these are -deep values by nature. */
    /* Ratios below are --paper / card / --paper-sunk. */
    --ok: #0F6349; /* 6.21 / 7.24 / 5.61 */
    --warn: #71520A; /* 6.19 / 7.21 / 5.59 */
    --crit: #A5261D; /* 6.23 / 7.25 / 5.62 — white on it, 7.25 */
    /* Hover of a filled destructive surface, the counterpart of --brand-strong. It was once a
     hard-coded #9E2F23 tuned to a --crit two palettes ago, so it drifted in hue the moment the
     palette moved and nothing said so. */
    --crit-strong: #851C15; /* white on it, 9.66 */
    /* Medals, split into the same roles as the lead colour and for the same measured reason:
     gold-on-white fails as text whatever shade is chosen. The plain tokens draw the podium
     borders, the -deep ones carry the figures.
     The border tokens are also brought over 3.0 here, which they were not: a rank stripe is
     what tells first from second, so it carries meaning and the threshold applies to it.
     The -deep values are unchanged — they are measured against --paper-raised, which is
     still pure white. */
    --gold: #B98C30; /* 3.06 on a card — the podium step is --paper-raised */
    --gold-deep: #7A5A0B; /* 6.38 */
    --silver: #82959B; /* 3.12 */
    --silver-deep: #5C6A70; /* 5.60 */
    --bronze: #B37D52; /* 3.52 */
    --bronze-deep: #8A5326; /* 6.28 */
    /* ⚠️ The display face was a Palatino/Georgia serif stack. On Windows that resolved to
     Palatino Linotype over Segoe UI, which is the default pairing of a word processor — the
     single biggest reason the site read as homework rather than as a product. A grotesque
     over the same UI face reads as one voice instead of two.

     It is Bricolage Grotesque now — declared in section 0, self-hosted, OFL, headings only.
     The system stack stays behind it as a fallback, so a failed request degrades to what the
     machine has rather than to Times New Roman. It is also what shows during the swap. */
    --font-display: 'Bricolage Grotesque', 'Segoe UI Variable Display', 'Segoe UI Semibold', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif;
    --font-ui: 'Segoe UI Variable Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif;
    /* Self-hosted now — see section 0. The system stack stays behind it so a failed request
     degrades to whatever the machine has rather than to Courier, and it is also what shows
     during the swap. */
    --font-data: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', 'SF Mono', Menlo, Consolas, monospace;
    --step--1: 0.8125rem;
    --step-0: 1rem;
    --step-1: 1.1875rem;
    --step-2: 1.5rem;
    /* Fluid from step 3 upwards, because these are the only sizes that cannot be right at every width.
     --step-5 is the hero title: fixed at 3.75rem it rendered 60px on a 375px phone, which put
     "Devine notre bebe." on two lines that filled the screen before a single word of content.
     The smaller steps stay fixed — body text does not want to change size with the window.

     Each preferred value keeps a rem term next to the vw term on purpose: a clamp() built from vw
     alone stops responding to the reader's own font-size setting, so a heading would ignore someone
     who enlarges text in their browser. */
    --step-3: clamp(1.5rem, 1.27rem + 0.97vw, 2rem);
    --step-4: clamp(1.75rem, 1.3rem + 1.94vw, 2.75rem);
    --step-5: clamp(2.25rem, 1.57rem + 2.91vw, 3.75rem);
    --sp-1: 0.375rem;
    --sp-2: 0.75rem;
    --sp-3: 1.125rem;
    --sp-4: 1.75rem;
    --sp-5: 2.5rem;
    --sp-6: 4rem;
    /* ⚠️ One radius of 4px used to serve everything, from a "small radius and crisp rules: a
     record card, not a SaaS panel" reading of the theme. That reading is precisely what made
     the site look like a printed form. Two radii now, because a control and a panel are not
     the same object: a 16px card holding 16px buttons reads as mush, and a 4px card holding
     anything reads as a table cell.
       --radius       controls — buttons, inputs, chips, the small stuff you click
       --radius-lg    panels — cards, tiles, anything that holds other things
       --radius-pill  fully round: choice chips and status pills */
    --radius: 10px;
    --radius-lg: 16px;
    --radius-pill: 999px;
    /* Cards lift off the ground rather than being cut out of it. Two layers on purpose: a 1px
     contact shadow that gives the card an edge, and a wide, very soft one that gives it height.
     A single blurred shadow does one or the other and reads as a smudge.
     Tinted with the bottom of the ink ramp — rgb(10, 15, 44), the #0A0F2C this palette was
       built around. It is too dark to write with and exactly right to cast with: a shadow is
       the one place where a colour is only ever seen at a few percent opacity, so the loss of
       perceptible hue that disqualified it everywhere else costs nothing here.
       ⚠️ Tint, never black. A neutral grey shadow over a blue-grey ground goes muddy — which
       is the same reason the previous palette's shadow was warm and not black. */
    --shadow: 0 1px 2px rgba(10, 15, 44, 0.05), 0 10px 26px -14px rgba(10, 15, 44, 0.22);
    --shadow-lg: 0 2px 4px rgba(10, 15, 44, 0.06), 0 18px 40px -20px rgba(10, 15, 44, 0.28);
    --measure: 64ch;
}

/* No dark override on purpose — see the note above the tokens. */

/* ==========================================================================
   1b. RESET — what Bootstrap used to do, minus what it also did.

   Bootstrap was linked from App.razor "for its reset and box-sizing ONLY", and no Bootstrap
   class appears in any component. The trouble is that a reset is not what it was: it also
   sized every heading, fluidly, and it loaded FIRST, so it won every heading this file did not
   explicitly claim by class.

   ⚠️ Measured on /{slug}/classement before the change: `h3` "Tout le classement" came out at
   28px — Bootstrap's calc(1.3rem + .6vw) — directly under the `h2` that introduces it at 24px.
   The subtitle was larger than its own title, on a page nobody had touched. Three of the four
   public pool pages had the same shape.

   The lesson is not "Bootstrap is heavy". It is that --step-* was never the law: it was a set
   of exceptions layered over someone else's defaults, and the gaps between the exceptions were
   invisible in this file because the rule that filled them lives in another one.
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

/* The margin reset Bootstrap also did. Kept element by element rather than as `* { margin: 0 }`
   so that a control's own metrics are not flattened along with them. */
body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ol, ul { margin: 0; }

/* Inputs do not inherit the page font on their own; this is the single most visible thing a
   reset does. `font: inherit` covers family, size, weight and line-height in one declaration. */
button, input, select, textarea { font: inherit; color: inherit; }

img, svg, video { max-width: 100%; }

/* Bootstrap set this on `table`, and losing it doubles every border in .data. */
table { border-collapse: collapse; }

/* Chrome and Safari do not inherit a fieldset's width constraints; both of these are what stops
   a legend or a fieldset from ignoring the grid it sits in. */
fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }

/* ==========================================================================
   2. BASE
   ========================================================================== */
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  line-height: 1.15;
  /* Negative tracking is what a grotesque needs at heading sizes and a serif did not: letter
     spacing is set for running text, so at 40px the same value leaves the words looking
     spaced out. Applied here rather than per heading so no size is left behind. */
  letter-spacing: -0.02em;
  text-wrap: balance;
}

/* ⚠️ EVERY LEVEL IS SIZED, INCLUDING THE ONES NOTHING USES YET. That is the whole point: an
   unclassed heading has to land somewhere on --step-*, and the only way to guarantee it is to
   leave no level unclaimed. A class may still override a level — .hero h1 does — but a heading
   that no rule mentions can no longer come out at a size this file never chose.

   The weights are not decoration either. Bricolage is a variable face with a 200–800 axis and
   the theme used exactly one setting of it, 600, from the 60px hero title down to a 16px card
   title. Paying for a display face and then playing it on one note is what made every heading
   read with the same density. The ramp now tightens as it grows: heavier and more negative
   tracking at the top, lighter and looser at the bottom, which is how a grotesque is meant to
   be set. */
h1 { font-size: var(--step-4); font-weight: 700; letter-spacing: -0.03em; }
h2 { font-size: var(--step-3); font-weight: 700; }
h3 { font-size: var(--step-2); font-weight: 600; }
h4 { font-size: var(--step-1); font-weight: 600; letter-spacing: -0.01em; }
h5, h6 { font-size: var(--step-0); font-weight: 600; letter-spacing: 0; }

a { color: var(--brand); text-underline-offset: 2px; }

:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; border-radius: 2px; }

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

/* ⚠️ Eyebrows are NOT coloured, and that reverses the previous rule, which said they should
   carry the lead colour so that "this alone puts colour on every page". Putting colour on
   every page was the disease, not the cure: an eyebrow opens every section of every screen,
   so colouring it made the lead colour the single most repeated ink on the site and left it
   unable to point at anything. What an eyebrow needs is to be small, spaced and quiet — it is
   a label, and it is already doing its job in --ink-faint. */
.eyebrow {
  font-family: var(--font-ui);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* The muted variant, for genuinely secondary notes. --ink-faint against the base rule's
   --ink-soft: once the base stopped being coloured, the two had to be told apart by weight
   AND by ink, because a 600/700 difference alone is invisible at 11px. */
.eyebrow--quiet { color: var(--ink-faint); font-weight: 600; }

/* Every measured value goes through this: digits line up in columns. */
.num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* The tick motif — the measuring rule, signature of the theme.
   ⚠️ Drawn in the neutral rule colour, which reverses the previous rule ("drawn in the accent
   because it runs the full width of the countdown, so it is the largest single piece of colour
   on the landing page"). Being the largest piece of colour is the argument against, not for:
   a graduation is the scale, not the reading. The one thing on it that carries a value is the
   marker, and the marker is what the warm colour is for — a scale painted in the same family
   as its needle hides the needle. */
.ticks {
  height: 14px;
  background-image: repeating-linear-gradient(
    to right,
    color-mix(in srgb, var(--rule-strong) 70%, transparent) 0 1px,
    transparent 1px 12px
  );
}

/* ==========================================================================
   3. SHELL
   ========================================================================== */
.shell { max-width: 1120px; margin: 0 auto; padding: 0 var(--sp-3); }

/* Not sticky on a phone, and that is the point of measuring it: the bar needs three rows there — the
   wordmark, the pool links, then the call to action — which came to 180px of an 812px screen. A sticky
   header that keeps 22% of the viewport for navigation is worse than one that scrolls away, on pages
   whose whole job is to be read. It becomes sticky again as soon as the rows collapse into one. */
.topbar {
  z-index: 40;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--rule);
}

@media (min-width: 720px) {
  .topbar { position: sticky; top: 0; }
}

/* Room above the wordmark, which needs two different levers because two different things were cramping
   it — measured, one after the other.

   `min-height: 4.5rem` handles a bar of a single row, like the showcase: there, height is set by the
   floor, so raising the floor is what gives the centred wordmark somewhere to sit (about 20px above it
   instead of 14). Padding alone would have done nothing there: with border-box it is absorbed by the
   min-height, and align-items:center puts the wordmark back exactly where it was.

   `padding-block-start` handles a bar of several rows, like a pool's. That one already runs past the
   floor, so the floor is irrelevant and the first row started flush against the top of the screen — 0px
   above the logo. Padding is the only thing that moves it there.

   Top only, no bottom: the last row already sits above the bar's own bottom border, and the pool's bar
   is 114px on a phone. Twelve pixels of air is worth it; twenty-four would be spending the room this
   header was just trimmed down to. */
.topbar__inner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 4.5rem;
  padding-block: var(--sp-2) 0;
  flex-wrap: wrap;
}


/* Pushed to the far end of the bar, and the only thing that puts it there.

   `margin-left: auto` rather than `justify-content: space-between` on the bar: the bar is empty on
   every page but the showcase, and space-between would leave the wordmark alone but re-anchored the
   moment anything else is ever added beside it. The rule belongs to the element that wants to be last,
   not to the container.

   ⚠️ It wraps onto its own row on a narrow screen, which is what the bar's flex-wrap does to anything
   put here — measured at 375px. Acceptable for one quiet link and not for a row of navigation, which is
   what used to sit here and what took a whole row of a phone. */
.topbar__action { margin-left: auto; }
.brand {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-right: auto;
  color: var(--ink);
  text-decoration: none;
}

/* A bar whose mark and wordmark lead to two different places, which is the pool's case: the mark
   returns to the site, the name returns to the pool. One anchor cannot hold two destinations and
   nested anchors are not legal HTML, so the pair becomes a group — and the group takes over the auto
   margin .brand carries when it is the flex item itself, otherwise the nav stops being pushed right.

   Same gap and same baseline alignment as .brand, so a bar built this way is indistinguishable from
   one built with a single link. */
.brandgroup {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-right: auto;
}

.brandgroup .brand { margin-right: 0; }

/* display:flex rather than inline: an inline anchor puts its image on the text baseline and leaves a
   few pixels of descender space under it, which shows as the mark sitting low. */
/* text-decoration on a link that holds nothing but an image draws nothing, so this changes no pixel
   today. It is here so it stays true if the mark ever gains a word beside it: a logo is never
   underlined. */
.brandmark { display: flex; align-self: center; text-decoration: none; }

/* The mark, beside the wordmark.

   Centred rather than left on the flex default: .brand aligns on the baseline, which is right for the
   name and its small caption, and wrong for the image — it would hang from the text's baseline and sit
   visibly too low. display:block for the same family of reason: an inline image sits on the baseline
   and leaves a few pixels of descender space under it. */
.brand__logo { align-self: center; flex: 0 0 auto; display: block; }

.brand small {
  font-family: var(--font-data);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The base case is the phone: one row that scrolls sideways, instead of two that wrap.
   Five items do not fit in 375px, and stacking them cost the top of the screen on every page.

   `min-width: 0` is what actually makes it scroll rather than stretch. A flex item's automatic minimum
   size is its own content, so without it the bar would simply grow wider than the screen — the content
   would overflow the *page* instead of overflowing *inside* the nav, which is a horizontal scrollbar on
   the whole document rather than on the menu.

   ⚠️ The clipped item is NOT a usable hint, measured rather than assumed: at 375px the five entries come
   to 433px for 339px of room, and the cut falls such that "Se connecter" shows **2 pixels**. Two pixels
   announce nothing. Hence the shading below. */
.nav {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  flex-wrap: nowrap;
  flex: 1 1 100%;
  min-width: 0;
  overflow-x: auto;
  /* Keeps a sideways swipe on the menu from turning into a page scroll or a browser back gesture. */
  overscroll-behavior-x: contain;
}

/* ⚠️ Entries keep their own width and never compress. This is not tidiness — without it the row does not
   scroll, it *squeezes*: `flex-shrink` is 1 by default, so a browser short of room prefers narrowing an
   entry until its label breaks over several lines rather than letting the row overflow. In the
   administration bar, "Voir le pool" came out stacked as three lines of one word.
   `nowrap` on top of that, because a label with a space in it would still break inside its own box even
   at full width. Correct at every width, so not scoped to the phone: a menu entry that wraps its text is
   a menu entry too narrow, never a menu entry doing the right thing. */
.nav > * {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The "there is more to the right" hint: a shading pinned to the right edge of the bar.
   `background-attachment: scroll` is what pins it — the layer stays on the box while the links slide
   underneath it, which is the opposite of the default for a scrolling container.

   ⚠️ Capped at 479px rather than at the 720px breakpoint, and the number is measured, not chosen: the
   five entries occupy 433px, so from roughly 480px of bar they all fit and there is nothing left to
   reveal. Drawing the hint there would state something false. Between 480px and 720px the row therefore
   scrolls if it ever needs to, silently, and shows no hint because it does not need one.

   A shading rather than a drawn arrow: an arrow has to point somewhere and would need to disappear on
   the last item, which no stylesheet can know without scroll-driven animations. This says "the surface
   continues" without claiming a direction that could be wrong. */
@media (max-width: 479px) {
  .nav {
    background-image: linear-gradient(to left, var(--rule) 0%, transparent 100%);
    background-position: right center;
    background-size: 1.25rem 100%;
    background-repeat: no-repeat;
    background-attachment: scroll;
  }
}

@media (min-width: 720px) {
  .nav { flex-wrap: wrap; flex: 0 1 auto; overflow-x: visible; }
}

.nav__link {
  font-size: var(--step--1);
  color: var(--ink-soft);
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  text-decoration: none;
  position: relative;
}

.nav__link:hover { color: var(--ink); background: var(--paper-sunk); }

.nav__link.active { color: var(--ink); font-weight: 600; }

.nav__link.active::after {
  content: "";
  position: absolute;
  left: 0.6rem; right: 0.6rem; bottom: 0.05rem;
  height: 2px;
  background: var(--brand);
}

/* No rule for a primary button inside .nav, on purpose: no navigation bar holds one any more. The pool
   shell used to repeat the landing page's "Faire ma prédiction", which meant two identical buttons on
   that page, and on a phone the button was wide enough to push the four pool links onto a second line.
   Removing it from the markup solved both, so the CSS that had been propping it up went with it. */

main { padding: var(--sp-5) 0 var(--sp-6); }

.stack { display: flex; flex-direction: column; gap: var(--sp-3); }
.stack-4 { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack-5 { display: flex; flex-direction: column; gap: var(--sp-5); }
.row-flex { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

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

/* A section head introduces either the page or a part of it, and the two are not the same
   size. The h1 is the page's own title — the pool pages had none at all until this change, so
   they opened on an h2 and every heading below had to be squeezed under it. */
.section-head h1 { font-size: var(--step-3); }
.section-head h2 { font-size: var(--step-2); }
.section-head p { color: var(--ink-soft); font-size: var(--step--1); max-width: var(--measure); }

/* ==========================================================================
   4. CARDS — white panels lifting off a tinted ground.

   This section used to open with "small radius and crisp rules: a record card, not a SaaS
   panel", and it did exactly what it said. Combined with a near-white ground it produced
   outlined rectangles on paper — legible, and the reason the site read as a printed form.

   A card is now defined by its lift, not by its outline: white fill, generous radius, a
   hairline border and a soft shadow. The border stays because a shadow alone disappears at
   the top edge of a panel; it is no longer doing the job on its own.
   ========================================================================== */
.card-p {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--sp-3);
  box-shadow: var(--shadow);
}

.card-p--flush { padding: 0; overflow: hidden; }

/* See the note on .card-p__head: flush against the card's edge, its corners are already cut by the
   card's own radius, so it must not add one of its own. Declared after the base rule, equal
   specificity, later wins. */
.card-p--flush > .card-p__head { border-radius: 0; }

/* Card headers keep their tinted band — it is real wayfinding on the administration screens,
   where several cards stack. What goes is the 2px accent rule under it: at that weight it
   read as an underline drawn in marker, which is the "crisp" of the old section title. A
   hairline separates just as well now that the card has an edge of its own. */
/* ⚠️ Rounded, because it is a filled block sitting INSIDE the card's padding, not flush against
   its edge. Left square it was the one hard-cornered shape on a page where everything else curves,
   and it read as abrupt — a green rectangle dropped into a rounded white box. --radius rather than
   --radius-lg: it is contained by the card, so it takes the smaller of the two.

   The flush variant below is the opposite case and must stay at 0: there the head runs to the card's
   own edge, and the card clips it with overflow:hidden, so its corners already follow the 16px curve.
   Giving it a radius as well would draw a second, tighter curve inside the first. */
.card-p__head {
  padding: var(--sp-2) var(--sp-3);
  background: var(--brand-wash);
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* --step-0, not --step-1. At 19px this sat 5px under the section's own h2 at 24px, so a card title
   competed with the heading that introduced it — and on the statistics page it was larger than the
   chart it labelled. It also cost the header a second line: the title and its eyebrow share a row and
   wrap when they no longer fit, which doubled the tinted band's height on a narrow card and left a big
   coloured block above a small drawing. */
.card-p__head h3 { font-size: var(--step-0); margin-right: auto; color: var(--brand); }

.card-p__body { padding: var(--sp-3); }

/* ==========================================================================
   5. BUTTONS & PILLS
   ========================================================================== */
.btn-p {
  appearance: none;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.6rem 1.1rem;
  border-radius: var(--radius);
  border: 1px solid var(--rule-strong);
  background: var(--paper-raised);
  color: var(--ink);
  cursor: pointer;
  text-decoration: none;
  transition: background 140ms ease, border-color 140ms ease, transform 140ms ease;
}

.btn-p:hover { background: var(--paper-sunk); border-color: var(--ink-faint); color: var(--ink); }
.btn-p:active { transform: translateY(1px); }
.btn-p:disabled { opacity: 0.5; cursor: default; transform: none; }

/* Filled with --accent-deep, not --accent: white on the mid-tone measured 4.0, under the 4.5
   minimum for the button label. */
.btn-p--primary { background: var(--brand); border-color: var(--brand); color: var(--on-solid); }
.btn-p--primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); color: var(--on-solid); }

.btn-p--lg { font-size: var(--step-0); padding: 0.8rem 1.5rem; }
.btn-p--quiet { background: none; border-color: transparent; color: var(--ink-soft); }
.btn-p--quiet:hover { background: var(--paper-sunk); color: var(--ink); }

/* The way into the administration.
   Set apart by a hairline rather than by shouting: it belongs to the owner, and a guest who reads
   "Se connecter" as loudly as "Faire ma prédiction" would go looking for a password nobody gave
   them. Everything else it needs — no background, muted ink — already comes from --quiet, so it
   must be declared AFTER it: equal specificity, and the later rule wins. */
/* It follows --radius like every other control now. It was pinned to 0 when everything was
   4px and the difference was invisible; against a 10px sibling a square corner reads as a
   rendering fault, not as restraint. What sets it apart is the hairline below, not its shape. */
.nav__admin { border-radius: var(--radius); }

/* The dividing rule only divides when something sits beside it. Wrapped onto a line of its own on a
   phone, that left border became a stray vertical stroke in front of a lone link — which reads as a
   misplaced element rather than as a separator, and was the "Administration is floating somewhere"
   effect. It comes back as soon as the bar is wide enough to hold the link in a row. */
@media (min-width: 720px) {
  .nav__admin {
    border-left: 1px solid var(--rule);
    margin-left: 0.2rem;
    padding-left: 0.9rem;
  }
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  border: 1px solid currentColor;
}

.pill--open { color: var(--ok); }
.pill--closed { color: var(--warn); }
.pill--done { color: var(--brand); }
.pill--draft { color: var(--ink-faint); }

/* ==========================================================================
   6. HOME
   ========================================================================== */
/* `start`, not `center`. Centring made sense when the second column held a 4/3 picture of
   roughly the first column's height; it is what produced the island described below when the
   picture is absent, and even with one it floats both columns instead of hanging them from the
   headline. */
.hero { display: grid; gap: var(--sp-5); align-items: start; }
@media (min-width: 900px) { .hero { grid-template-columns: 1.15fr 0.85fr; } }

/* ⚠️ A POOL WITH NO PICTURE HAS NO SECOND COLUMN. Measured at 1280px before this rule: the
   right-hand column was 444px wide and held 198px of content — four short rows — inside a
   568px row, leaving 185px of air above it and 185px below. A list floating in the middle of
   an empty column reads as something that failed to load.

   A column has to hold something. Without the picture there is not enough to hold, so the
   facts run as a strip under the hero instead of pretending to be a sidebar.

   Declared after the rule above and inside the same media query on purpose: equal specificity,
   so this only wins by being later. Moving either block reverses the result silently. */
@media (min-width: 900px) { .hero--flat { grid-template-columns: 1fr; } }

/* The one place the display face is allowed to run to the top of its axis. At 60px a grotesque
   wants weight and tight tracking or it reads as a wide, soft banner; at 16px the same setting
   would be unreadable, which is why the ramp in section 2 exists rather than one weight for
   every heading. */
.hero h1 { font-size: var(--step-5); font-weight: 800; letter-spacing: -0.035em; }
.hero__lede { font-size: var(--step-1); color: var(--ink-soft); max-width: 34ch; }
.hero__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* Footnote to the actions, not a competing message: it speaks only to the visitor this browser does
   not recognise, and must not compete with the call to action just above it. */
.hero__hint { font-size: var(--step--1); color: var(--ink-faint); max-width: 42ch; }

/* Countdown drawn as a measuring rule. */
.gauge { display: flex; flex-direction: column; gap: var(--sp-2); }
.gauge__figures { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.gauge__unit { display: flex; flex-direction: column; }

/* Countdown figures in the accent: they are the biggest numbers on the site and the thing
   people come back to look at. Painted in the brand rather than written in the ink: this is
   the one figure on the site that changes while you watch it. */
.gauge__value {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--step-3);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--brand);
}

.gauge__label {
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 0.35rem;
}

.gauge__track { position: relative; height: 26px; border-bottom: 1px solid var(--rule-strong); }
.gauge__track .ticks { position: absolute; inset: auto 0 0 0; }

/* The marker is coral, not green: it must stand out against the accent-coloured ticks it
   slides along, and it gives the counterpoint hue a job. */
.gauge__marker {
  position: absolute;
  bottom: -6px;
  width: 11px; height: 11px;
  border-radius: 50%;
  background: var(--signal);
  transform: translateX(-50%);
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 4px color-mix(in srgb, var(--signal) 40%, transparent);
}

.gauge__ends { display: flex; justify-content: space-between; font-size: var(--step--1); color: var(--ink-faint); }

.heromedia {
  aspect-ratio: 4 / 3;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--paper-raised);
  overflow: hidden;
  display: flex;
}

.heromedia img { width: 100%; height: 100%; object-fit: cover; display: block; }

.factlist { display: grid; gap: 0; border-top: 1px solid var(--rule); margin: 0; }

.factlist > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--rule);
}

.factlist dt, .factlist .k { color: var(--ink-soft); font-size: var(--step--1); margin: 0; }
.factlist dd { margin: 0; }

/* The strip form, used when the hero has no second column to put the list in. Label above
   value rather than label-then-value across a row: four label/value pairs side by side on one
   line read as a table with no header, and the eye cannot tell which number belongs to which
   word. Stacking makes each cell self-contained. */
.factlist--strip { grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0 var(--sp-4); }

.factlist--strip > div {
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 0.15rem;
  padding: var(--sp-2) 0;
}

.factlist--strip .v { font-size: var(--step-1); }

/* ⚠️ Values in the plain ink, NOT in the brand, though the label beside them stays soft. The
   weight and the tabular face are what make the eye land on the figure; adding colour on top
   of that was the reflex that put the lead colour on every row of every screen. */
.factlist .v {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink);
}

/* ==========================================================================
   7. FORM — laid out like a record sheet: label left, input right, stacked on mobile.
   ========================================================================== */
.field {
  display: grid;
  gap: var(--sp-1);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--rule);
}

@media (min-width: 720px) {
  .field { grid-template-columns: 15rem 1fr; gap: var(--sp-3); align-items: start; }
}

.field:last-child { border-bottom: 0; }
.field__label { display: flex; flex-direction: column; gap: 0.15rem; }
.field__label > span:first-child { font-weight: 600; font-size: var(--step-0); }
.field__hint { font-size: var(--step--1); color: var(--ink-faint); }
.field__req { color: var(--crit); }
.field__error { font-size: var(--step--1); color: var(--crit); font-weight: 600; }

input[type="text"], input[type="email"], input[type="date"],
input[type="time"], input[type="number"], input[type="password"], textarea, select {
  font: inherit;
  width: 100%;
  max-width: 30rem;
  padding: 0.55rem 0.7rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
}

input:focus, textarea:focus, select:focus { border-color: var(--brand); outline: none; }
input.invalid, textarea.invalid, select.invalid { border-color: var(--crit); }

/* Autofilled fields, painted back into the theme.

   ⚠️ The yellow this overrides is not ours: Chrome paints its own background on every field it
   filled, and it paints *only* those — so a registration form came out with two yellow boxes and one
   in the theme's colour, which reads as three different kinds of field. The odd one out was the
   correct one.

   A box-shadow rather than a background, because `background-color` is the one declaration the
   autofill style wins against; an inset shadow large enough to cover the box is the only way to
   repaint it. -webkit-text-fill-color goes with it: the text colour is overridden too, and setting
   `color` alone leaves it as the browser chose. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 100px var(--paper) inset;
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--ink);
}

/* A textarea is the same control as the input above it, so it is typed in the same face at the same
   size — `font: inherit` from the shared control rule is all it needs. It used to override that with
   --font-display at --step-1: 19px of Bricolage sitting directly under a 16px system-font input, on
   a form where both hold the same kind of sentence. The display face is for headings; giving it to
   one field made that field look like a different species. */
textarea { min-height: 6.5rem; resize: vertical; }
input.num-input { font-family: var(--font-data); font-variant-numeric: tabular-nums; max-width: 11rem; }

.inputgroup { display: flex; align-items: center; gap: 0.5rem; }
.inputgroup__unit { font-family: var(--font-data); font-size: var(--step--1); color: var(--ink-faint); }

/* Choice groups: segmented chips, no decorative emoji. */
.choices { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.choice { position: relative; margin: 0; }
.choice input { position: absolute; opacity: 0; width: 0; height: 0; }

/* Fully round, and roomier than the controls around them. A choice chip is the one thing on
   this form that is tapped rather than typed into, and a pill says that without a label. */
.choice span {
  display: inline-block;
  font-size: var(--step--1);
  padding: 0.5rem 0.95rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: var(--paper-raised);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}

.choice span:hover { border-color: var(--brand); background: var(--brand-wash); }

/* A selected chip is filled, not merely tinted: on a form of six choice questions, the answers
   should be readable at a glance from across the page.

   Driven by `.choice--selected`, a class the component sets, rather than by
   `input:checked + span`. Blazor inserts marker comments between the input and the span and
   re-renders both, and the pseudo-class styling then failed to apply even though the selector
   matched — leaving white text on a cream chip, contrast 1.08, effectively invisible.
   The `:checked` rule is kept as well so keyboard selection looks right before the round trip
   completes; whichever applies first, both produce the same appearance. */
.choice--selected span,
.choice input:checked + span {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-solid);
  font-weight: 700;
}

.choice input:focus-visible + span { outline: 2px solid var(--signal); outline-offset: 2px; }

.formfoot { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; padding-top: var(--sp-3); }
.formfoot p { font-size: var(--step--1); color: var(--ink-faint); max-width: 42ch; }
/* A footer holding nothing but its action has no left edge to align to, so it centres. */
.formfoot--center { justify-content: center; }

/* ==========================================================================
   8. TABLES — the container scrolls, never the page.
   ========================================================================== */
.tablewrap { overflow-x: auto; }

table.data { width: 100%; border-collapse: collapse; font-size: var(--step--1); }

table.data thead th {
  text-align: left;
  padding: 0.7rem var(--sp-2);
  /* Sunk rather than accent-washed, with a hairline instead of a 2px accent rule. A header row
     is scaffolding: it should recede behind the 29 rows the reader came for, and the colour it
     was painted in made it the loudest band on the page. */
  border-bottom: 1px solid var(--rule);
  background: var(--paper-sunk);
  white-space: nowrap;
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  font-weight: 700;
}

table.data tbody td { padding: 0.6rem var(--sp-2); border-bottom: 1px solid var(--rule); vertical-align: middle; }

/* Zebra striping plus a warm hover: 32 rows of identical background is hard to track across.  */
table.data tbody tr:nth-child(even) { background: color-mix(in srgb, var(--paper-sunk) 55%, transparent); }
table.data tbody tr:hover { background: var(--brand-wash); }
/* `white-space: nowrap` belts what the no-break space braces: a measurement and its unit are one
   token, and the table already scrolls inside its own container, so a wider column costs nothing.
   Without it, "3 250 g" split across two lines as soon as twelve columns squeezed the table. */
table.data td.num, table.data th.num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* And for the instant a prediction was sent: "6 août · 14:32" is one token, and breaking it after the
   month turns a timestamp into two half-readable lines. Quieter than the answers around it — it orders
   the table, it is not what anybody came to read. */
table.data td.when, table.data th.when { white-space: nowrap; }
table.data td.when { color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* Same reasoning for the value column of a definition list, used on the confirmation recap. */
.factlist .v { white-space: nowrap; }

/* Sentences instead of measured values — the showcase.
   Two changes, and both are needed. The value may break across lines, since a sentence has nothing to
   protect: "12 sept · 14:30" splitting in two is a defect, "voir les autres ferme sa réponse" splitting
   is just a second line. And the row may wrap, so a value that no longer fits beside its label drops
   below it instead of being pushed off the edge.

   ⚠️ Measured before deciding: at 381px the longest of these values is 300px and its row is 363px wide,
   so it fitted with **zero** slack — and `ui-monospace` resolves to a different face on Windows, iOS and
   Android, each with its own width. A layout that survives only on the machine it was checked on is not
   a layout, which is why this wraps rather than being tuned to fit. */
.factlist--prose > div { flex-wrap: wrap; }
.factlist--prose .v { white-space: normal; }

/* ⚠️ And they drop to the size of their own label, which the measured form must never do.
   `.factlist .v` is 16px and semibold because its job is to make the eye land on a figure — a weight,
   a date, a score. These are sentences, so that job does not exist here: what the size bought was
   nothing, and what it cost was a caption set larger and heavier than the paragraph above it, in a
   monospace face that already reads bigger than a sans at the same nominal size. Same 13px as the
   label, and the mono face plus the full-strength ink still tell value from label. */
.factlist--prose .v { font-size: var(--step--1); font-weight: 500; }

.sortlink { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 0.3rem; }
.sortlink:hover { color: var(--ink); }
.sortlink__arrow { color: var(--brand); font-size: 0.7em; }

.who { display: flex; align-items: center; gap: 0.6rem; min-width: 11rem; }
.who__name { font-family: var(--font-display); font-size: var(--step-0); }

.avatar { flex: none; width: 30px; height: 30px; border-radius: 50%; overflow: hidden; background: var(--paper-sunk); }
.avatar--lg { width: 56px; height: 56px; }
.avatar--xl { width: 76px; height: 76px; }
.avatar svg { display: block; width: 100%; height: 100%; }

.swatch { display: inline-flex; align-items: center; gap: 0.4rem; white-space: nowrap; }
.swatch i { width: 9px; height: 9px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.15); }

/* ==========================================================================
   9. GATE — the anti-cheating lock. Explains itself; it is not a wall.
   ========================================================================== */
/* The gate is a friendly interruption, not a dead end, so it is tinted rather than grey. */
.gate {
  border: 1px dashed var(--brand);
  border-radius: var(--radius-lg);
  background: linear-gradient(
    to bottom,
    var(--brand-wash),
    color-mix(in srgb, var(--brand-wash) 30%, var(--paper-raised))
  );
  padding: var(--sp-5) var(--sp-3);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-2);
}

.gate__mark {
  width: 38px; height: 38px;
  border: 2px solid var(--brand);
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--brand);
  background: var(--paper-raised);
}

.gate h3 { font-size: var(--step-2); color: var(--brand); }
.gate p { color: var(--ink-soft); max-width: 44ch; font-size: var(--step--1); }

/* Addressed to someone who has already played and is on another device. Set apart by a rule rather
   than by a second box: it is a footnote to the refusal, not a competing message. */
.gate__already {
  border-top: 1px solid var(--brand);
  padding-top: var(--sp-2);
  margin-top: var(--sp-1);
  color: var(--ink-faint);
}
.gate__actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; justify-content: center; margin-top: var(--sp-1); }

/* ==========================================================================
   10. BANNERS, TILES, CHARTS
   ========================================================================== */
.banner {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-start;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius);
  background: var(--paper-raised);
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.banner--info { border-left-color: var(--data-blue); }
.banner--error { border-left-color: var(--crit); color: var(--ink); }
.banner--ok { border-left-color: var(--ok); }

.tiles { display: grid; gap: var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr)); }

/* Tiles are colour-coded by position through nth-child, so a row of figures reads as a set
   of distinct measures rather than four identical boxes. */
.tile {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--brand);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.tile:nth-child(2) { border-top-color: var(--data-blue); }
.tile:nth-child(3) { border-top-color: var(--data-clay); }
.tile:nth-child(4) { border-top-color: var(--data-violet); }
.tile:nth-child(5) { border-top-color: var(--data-sand); }

.tile__value {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--step-2);
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--brand);
}

.tile:nth-child(2) .tile__value { color: var(--data-blue); }
.tile:nth-child(3) .tile__value { color: var(--data-clay); }
.tile:nth-child(4) .tile__value { color: var(--data-violet); }
.tile:nth-child(5) .tile__value { color: var(--data-sand); }

.tile__value small { font-size: 0.5em; color: var(--ink-faint); letter-spacing: 0; }
.tile__label { font-size: 0.625rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }

.chartgrid { display: grid; gap: var(--sp-3); }
@media (min-width: 860px) { .chartgrid { grid-template-columns: 1fr 1fr; } }

.chart { width: 100%; height: auto; display: block; overflow: visible; }
/* ==========================================================================
   Horizontal bars, in HTML rather than SVG.

   A grid of three columns per row: the label, the track that flexes, the count. The label and the
   count are ordinary text at --step--1, so they stay the same size whatever the card does; only the
   middle column changes width. That is the whole reason this is not an SVG.
   ========================================================================== */
.bars { display: flex; flex-direction: column; gap: 0.45rem; }

.bars__row {
  display: grid;
  /* minmax(0, …) rather than a bare 6.5rem: without the zero minimum a long label refuses to shrink
     and pushes the track off the card instead of wrapping. */
  grid-template-columns: minmax(0, 6.5rem) 1fr auto;
  align-items: center;
  gap: 0.6rem;
}

.bars__label {
  font-size: var(--step--1);
  color: var(--ink-soft);
  text-align: right;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.bars__track {
  height: 0.95rem;
  border-radius: var(--radius-pill);
  background: var(--paper-sunk);
  overflow: hidden;
}

.bars__fill { display: block; height: 100%; border-radius: var(--radius-pill); }

.bars__count {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--step--1);
  color: var(--ink-soft);
  min-width: 1.5rem;
}

/* ==========================================================================
   Vertical columns, also in HTML. Same reason as the bars above: a viewBox would tie the label size
   to the card's width, and the narrowest card is the one that most needs readable labels.

   The plot is a grid of --cols equal columns with a fixed height, so a column's height is a
   percentage of something real, and the axis below is the same grid again — one tick per column,
   which keeps ticks and labels aligned with the columns they belong to without any arithmetic.
   ========================================================================== */
.cols { display: flex; flex-direction: column; }

.cols__plot {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  align-items: end;
  gap: 2px;
  height: 8.5rem;
  /* The axis. On the plot rather than on the ticks below, so it is one unbroken line. */
  border-bottom: 1px solid var(--rule-strong);
  /* Counts and the marker's word sit above the tallest column and must not be clipped. */
  overflow: visible;
}

.cols__col {
  /* ⚠️ Row 1 explicitly, not just the column. A definite column with an automatic row still makes an
     item look for a row where that column is free — so the column under the marker kept sliding to
     row 2, splitting the plot's height in half and dropping the axis below a phantom row. Both axes
     have to be definite before two items are allowed to share a cell. */
  grid-row: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  height: 100%;
  min-width: 0;
}

.cols__bar { width: 100%; border-radius: 3px 3px 0 0; }

.cols__n {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 0.6875rem;
  line-height: 1.4;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* The dashed rule marking one column. A grid item in that column, so its position is exact. */
.cols__mark {
  grid-row: 1;
  justify-self: center;
  align-self: stretch;
  width: 0;
  border-left: 1px dashed var(--signal);
  position: relative;
}

.cols__mark-label {
  position: absolute;
  bottom: 100%;
  padding-bottom: 0.15rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--signal-deep);
  white-space: nowrap;
  /* Centred on the rule by default; the modifiers below pull it inside when the marked column is
     near an edge, so the word never hangs off the card. */
  left: 50%;
  transform: translateX(-50%);
}

.cols__mark--start > .cols__mark-label { left: 0; transform: none; }
.cols__mark--end > .cols__mark-label { left: auto; right: 0; transform: none; }

.cols__axis {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: 2px;
  margin-top: 0.35rem;
}

/* One tick per column, drawn as a short rule above the label. */
.cols__tick {
  position: relative;
  min-width: 0;
  padding-top: 0.3rem;
  font-family: var(--font-data);
  font-size: 0.6875rem;
  color: var(--ink-faint);
  white-space: nowrap;
  text-align: center;
}

.cols__tick::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: 1px;
  height: 3px;
  background: var(--rule-strong);
}

/* ⚠️ A label is wider than its own cell, so it overflows — and text-align is what decides which way.
   Centred it spills both sides, which is right in the middle of the axis and wrong at the ends: the
   first column's centre is a few pixels from the card's edge. Aligning the outermost labels to their
   own outer edge sends the overflow inwards instead. */
.cols__tick--start { text-align: left; }
.cols__tick--end { text-align: right; }
.chart .axis { stroke: var(--rule-strong); stroke-width: 1; }
.chart .grid-line { stroke: var(--rule); stroke-width: 1; stroke-dasharray: 2 3; }

/* Bar labels carry text, so they take the ink colour rather than the faint one the axis uses. */
.chart .bar-label { fill: var(--ink); }
.chart .axis-label { fill: var(--ink-faint); }

/* The empty track behind a bar. Without it, an option nobody picked is indistinguishable from an
   option that is not on the chart at all. */
.chart .bar-track { fill: var(--paper-sunk); }

/* The reference-date marker. Coral rather than --crit: red means "error" everywhere else in this
   interface, and a reference point is not a fault. The coral is also what the countdown marker on
   the landing page already uses for exactly this date, so the two agree.
   Text takes --warm-deep, measured at 5.02 on --paper-raised; --warm is decorative only. */
.chart .mark-line { stroke: var(--signal); stroke-width: 1; stroke-dasharray: 3 2; }

.chart .mark-label {
  fill: var(--signal-deep);
  font-weight: 700;
  letter-spacing: 0.08em;
}

.chart__note { font-size: var(--step--1); color: var(--ink-soft); }

.legend { display: flex; flex-wrap: wrap; gap: var(--sp-2); font-size: var(--step--1); color: var(--ink-soft); }
.legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
.legend i { width: 10px; height: 10px; border-radius: 2px; }

/* ==========================================================================
   10b. PODIUM AND LEADERBOARD
   ========================================================================== */
.podium { display: grid; gap: var(--sp-2); list-style: none; margin: 0; padding: 0; }

/* Second, first, third — the arrangement everyone recognises. Source order stays 1, 2, 3 so the
   markup is a real ordered list, and on a narrow screen it stacks in rank order. */
@media (min-width: 720px) {
  .podium { grid-template-columns: 1fr 1.15fr 1fr; align-items: end; }
  .podium__step--first { order: 2; }
  .podium__step--second { order: 1; }
  .podium__step--third { order: 3; }
}

.podium__step {
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--rule-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: var(--sp-3) var(--sp-2);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  text-align: center;
}

.podium__step--first { border-top-color: var(--gold); padding-block: var(--sp-4); }
.podium__step--second { border-top-color: var(--silver); }
.podium__step--third { border-top-color: var(--bronze); }

.podium__rank {
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1;
  color: var(--ink-faint);
}

.podium__step--first .podium__rank { color: var(--gold-deep); }
.podium__step--second .podium__rank { color: var(--silver-deep); }
.podium__step--third .podium__rank { color: var(--bronze-deep); }

.podium__who { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; }
.podium__name { font-family: var(--font-display); font-size: var(--step-1); }

.podium__score {
  font-size: var(--step-2);
  font-weight: 700;
  color: var(--brand);
  line-height: 1;
}

.podium__unit { font-size: 0.625rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }

/* The full ranking is a list of <details>, not a table: opening a row needs no JavaScript, and the
   breakdown would otherwise have to live in a second row pretending to belong to the first. */
/* A panel, and now it looks like one. It was a stack of white rows held between two hairlines: no
   radius, no shadow, so on an off-white page it read as a bare rectangle among rounded cards — the
   same fault as the title band, in the one place that had no card around it to hide it.
   overflow: hidden is what makes the first and last rows follow the corner; without it they square
   the ends off again from the inside. */
.board {
  display: flex;
  flex-direction: column;
  background: var(--paper-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.board__row { border-bottom: 1px solid var(--rule); background: var(--paper-raised); }

/* The board's own border closes the bottom; a row rule there would double it. */
.board__row:last-child { border-bottom: 0; }

.board__head {
  display: grid;
  grid-template-columns: 2rem 30px 1fr auto 1rem;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem var(--sp-2);
  cursor: pointer;
}

/* Both are needed: browsers disagree on which one hides the disclosure triangle. */
.board__head { list-style: none; }
.board__head::-webkit-details-marker { display: none; }
.board__head::marker { content: ''; }

.board__head::after {
  content: '▾';
  color: var(--brand);
  font-size: 0.75em;
  transition: transform 120ms ease;
}

.board__row[open] > .board__head::after { transform: rotate(180deg); }
.board__row[open] > .board__head { background: var(--brand-wash); }
.board__head:hover { background: var(--brand-wash); }

.board__rank { color: var(--ink); font-weight: 700; text-align: right; }
.board__name { font-family: var(--font-display); }

/* ⚠️ No underline at rest, and the boundary matters: this applies to a link that IS the row, not to
   a link inside a sentence. WCAG 1.4.1 refuses colour as the only thing telling a link from the text
   around it — but here there is no text around it. Every row of this list is a link, in display type,
   on its own line; nothing is being distinguished from anything.
   An inline link in a banner or a paragraph keeps its underline for exactly that reason. Do not
   promote this rule to `a { text-decoration: none }`.
   It comes back on hover and on focus, so the affordance is still there the moment you reach for it. */
.board__name a { text-decoration: none; }
.board__name a:hover,
.board__name a:focus-visible { text-decoration: underline; text-underline-offset: 3px; }
.board__total { font-weight: 700; color: var(--ink); }

.board__detail { padding: var(--sp-2) var(--sp-3) var(--sp-3); background: var(--paper-sunk); }
.board__why { display: block; font-size: var(--step--1); color: var(--ink-faint); }

/* A board row that is not a disclosure: same rhythm, no pointer, no chevron. */
.board__head--static { grid-template-columns: 1fr auto; cursor: default; }
.board__head--static::after { content: none; }
.board__head--static:hover { background: none; }

/* ==========================================================================
   10c. ADMINISTRATION
   ========================================================================== */
/* The administration wears the same paper, with a coral rule along the top. Enough to make it
   obvious at a glance that what is typed here is publicly visible, without building a second theme. */
.topbar--admin { border-top: 3px solid var(--signal); }
.topbar--admin .brand small { color: var(--signal-deep); }

.signin { max-width: 34rem; margin: 0 auto; display: flex; flex-direction: column; gap: var(--sp-3); }
.signin input[type="password"] { width: 100%; }

/* One question's true answer: the generated field, then the state line under it. */
.truth { border-bottom: 1px solid var(--rule); }
.truth:last-of-type { border-bottom: 0; }
.truth .field { border-bottom: 0; padding-bottom: 0; }

/* A pending question is marked, not disabled: its field stays editable, because typing an answer is
   what clears the "pas encore connue" box. Only the label softens, so the eye can find the questions
   still to answer without the row looking switched off. */
.truth--pending .field__label > span:first-child { color: var(--ink-soft); }
.truth--pending .truth__toggle { color: var(--signal-deep); font-weight: 600; }

.truth__state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: 0 0 var(--sp-3);
  font-size: var(--step--1);
}

@media (min-width: 720px) {
  /* Lines up with the second column of .field, so the checkbox sits under its own input. */
  .truth__state { padding-left: calc(15rem + var(--sp-3)); }
}

/* A read-only value and its copy button. The field takes the space so a long link stays selectable
   even when the clipboard API is unavailable and selecting is the fallback. */
.copyrow { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
.copyrow > input { flex: 1 1 18rem; min-width: 0; }
.copyrow > .btn-p { flex: none; }

/* Hand-scoring sheet. An unjudged row is tinted rather than badged: with thirty rows, the eye wants
   to find the remaining work by scanning, not by reading a column of labels. */
tbody tr.mark--todo { background: color-mix(in srgb, var(--signal-wash) 55%, transparent); }
tbody tr.mark--todo:hover { background: var(--signal-wash); }

.mark--skipped { color: var(--ink-faint); font-style: italic; }
.mark__points { width: 6rem; text-align: right; }

/* Present in the page but invisible — and, unlike display:none, still selectable.
   The copy script falls back to selecting a field when the clipboard API is unavailable, which is
   what happens over plain HTTP; a hidden field would make that fallback do nothing. */
.offscreen {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Row controls on the questions list: move, move, remove. Small, and out of the way until wanted. */
.qrow__tools { display: inline-flex; align-items: center; gap: 0.2rem; }
.qrow__tools .btn-p { padding: 0.25rem 0.5rem; font-size: var(--step--1); }

/* The delete control only turns red once armed — see .btn-p--danger. Before that it is a quiet
   control like the others, because an always-red button stops being read after the third visit. */
.qrow__delete { color: var(--ink-faint); }
.qrow__delete:hover { color: var(--crit); }

/* Reserved for the second click of a destructive action, never the first. */
.btn-p--danger { background: var(--crit); border-color: var(--crit); color: var(--on-solid); }
.btn-p--danger:hover { background: var(--crit-strong); border-color: var(--crit-strong); color: var(--on-solid); }

.truth__toggle { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--ink-soft); cursor: pointer; }
.truth__warning { color: var(--signal-deep); font-weight: 600; }
.truth__recorded { color: var(--ink-faint); font-family: var(--font-data); }

/* ==========================================================================
   11. CONFIRMATION
   ========================================================================== */
.confirm {
  max-width: 46rem;
  margin: 0 auto;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  align-items: center;
}

.confirm h1 { font-size: var(--step-4); }
.confirm__note { color: var(--ink-soft); max-width: 46ch; }

.quote {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-style: italic;
  color: var(--ink);
  border-left: 2px solid var(--brand);
  padding-left: var(--sp-3);
  text-align: left;
  max-width: 40ch;
  margin: 0;
}

.recap { text-align: left; width: 100%; }

/* The sentence that names the panel, on a filled brand band running the width of the card.

   ⚠️ Solid --brand and not --brand-wash, which is what .card-p__head uses everywhere else. The
   administration screens stack several cards and the band is wayfinding there — quiet on purpose,
   because a page of loud bands is a page with no hierarchy. Here there is exactly one card, on the
   screen the whole flow exists to reach, and the band is the roof of the payoff rather than a label
   in a list. It is the only filled band in the application, and it has to stay that way to keep
   meaning anything.

   White on --brand measures 8.15, the same pair as the primary button — so this does not depend on
   the band's own size and nothing here is borderline.

   ⚠️ The risk it carries is that a full-width blue bar reads as something to press. What answers it
   is that it holds no affordance at all: no arrow, no chevron, no hover, and the one control on this
   page painted that blue sits well below it with a button's shape. Putting anything clickable in
   here would turn the ambiguity into a real trap.

   No border-radius: the card is --flush, so overflow:hidden clips this band to the card's own curve.
   Giving it one would draw a second, tighter curve inside the first — see the note on
   .card-p--flush > .card-p__head, which is the same problem.

   It is no longer sized with .keepsake__title. The two were one rule while they were one voice; a
   filled band and a demoted heading are not, and keeping them welded would mean any change to one
   silently repaints the other. */
.recap__title {
  background: var(--brand);
  color: var(--on-solid);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* The personal address, with the quiet line that introduces it once a message already carries it.

   A group of its own rather than two children of .confirm: that container is a flex column with a
   --sp-3 gap, which is a full block of air — right for two paragraphs side by side, wrong between a
   caption and the box it labels. At --sp-1 the line reads as belonging to the box beneath it.

   The lead is --ink-faint and one step down on purpose. It is a fallback, addressed only to the
   reader whose message did not arrive; sized like the sentence above it, it would compete with the
   very statement that makes it unnecessary for everybody else. */
.tokengroup {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

/* Two quiet lines around the box: the caption that introduces it, and the way out when the link is
   lost. One rule for both — they are the same voice, an aside to the statement above, and giving
   them separate rules is how one of them ends up repainted alone. */
.tokengroup__lead,
.keepsake__aside {
  margin: 0;
  text-align: left;
  font-size: var(--step--1);
  color: var(--ink-faint);
}

.tokenbox {
  font-family: var(--font-data);
  font-size: var(--step--1);
  background: var(--paper-sunk);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--sp-2);
  word-break: break-all;
  text-align: left;
  width: 100%;
}

/* The personal link and the correction rule, after the answers rather than before them.

   The same hairline device as .followup below: no card, because the page is already a stack of
   panels and a sixth one for the housekeeping would read as another record to file. Left-aligned
   against .confirm's centring — it holds an address and a rule, read line by line, not a greeting.

   ⚠️ The heading is a real h2 sized down by class, not an h4 chosen for its size. The level says
   where the block sits in the document, the class says how loud it is in the page; picking the tag
   by appearance is how a screen ends up with a heading outline nobody can follow. Section 2 sizes
   h2 at --step-3/700, which is right for a section title and far too loud for a demoted one. */
.keepsake {
  width: 100%;
  border-top: 1px solid var(--rule);
  padding-top: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  text-align: left;
}

.keepsake__title {
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.keepsake__note { color: var(--ink-soft); }

/* The owner's invitation, high on the page — the bottom of a confirmation is not read. See the note
   in Confirmation.razor for why it moved there and what it costs.

   A filet rather than a card: the page is already a stack of white panels on a pale ground, and one
   more for two lines of text reads as another record to file. A hairline says "and now, something
   else" for the cost of one pixel — and it is the only separator on this page that does not enclose
   anything, which is the point. That device works in both directions, which is what made the move
   possible without redrawing the block.

   ⚠️ Deliberately NOT .btn-p--quiet on the link. Quiet is the "Annuler" role: no background, no
   border. It was the only control in the block, and it rendered as a caption. The plain .btn-p —
   raised, outlined — is the middle role: unmistakably a control, and still not competing with the
   page's primary button, which now sits below it rather than above. */
.followup {
  width: 100%;
  border-top: 1px solid var(--rule);
  padding-top: var(--sp-4);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
}

.followup__note { max-width: 42ch; }

footer.pagefoot {
  border-top: 1px solid var(--rule);
  padding: var(--sp-3) 0 var(--sp-5);
  font-size: var(--step--1);
  color: var(--ink-faint);
}

/* Two blocks on one line, and they wrap rather than shrink: the contact address is the part a reader
   might have to copy by hand, so it must never be the thing that gets cut. `wrap` alone would stack
   them left-aligned on a narrow screen, which is the right shape — space-between only separates them
   once there is room for both. */
.pagefoot__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-1) var(--sp-4);
}

/* Blazor's own error UI. The comment here used to claim it was restyled to match; it was not
   — #FFF3CD is the stock yellow it ships with, and it was the last unthemed surface on the
   site. On the tokens now, so it follows the palette instead of announcing the framework. */
#blazor-error-ui {
  color-scheme: light only;
  background: var(--signal-wash);
  border-top: 2px solid var(--signal);
  color: var(--ink);
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
  font-size: var(--step--1);
}

#blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; }
