/* LA POLICE DE L'APP, SERVIE PAR NOUS.
   Elle ne peut pas venir d'un CDN tiers : HeliWX est une PWA dont le service
   worker sert l'app hors ligne, et un <link> vers un service de polices tiers
   ne répond pas dans un hangar sans réseau — exactement là où l'app s'ouvre.
   Les fichiers vivent donc dans /assets/fonts/ et le worker précache le latin
   (APP_SHELL, service-worker.js).

   IBM PLEX SANS, UNE SEULE FAMILLE (docs/ui/grammaire.md, « La typographie ») : une police
   VARIABLE réduite à l'axe 400–700, les seules graisses de l'app — 36 ko en
   latin, 24 ko en latin-ext, sous les 72 ko de la police qu'elle remplace.
   Licence SIL OFL 1.1, à côté des fichiers. Un nom de terrain en cyrillique ou
   en grec retombe sur la police du système, ce qui est la bonne dégradation. */
@font-face {
    font-family: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/assets/fonts/ibm-plex-sans-latin.woff2') format('woff2');
    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: 'IBM Plex Sans';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/assets/fonts/ibm-plex-sans-latin-ext.woff2') format('woff2');
    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;
}

:root {
    /* Typography — ONE FAMILY, IBM Plex Sans, served by us (owner's call): the
       interface, the titles and every reading. What the mono did — line the
       digits up — tabular-nums does (body); a column of readings lines up on
       its figures, and the letters keep their own widths. */
    --app-font: "IBM Plex Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

    /* Brand palette — this :root is the source for the app's OWN inks: the
       grounds, the inks, and the state colours below.
       The five MINIMA IDENTITIES are not here and never were: they live in
       COLOR_PALETTE (briefing-core.js), are handed out per loaded configuration
       at runtime, and are kept in step with the PDF by
       tests/test_palette_parity.py.
       *-rgb siblings hold the same color as R, G, B channels for rgba() alpha washes,
       so tinted overlays never drift from the base brand color. */
    /* LE SOL ET LES ENCRES SONT NEUTRES (docs/ui/grammaire.md, « Le sol et les encres ») : la page
       est une seule feuille à #fbfbfb — pas le blanc pur, qui serait un plafond
       sans échelon au-dessus pour ce qui doit se lever. La surface EST le sol ;
       --surface-2 est le seul creux (rails, champs). */
    --bg: #fbfbfb;
    --bg-rgb: 251, 251, 251;
    --surface: #fbfbfb;
    --surface-2: #f2f2f3;
    --surface-rgb: 251, 251, 251;
    /* LA RÉSERVE : ce qui doit se lever — menu ouvert, champ actif, carte du
       rapport, feuille du PDF — et le seul blanc pur. */
    --card: #ffffff;
    --card-rgb: 255, 255, 255;
    /* THE REPORT CARD RUNS ON THREE GREYS AND ITS GROUND (owner's call): two
       INKS — --text for a reading, --muted for everything that names one —
       and ONE MATTER, which draws rather than writes. --icon is that matter:
       the night's segment, the validity of a NOTAM, the moon, the sun.
       A BAR IS THE MATTER BROUGHT BACK TOWARDS THE CARD (owner's call): a bar
       runs the width of the card and a glyph is a few pixels, so the same tone
       weighs differently in each — the bar was reading too dark. --bar is that
       one lighter tone, declared once, and MIXED FROM THE CARD'S OWN TWO
       VALUES rather than picked: --icon brought 35 % of the way back to
       --card. Being a mix of the two, it follows every theme by construction —
       there is nothing to re-tune when the ground or the matter moves, and it
       stands at the same share of the way from the card's ground towards its
       ink on both (17 %, against the matter's 25). Every hairline of the
       card, without exception, is --rg-line.
       NO FOURTH GREY IS DECLARED for a bar that needs a lighter one: the DOSE
       moves, scoped to where the reason holds (.rg-rail), and the token does
       not. A new token would be a colour the guide has to name and the themes
       have to mirror; a dose is the same matter, brought further back because
       that bar stands alone. See .rg-rail for the doses and for what fixes
       them.
       THE STATE IS LIGHT, AND WHAT IS NOT THE ESTABLISHED STATE IS WHITE
       (owner's call): a BECMG's window is that matter COMBED — teeth of matter
       whose opening shuts where the state is reached — and a TEMPO is that same
       matter with THIN WHITE CUTS through it. So what tells a
       forecast from a state is the TEXTURE and never the force: a TEMPO is
       less sure than what holds, it must never read louder. That is what
       retired the old soft matter: its two jobs are now done by the card's own
       white.
       THE DARK THEME MIRRORS THE LIGHT ONE (owner's call), which is where the
       card is judged. The tone cannot be shared across themes, because neither
       the ground nor the ink is the same: the stretch from ground to ink is 92
       levels of L* on the light theme against 84 on the dark. So the
       PROPORTION is what carries over, not the ink — the matter stands at the
       same share of the way from the card's ground towards its ink on every
       theme: 25 %.
       Measured, not chosen: the matter sits 66 levels of grey under the card,
       the hairline 39, and under roughly 25 levels a matter stops being read
       without looking for it. Never a colour: a bar says when, an icon says
       which way. */
    --icon: #bdbdc1;
    --bar: color-mix(in srgb, var(--icon) 65%, var(--card));
    /* LA MATIÈRE RAMENÉE PLUS LOIN ENCORE VERS LA CARTE. Elle porte la BANDE DE
       NUIT et ses deux glyphes (planche TRESSE) : une bande de 5 px derrière la
       règle des heures pèse autrement qu'une barre, et c'est la dose à laquelle
       elle se remarque sans se faire lire.
       ÉCRITE EN PROPORTION, donc les deux thèmes suivent par construction et
       AUCUN QUATRIÈME GRIS N'EST NOMMÉ. La robe du passé d'une barre, elle, est
       une DOSE écrite là où sa raison tient (.rg-bar-past) et non un jeton de
       plus : elle doit s'écarter franchement de la matière que porte une barre
       hors garde, ce que ce jeton-ci ne fait pas. */
    --bar-past: color-mix(in srgb, var(--icon) 35%, var(--card));
    /* CE QUI EST HORS DE TA GARDE S'ÉCLAIRCIT (décision du propriétaire).
       LA TRESSE le dit par la ROBE de ce qu'on lit : plus de voile sur son sol,
       et --guard-dim n'y porte plus que les LECTURES hors garde. LE MESSAGE
       BRUT, lui, connaît la garde sans connaître l'instant : il garde son voile
       (`.rg-raw-veil`) et son calque, et dit le même fait avec ce qu'il a.
       LE VOILE teinte le SOL et dit OÙ est la zone. Mesuré, il descend 2,4 ·
       2,8 · 2,8 niveaux de L* sous la carte : juste assez pour que la zone
       existe sans se faire remarquer.
       LE CALQUE est une OPACITÉ et dit CE QUI s'y trouve. Une opacité compose
       vers le sol qu'elle a derrière elle : elle PÂLIT SANS DÉTEINDRE, un rouge
       reste rouge en plus clair. C'est ce qu'un gris ne sait pas faire — il
       remplace la teinte, et un mot qui dépasse un minimum cesserait de le
       dire. */
    --guard-veil: rgba(var(--text-rgb), 0.03);
    --guard-dim: 0.6;
    /* CE QUI EST DERRIÈRE NOUS NE REVIENDRA PAS, ce qui est hors garde reste une
       prévision qu'on peut avoir à lire : deux faits, deux doses (décision du
       propriétaire). Une OPACITÉ dans les deux cas — elle pâlit sans déteindre,
       donc une valeur qui dépasse un minimum continue de le dire. */
    --spent-dim: 0.3;
    /* L'encre de lecture 17,3:1 sur le sol, le secondaire 8,5:1 : sous reflet,
       c'est le secondaire qui meurt en premier (§ 2.1–2.2). --heading-rgb porte
       l'encre de titre en canaux, pour les filets qui la dosent (§ 5.3). */
    --text: #17171a;
    --text-rgb: 23, 23, 26;
    --heading: #0f0f12;
    --heading-rgb: 15, 15, 18;
    --muted: #4a4a50;
    /* LE SECONDAIRE QUI RECULE (décision du propriétaire, planche ÉPURE L1) :
       les libellés du METAR, les têtes de source, leurs heures — ce qui NOMME une
       lecture sans en être une. 4,75:1 sur le sol, au-dessus du seuil de 4,5. */
    --quiet: #707076;
    --line: #e2e2e4;
    --contour: #d8d8da;
    --soft-line: #e7e7e9;
    --hover-bg: #ececed;
    /* CHOIX C.1: the ground of the chosen row, the ink at 7 % (owner's decision). */
    --chosen-bg: rgba(var(--text-rgb), 0.07);
    --shadow-rgb: 15, 15, 18;
    /* LE VOILE (docs/ui/grammaire.md, « Les couches, le mouvement et les icônes ») : ce qui est au-dessus se lit par le
       voile posé sur ce qui est dessous, jamais par une ombre. Sombre dans les
       deux thèmes — la nuit, un voile clair éclairerait la page qu'il éteint. */

    /* Run statuses — a word, so held at 6.7:1 on the ground (docs/ui/report.md, « Les neuf encres d'état »).
       --status-ok is --cam-fresh's green: both say "this is good". */
    --status-ok: #0E672F;
    --status-error: #B01C1C;


    /* L'ÂGE D'UNE IMAGE DE WEBCAM, sur trois paliers. Ce n'est PAS une identité
       de minima (celles-là vivent dans COLOR_PALETTE) : celle-ci porte un ÉTAT, comme --text ou --red — donc une valeur par régime.
       Elle a ses propres noms parce qu'elle ne peut pas emprunter --green : ce
       token vaut #16A34A, l'hexadécimal exact de l'identité « dans les limites »
       (COLOR_HEX.green, imprimé par le PDF et verrouillé par
       tests/test_palette_parity.py). Le déplacer ferait dériver l'identité.
       Le troisième palier, « vieille ou sans date », reste --muted, qui a déjà
       sa valeur par régime.
       Le PION de la carte et la LIGNE D'ÂGE de la fiche portent les mêmes : une
       couleur, expliquée par la fiche, parce que les caméras n'ont pas de carte
       de calque pour tenir une légende. Sur le sol, 6,76 et 6,74 (§ 6.3). */
    --cam-fresh: #0E672F;
    --cam-ageing: #954004;

    /* Weather alert colors — reserved for operational status only */
    --yellow: #facc15;
    --yellow-rgb: 250, 204, 21;
    --orange: #f97316;
    --orange-rgb: 249, 115, 22;
    --red: #dc2626;
    --red-rgb: 220, 38, 38;
    --green: #16a34a;
    /* --notam-hazard : l'ambre de la palette d'icônes NOTAM (décision du
       propriétaire). Pas --orange ni --yellow : ces deux-là sont des IDENTITÉS
       de carte de minima, distribuées par configuration chargée, et une icône
       de NOTAM qui les emprunterait recréerait exactement la collision que la
       planche a défaite. Une valeur par thème comme --red et --notam-gives,
       même teinte (38°) sur les deux, seule la clarté bouge : 6,7:1 sur le sol
       de jour, 7,0:1 de nuit, la cible des encres d'état (docs/ui/report.md, « Les neuf encres d'état »). Le vert de l'app, lui, tombe à 3,30:1 sur le blanc : assombri ici,
       et ici seulement. */
    --notam-gives: #116731;
    --notam-hazard: #7B5105;
    --green-rgb: 22, 163, 74;
    --critical: #374151;
    /* Cold, the one colour the alert palette has no word for: the cross-section
       paints air below ISA and the freezing level, against --red for air above
       ISA. The
       palette's own blue (COLOR_PALETTE "blue", also the PDF's) — not the
       airspace blue, which belongs to airspace geometry alone. */
    --wx-cold: #2563eb;
    --wx-cold-rgb: 37, 99, 235;
    /* Fog and freezing fog, ONE pair for the map and the cross-section
       (owner's choice, 04/10, planche PREV B.2): an ochre close to the yellow
       fog of the significant-weather charts, and a violet nothing else wears
       beside it; the thin cloud's grey they wore before sank into the pale
       basemap. */
    --wx-fog-rgb: 212, 160, 23;
    --wx-fog-freezing-rgb: 142, 68, 173;
    /* The weather AT ALTITUDE, on the map. The wind's notation — the flying
       particles and the iso-tachs that grade them — is drawn in the near-black
       the app already calls --critical, never in a chart ink (those belong to
       chart objects) and never in a minima colour (those mean "which limit
       fired").

       The cloud is A RAMP OF TWO GREYS, thin to dense, mixed per block by the
       model's own percentage and weighted through the ONE cloud law
       (cloudAlpha, profile-core.js) at the operator's ONE dial. It was a single
       near-black ink at eleven transparencies, and the owner could not see his
       clouds: on pale paper a thin deck and a broken one were the same smudge,
       and on imagery the whole layer sank into the dark.

       NEUTRAL greys — r = g = b — on purpose. The vividness dial sets a
       saturation absolutely (washInkRgb), so an ink with the faintest hue would
       come out blue at the dial's own saturation; a grey has none to saturate
       towards, so this is the one ramp the dial leaves alone, by construction.

       And the ramp FOLLOWS THE GROUND: over the pale basemaps it runs light
       grey to near-black, away from the paper; over imagery it runs the other
       way, pale to white, because cloud seen from above is bright and a dark
       wash on dark tiles is no wash at all. The variant is below, on the root
       class the map sets while it is the view being looked at. */
    --wx-wind: var(--critical);
    --wx-cloud-thin-rgb: 168, 168, 168;
    --wx-cloud-solid-rgb: 38, 38, 38;

    /* The wind along the route: ONE COLOUR PER SIDE and no more — green with the
       flight, red against it, each fading to nothing at the crossing. The
       strength is the opacity; a side that changed hue as well as weight said
       "harder" twice. Each has to serve three things at once: the wash, the
       flying arrows and the iso-tach that explains them, so neither a
       near-black green nor a mint one will do. Red is shared with the minima
       boxes, which are drawn as thin outlines in the pilot's OWN configuration
       colour — a wash and a rectangle, never the same mark twice. */
    --wind-tail: #16a34a;
    --wind-tail-rgb: 22, 163, 74;
    /* Head wind is the app's own --red, not a hue of its own: a wind against
       the flight costs fuel and time, and red is the word this app already
       uses for "this one is against you". */
    --wind-head: var(--red);
    --wind-head-rgb: var(--red-rgb);

    /* The cross-section's two continuous fields, on Windy's own ramps — the
       colours a pilot has already learnt to read a temperature and a rain rate
       off, so the wash says the number before the legend does. Both are FIXED
       scales in real units (profile-core.js, FIELD_SCALES): one band per ten
       degrees from −45 to +45, and the radar rates from a drizzle to a
       downpour. Only the rgba() channels are declared — a palette that is never
       anything but a graded wash has no use for a hex sibling. */
    --wx-temp-frigid-rgb: 72, 39, 110;    /* −40 °C, deep violet */
    --wx-temp-arctic-rgb: 91, 79, 190;    /* −30 °C, violet-blue */
    --wx-temp-cold-rgb: 63, 122, 212;     /* −20 °C, blue */
    --wx-temp-chill-rgb: 69, 180, 224;    /* −10 °C, light blue */
    --wx-temp-freezing-rgb: 127, 211, 192; /*  0 °C, the pale turn at freezing */
    --wx-temp-cool-rgb: 143, 208, 106;    /* +10 °C, green */
    --wx-temp-mild-rgb: 234, 215, 87;     /* +20 °C, yellow */
    --wx-temp-warm-rgb: 239, 144, 64;     /* +30 °C, orange */
    --wx-temp-hot-rgb: 217, 68, 54;       /* +40 °C, red */

    /* The weather ladder, five steps and an open end, worn by the ceiling and
       the visibility alike — a colour means the same severity whichever number
       produced it. The middle three are the app's OWN alert colours, so a
       yellow here and a yellow on a NOTAM caret mean the same urgency; the ends
       keep the two colours the flight categories already wore. The open end has
       no ink at all: past the top bound nothing is painted (owner's call), so
       the map shows its terrain and the chart its air. */
    --wx-cat-extreme-rgb: 166, 93, 165;   /* magenta — the tightest step */
    /* UNE RAMPE EN UNITÉS RÉELLES NE CHANGE PAS AVEC LE THÈME (§ 6.4) : écrite
       en valeur, pas lue sur --red, qui s'éclaircit la nuit. */
    --wx-cat-severe-rgb: 220, 38, 38;
    --wx-cat-alert-rgb: var(--orange-rgb);
    --wx-cat-caution-rgb: var(--yellow-rgb);
    --wx-cat-fair-rgb: 90, 169, 90;       /* green — the last step that paints */

    /* Off-palette accents — deliberately outside the brand colors above */
    --favorite-bg: #fff8df;   /* starred items: warm amber wash */
    --favorite-text: #755300;
    --success-text: #14532d;   /* readable text over a light --green wash */
    --on-color-light: #fff;    /* foreground text on saturated/dark backgrounds */
    /* The own-ship symbol of the flight mode (owner's decision D1): black
       in a white halo, white in a black one on the dark theme (planche
       VOL-RETOUR), readable on a weather wash and on imagery — never a chart ink. */
    --ownship-ink: #000000;
    --ownship-paper: #ffffff;
    /* Aeronautical chart inks — the EUROCONTROL Guidance Material for the
       ICAO 1:500 000 chart, the palette a pilot already reads on the AIP VFR
       chart. Blue where entry is a clearance question and for the aeronautical
       symbols (aerodromes, navaids, reporting points); magenta for Class A
       bands and the restriction hatch; red for restriction outlines and the
       Class A tag; grey for TMZ/RMZ; green for FIS and nature areas. No brand
       colour on the chart (docs/heliwx-brand-guidelines.md, « Brand Colors »). Never for chrome — buttons and cards stay on the neutral tokens. */
    --chart-blue: #1f5fa9;
    --chart-blue-rgb: 31, 95, 169;
    --chart-magenta: #a3218e;
    --chart-magenta-rgb: 163, 33, 142;
    --chart-red: #c0392b;
    --chart-grey: #4b5563;
    --chart-green: #2e7d32;
    /* The VFR routes of an AIS (planche ENAIRE-INFO, owner's decision): a navy
       of their own, never the airspace blue nor the pilot's route magenta. */
    --chart-navy: #1d3f73;
    /* THE GROUND AGAINST THE HELICOPTER (owner's decision, planche HEMS A.3):
       red above and to 100 ft below, yellow to 500 ft below — each its own
       opacity, measured on the grounds (CIEDE2000, deuteranopia and protanopia
       simulated) so the bands part and the shading still reads under them.
       An obstacle wears its band's ink; the yellow, too pale for a stroke on
       the light paper, is darkened for it (-ink). */
    --terrain-above: #ae0003;
    --terrain-above-alpha: 0.54;
    --terrain-near: #fce700;
    --terrain-near-alpha: 0.40;
    --terrain-near-ink: #b89a00;
    /* The pilot's OWN route — the thread, its points and its halo read this
       one ink. MAGENTA, the route's colour in
       every EFB and in the cockpit (owner's decision, planche ROUTE; FAA
       AC 25-11B, colour set 1) — brighter and pinker than --chart-magenta, the
       class A band it must never be taken for, and 4.5:1 on the map's ground
       so a waypoint's name reads in it. Its line also carries a width and a
       sleeve the airspace never wears. */
    --chart-route: #c80cb8;
    /* The ground's mass under the route — the profile's relief and the marks of
       the rock it is judged against: a neutral, the grey the route wore before
       it took its magenta. On the dark theme it mirrors it (§ 8.6). */
    --chart-relief: #3e3e43;
    /* The marks the pilot wrote: the filed waypoints and the names of his
       points — the dark grey they wore before the route took its magenta
       (owner's decision): magenta stays the route's, and only the route's. */
    --chart-pilot: #3e3e43;

    /* Basemap inks — the PAPER, not an ink.
       The --chart-* family above is the aeronautical overlay; these are the
       ground it is drawn on (basemapPaper, the one paper the map is read on),
       and the rule between the two families is not taste: nothing here may
       rival a --chart-* ink, or the ground starts competing with the message
       (tests/map-core.test.js, by day and by night). A neutral paper warmed by
       a hair, woods and towns as landmarks, roads above the railway. */
    --map-ground: #f5f4f0;
    --map-water: #d5dcdf;
    --map-wood: #e1e4d6;
    --map-built: #eae8e1;
    --map-boundary: #c4c2ba;
    --map-road: #c8c5ba;
    --map-motorway: #a9a69c;
    --map-road-quiet: #e7e5de;
    --map-label: #74746c;
    /* The shading computed from Mapterhorn's elevations (basemapPaper,
       `relief`): a warm shadow and a pale light; how strong they fall is the
       paper's exaggeration, not their alpha. */
    --map-relief-shadow: #5e5b52;
    --map-relief-highlight: #fcfcfa;
    /* The official runways, past the town scale: a grey that widens with the
       ground it stands for, and its edge. */
    --map-runway: #2a2a2e;
    --map-runway-edge: #1a1a1d;
    /* The aerodrome ground of an AIS's AIXM (planche SOL A.1): taxiways and
       aprons in one white, edged in the runway's edge ink, as ENAIRE's chart. */
    --map-paved: #ffffff;
    /* A taxiway's centreline (planche SOL-OACI E.2): a thin light grey on it. */
    --map-taxi-line: #a3a8b0;
    /* Street, the town map (streetPaper), drawn after Google Maps (owner's
       decision): its own paper, grey ground, blue sea, green parks, white
       streets on a grey casing, yellow motorways and trunks on an amber one. */
    --map-street-ground: #f4f3f0;
    --map-street-built: #e9e9e6;
    --map-street-wood: #d3e8c7;
    --map-street-park: #c5e6b1;
    --map-street-water: #aad3f2;
    --map-street-road: #ffffff;
    --map-street-casing: #c9cbcf;
    --map-street-major: #fcd77f;
    --map-street-major-casing: #e3aa3c;
    --map-street-building: #e1dfdb;
    --map-street-rail: #b6b8bc;
    --map-street-label: #5f6368;
    /* The same papers at night (paperInks: --map-* → --map-night-*), drawn by
       the dark theme. Near-black ground, the sea darker and bluer, woods green
       and towns, roads and names lifted until they read — each still under the
       quietest chart ink, which keeps its day colour on this ground. The relief
       keeps its shadow dark and its light dim, or it reads inverted. */
    --map-night-ground: #272726;
    --map-night-water: #0e1820;
    --map-night-wood: #33432b;
    --map-night-built: #3b3935;
    --map-night-boundary: #5a5953;
    --map-night-road: #56544c;
    --map-night-motorway: #5e5c56;
    --map-night-road-quiet: #3a3935;
    --map-night-label: #a8a79d;
    --map-night-relief-shadow: #000000;
    --map-night-relief-highlight: #55544d;
    --map-night-runway: #dcdce0;
    --map-night-runway-edge: #f0f0f2;
    --map-night-paved: #3a3a3c;
    --map-night-taxi-line: #6e7178;
    --map-night-street-ground: #212327;
    --map-night-street-built: #2a2d31;
    --map-night-street-wood: #1f2b22;
    --map-night-street-park: #213425;
    --map-night-street-water: #17263a;
    --map-night-street-road: #45484d;
    --map-night-street-casing: #16171a;
    --map-night-street-major: #7a6534;
    --map-night-street-major-casing: #4a3d1f;
    --map-night-street-building: #30333a;
    --map-night-street-rail: #5f6368;
    --map-night-street-label: #a8adb3;
    /* The coast and land borders drawn OVER the weather (chartOutline): the
       only ground inks above the washes, so they are set by contrast with the
       ground, not by the paper's quiet: dark on pale paper, pale on a dark
       ground (:root.map-ground-dark takes the second set). Three roles
       (planche TRAIT): the coast, the soft shadow under it, the border. */
    --map-outline-coast: rgba(30, 32, 36, 0.42);
    --map-outline-shadow: rgba(30, 32, 36, 0.22);
    --map-outline-border: rgba(30, 32, 36, 0.25);
    --map-outline-coast-on-dark: rgba(255, 255, 255, 0.55);
    --map-outline-shadow-on-dark: rgba(255, 255, 255, 0.25);
    --map-outline-border-on-dark: rgba(255, 255, 255, 0.25);
    /* Roads, quiet roads and place names had been borrowing --line, --soft-line
       and --muted — interface tokens, which say nothing about a ground. The
       basemap reads only --map-*. */

    /* The halo behind every label the APP draws over the ground — aerodrome and
       navaid names, airspace tags, the attribution. A halo's job is to be the
       ground, so that text separates from it; it was hard-wired to --surface,
       i.e. white, which over dark imagery is a white glow around pale text
       rather than a separation. */
    --map-label-halo: var(--surface);
    /* The crosshair at the map's centre (planche AGE B.1): a white stroke on a
       dark halo, read on every ground and every ramp, the motion arrows' ink. */
    --map-crosshair-ink: #ffffff;
    --map-crosshair-halo: rgba(var(--shadow-rgb), 0.45);

    /* Font weight scale — 400 to 700, the four weights the font is served at
       (docs/ui/grammaire.md, « La typographie »). --fw-black is the weight a NAME is read at
       (owner's decision): the one the segmented already stood at. 700 et non
       900 : c'est le haut de l'axe servi. Demander plus ferait synthétiser un
       faux gras par le navigateur, plus baveux qu'un vrai 700. */
    --fw-regular: 400;
    --fw-medium: 500;
    --fw-semibold: 600;
    --fw-black: 700;

    /* Radii — LES ARRONDIS D'AVANT LA REFONTE (décision du propriétaire,
       docs/ui/grammaire.md, « Les arrondis ») : trois crans pour les boîtes, la pilule
       (999px) pour les contrôles qui l'étaient, le disque pour ce qui est rond. */
    --radius-lg: 18px;
    --radius-md: 12px;
    --radius-sm: 8px;

    /* Couches — sept, nommées (docs/ui/grammaire.md, « Les couches, le mouvement et les icônes ») : aucun z-index en
       chiffres. Ce qui est dessus se lit par le voile et l'anneau, jamais par
       une ombre. Un ordre local à un composant se dit en décalage de sa couche. */
    --z-page: 0;
    --z-sticky: 10;
    --z-chrome: 100;
    /* A side panel over the page (planche PANNEAU): above the chrome it may
       cover — the header slab, the capsule — and under the sheets and pages
       it opens. */
    --z-panel: 150;
    --z-sheet: 200;
    --z-overlay: 300;
    --z-toast: 400;
    --z-splash: 500;

    /* Les icônes de chrome (docs/ui/grammaire.md, « Les couches, le mouvement et les icônes ») : un seul trait, en
       --icon, dans une seule boîte. */
    --icon-stroke: 1.75px;
    --icon-box: 20px;

    /* Round icon buttons (refresh, funnel, account, drawer toggle/close, params
       chevron, "…" menus): one shared size. Only the "+" add buttons opt out and
       keep their own compact size. */
    --round-btn-size: 24px;

    /* Shared control height: every pill-shaped control (view toggles, element
       pills, WX filter pills, schedule selects) sits on this one min-height so
       adjacent controls stop drifting apart. */
    --control-h: 30px;
    /* The air between a segmented's words (planche INTER I.9). */
    --pill-gap: 16px;

    /* THE BRIEFING PANEL (planche PANNEAU B.2): one width at every size, in
       rem so it follows the text size — 375 px at 16. --free-left is where the
       chrome's zone starts: 0, or the panel's width where the Briefing is
       fixed (B.10, B.11 — the fixed block below), open or closed, so nothing
       jumps when it closes. The rail (planche RAIL A1) is part of that zone:
       --rail-w is 0 taller than wide, the rail's width wider than tall. */
    --panel-w: 23.4375rem;
    --rail-w: 0px;
    --free-left: var(--rail-w);
    /* THE FOOT BAND (planche PANNEAU B.4, planche RAIL R.1, owner's decision):
       a panel runs to the screen's bottom edge, the bar stands over it, and
       what a panel holds ends above it. The bar IS the band. */
    /* THE HOME INDICATOR'S SHARE (planche RAIL R.6, owner's ask): the bar
       keeps half of its zone under the cases, as the pill did, and lies as
       low as that allows; everything that stands on the bar reads this band. */
    --menu-safe: calc(env(safe-area-inset-bottom, 0px) * 0.5);
    /* The room the bar takes at the foot of every page — 0 where the rail
       replaces it (the landscape block below, which must come after). */
    --app-menu-clear: 52px;
    --menu-band: calc(var(--app-menu-clear) + var(--menu-safe));
    /* The content column's own side gutter (.main's padding). It lives here, not
       on .main, because the things that have to LINE UP with the column are
       fixed overlays outside it — the floating docks — and a copied number is
       how two edges drift apart. 12px on phones (see the compact block). */
    --main-gutter: 24px;
    /* THE REPORT'S CEILING (planche PANNEAU E.1): past it, the air is shared
       on both sides of the column. The pad is the gutter, or half of that air
       — one token, read by the column's padding and by every bleed that
       undoes it, so a full-bleed child gives back exactly the pad. The free
       zone is measured from the Briefing's slot (--free-left), open or
       closed, in 100vw: a classic scrollbar, where a desktop draws one, is
       the one width it does not see. */
    --report-max: 960px;
    --col-pad: max(var(--main-gutter), (100vw - var(--free-left) - var(--report-max)) / 2);

    /* ONE AIR BETWEEN TWO CARDS, IN THE WHOLE APP (owner's ask): the measure
       read on the Briefing page, which is the reference. It was said in nine
       places at three different values — 22 on the Briefing page, 12 on the
       account and the history filters, 10 on every other page, every library
       list, the briefing form and the three sheets — so the same two cards
       stood further apart depending on which door you came in by. Said once
       here, every surface below reads it. It is the gap BETWEEN cards only:
       what a page or a sheet leaves under its own bar is that surface's top
       inset and keeps its own number. */
    --card-gap: 22px;

    /* Type scale — rem-based so browser/OS text-size preferences apply.
       Pixel values assume the default 16px root. */
    --fs-2xs: 0.6875rem; /* 11px — THE FLOOR OF THE SCALE (owner's call): no text
                            smaller, the platforms' own minimum. Annotations ON a
                            drawing: SVG axis/grid/contour labels, the map's own
                            airport pills, a cell's name and unit, the hours of a
                            slice on the TAF rail and a PROB's odds */
    --fs-xs: 0.75rem;    /* 12px — footer, footnotes, source line, badges, tags, status */
    --fs-sm: 0.8125rem;  /* 13px — A FORECAST READING (owner's call, planche ÉPURE T1):
                            the values of the TAF braid read under the METAR's */
    --fs-read: 1rem;     /* 16px — A READING A PILOT READS IN FLIGHT (owner's call): the
                            METAR's values, on every screen — never shifted down with
                            the desktop scale, where the eye is furthest */
    --fs-md: 0.875rem;   /* 14px — body, briefing data, and every UI control: labels,
                            buttons, settings, chips */
    --fs-lg: 1rem;       /* 16px — inputs, messages, tabs, subheadings, icon buttons */
    --fs-xl: 1.25rem;    /* 20px — titles: airport code/name, close buttons */
    --fs-2xl: 1.5rem;    /* 24px — page and section heading */
    /* LE VERRE (planche HABILLAGE, décision du propriétaire) : trois matériaux.
       Verre fin, ce qui se touche (boutons, pilules, champs) ; verre de barre, les
       bandes qui collent (navigation, bande météo, enregistrement) ; verre épais, ce
       qui porte du contenu (feuilles, panneau, pages, menus) ; groupe, une carte de
       lignes posée dans le verre. Le liseré d'Apple borde le verre à la place du filet
       gris ; l'ombre est large et diffuse, sans ombre de contact. Le contenu n'est
       jamais en verre. */
    --glass-thin: rgba(251, 251, 253, 0.5);
    --glass-bar: rgba(251, 251, 253, 0.6);
    --glass-thick: rgba(242, 242, 246, 0.66);
    --glass-group: rgba(255, 255, 255, 0.94);
    --glass-blur: blur(16px) saturate(185%) brightness(1.05);
    --glass-thick-blur: blur(30px) saturate(180%);
    --glass-rim: inset 1px 1px 0 rgba(255, 255, 255, 1), inset -1px -1px 0 rgba(255, 255, 255, 0.75), inset 0 0 0 0.5px rgba(255, 255, 255, 0.9), inset 0 0 8px rgba(255, 255, 255, 0.55);
    --glass-line: rgba(255, 255, 255, 0.95);
    --glass-glow: rgba(255, 255, 255, 0.55);
    --glass-drop: 0 10px 40px rgba(var(--shadow-rgb), 0.1);
    --glass-drop-big: 0 20px 60px rgba(var(--shadow-rgb), 0.16);
    --glass-drop-up: 0 -10px 40px rgba(var(--shadow-rgb), 0.08);
    --glass-drop-down: 0 10px 40px rgba(var(--shadow-rgb), 0.08);
    --glass-drop-side: 10px 0 40px rgba(var(--shadow-rgb), 0.08);
    /* Le séparateur d'une ligne : --line au clair ; au sombre, un blanc translucide
       qui garde le même écart de clarté au-dessus de n'importe quel verre. */
    --row-line: var(--line);
    /* Ce qui est plein (Save, le disque du compte) : l'encre au clair, un gris
       anthracite au sombre, où rien de clair et plein ne doit briller la nuit. */
    --solid: var(--text);
    --on-solid: var(--bg);
}

/* THE DARK THEME (owner's call, docs/ui/grammaire.md, « Le thème sombre »): the one night of the
   app. Neutral grounds at the low night's emission, never black; each
   configuration wears its declared dark hex (COLOR_PALETTE), the same hue as
   by day, lightened. The root carries
   data-theme — the account's Theme (System / Light / Dark), resolved by the
   head's script before the first paint and kept current by state.js
   (applyTheme) — so one block, keyed on it, is the whole night. Every token
   below is one the light theme declared above; nothing is defined only here. */
:root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #0e0e10;
    --bg-rgb: 14, 14, 16;
    --surface: #161619;
    --surface-2: #131316;
    --surface-rgb: 22, 22, 25;
    --card: #161619;
    --card-rgb: 22, 22, 25;
    --icon: #434348;
    --text: #e6e6ec;
    --text-rgb: 230, 230, 236;
    --heading: #f4f4f8;
    --heading-rgb: 244, 244, 248;
    --muted: #9a9aa4;
    /* 4,75:1 sur une carte, le rapport du jour (décision du propriétaire). */
    --quiet: #82828c;
    --line: #232327;
    --contour: #2a2a2f;
    --soft-line: #1e1e22;
    --hover-bg: #1c1c20;
    --shadow-rgb: 0, 0, 0;
    /* LE ROUGE D'ÉTAT, éclairci pour le sol sombre. Ce n'est pas une identité de
       configuration — celles-là ont leur palette par thème (COLOR_PALETTE). Celui-ci dit ce qui SE PASSE : une donnée périmée
       (.rg-src .rg-stale), un terrain fermé. Jamais un verbe destructif ni une
       erreur de l'app (décision du propriétaire : le rouge est réservé à la
       météo) — ceux-là sont en --muted et en --status-error. Sur la surface sombre le rouge du jour mesure 3,74:1, sous le
       seuil texte : la marque « cette donnée est vieille » serait la moins
       lisible de l'écran, la nuit, quand elle compte le plus. Celui-ci mesure
       6,51:1 sur la surface, 6,68 sur le fond d'un bloc METAR, 6,95 sur le
       sol — même teinte (0°), pleine saturation, monté en luminosité seulement.
       --wind-head lit ce token : le vent contraire s'éclaircit avec lui sur fond
       sombre, ce qui est
       le sens de la correction et non un effet de bord. */
    --red: #ff6b6b;
    --red-rgb: 255, 107, 107;
    /* Même teinte (142°) et même saturation que --green, juste éclairci :
       le vert de l'app mesure 5,48 sur la surface sombre ; celui-ci garde
       l'écart avec le fond de bloc. */
    --notam-gives: #18b552;
    --notam-hazard: #d78c09;
    /* L'échelle de fraîcheur, montée pour le sol sombre : même teinte (142° et
       25°), la luminosité seule change. Sur le sol, 7,06 et 7,01 (§ 6.3). */
    --cam-fresh: #18B451;
    --cam-ageing: #F9761A;
    /* Les encres d'état qui n'avaient pas de nuit (§ 6.3) : même teinte, 7,0:1 sur le sol.
       Le lavis du favori émet au niveau du sol, jamais une page blanche dans la nuit. */
    --status-ok: #18B452;
    --status-error: #EA7C7C;
    --success-text: #2BB260;
    --favorite-text: #D89A00;
    --favorite-bg: #1E1B10;
    /* Le bleu de carte, même teinte à mi-chemin du blanc : 7,4:1 sur la nuit (planche OBST N.1). */
    --chart-blue: #6eaeff;
    --chart-blue-rgb: 110, 174, 255;
    /* Le bleu marine des routes VFR, éclairci pour la nuit. */
    --chart-navy: #9cb8e8;
    /* La route du pilote, son magenta éclairci pour la nuit ; le relief, son gris éclairci. */
    --chart-route: #ee5ce3;
    --chart-relief: #bdbdc3;
    --chart-pilot: #bdbdc3;
    /* Le symbole de position, blanc dans un liseré noir (planche VOL-RETOUR). */
    --ownship-ink: #ffffff;
    --ownship-paper: #000000;
    --glass-thin: rgba(26, 26, 30, 0.56);
    --glass-bar: rgba(16, 16, 20, 0.81);
    --glass-thick: rgba(22, 22, 26, 0.74);
    --glass-group: rgba(255, 255, 255, 0.1);
    --glass-blur: blur(16px) saturate(160%) brightness(0.8);
    --glass-thick-blur: blur(30px) saturate(150%) brightness(0.8);
    --glass-rim: inset 1px 1px 0 rgba(255, 255, 255, 0.3), inset -1px -1px 0 rgba(255, 255, 255, 0.14), inset 0 0 0 0.5px rgba(255, 255, 255, 0.16), inset 0 0 8px rgba(255, 255, 255, 0.06);
    --glass-line: rgba(255, 255, 255, 0.22);
    --glass-glow: rgba(255, 255, 255, 0.06);
    --glass-drop: 0 10px 40px rgba(var(--shadow-rgb), 0.4);
    --glass-drop-big: 0 20px 60px rgba(var(--shadow-rgb), 0.5);
    --glass-drop-up: 0 -10px 40px rgba(var(--shadow-rgb), 0.32);
    --glass-drop-down: 0 10px 40px rgba(var(--shadow-rgb), 0.32);
    --glass-drop-side: 10px 0 40px rgba(var(--shadow-rgb), 0.32);
    --row-line: rgba(var(--text-rgb), 0.07);
    --solid: #2e2e33;
    --on-solid: var(--text);
    /* The relief tint at night (planche HEMS A.3). */
    --terrain-above: #ff2f1e;
    --terrain-above-alpha: 0.44;
    --terrain-near: #fff092;
    --terrain-near-alpha: 0.52;
    --terrain-near-ink: var(--terrain-near);
}
/* The marks are drawn dark on nothing: on the dark ground they turn light. */
:root[data-theme="dark"] .auth-logo { filter: invert(1) hue-rotate(180deg) brightness(1.15); }

* { box-sizing: border-box; }

/* The hidden attribute always wins, even over author display rules
   (flex/grid containers toggled by JS). */
[hidden] { display: none !important; }

/* DÉSACTIVÉ, UNE SEULE RÈGLE POUR SES TROIS ÉCRITURES (docs/ui/grammaire.md, « Les états ») : opacité 0,45, et jamais la couleur seule — quand la place le permet,
   le libellé dit pourquoi. */
:disabled, [disabled], [aria-disabled="true"] {
    opacity: 0.45;
    cursor: default;
}

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

/* LA MARQUE DU CLAVIER, écrite une fois pour toute l'app. Un contrôle qui prend
   le focus doit le montrer, et un `outline: none` sur un champ retire la seule
   marque qu'un clavier possède : sans elle on tabule à l'aveugle.
   `:focus-visible` et jamais le focus nu — le navigateur lève le premier pour un
   clavier et le retient pour une touche du doigt, donc un pouce n'hérite pas
   d'un anneau dessiné pour un clavier. C'est ce qui fait qu'une règle de focus
   survit : posée sur le focus nu, elle finit supprimée pour la mauvaise raison.
   L'encre des titres que l'app possède déjà, pas une encre de plus : 18,5:1 sur
   le papier, 17,6:1 sur le sol sombre, au-dessus des 3:1 qu'une marque non
   textuelle doit.
   Et un décalage vers l'EXTÉRIEUR, pour que la bordure d'un champ continue de
   dire ce qu'elle dit sans que les deux marques se disputent le même trait. */
:focus-visible {
    outline: 2px solid var(--heading);
    outline-offset: 2px;
}
/* SAUF UN CHAMP DE TEXTE (décision du propriétaire) : le navigateur y lève
   :focus-visible même sous le doigt, et le carré noir encadrait chaque saisie.
   Son curseur dit déjà où l'on écrit — c'est la marque du clavier dans un champ. */
:is(input:not([type]), input[type="text"], input[type="search"], input[type="email"], input[type="password"],
    input[type="number"], input[type="tel"], input[type="url"], textarea):focus-visible { outline: none; }

/* Install prompt: shown only outside the installed PWA (see initInstallBanner).
   A full-width band in normal flow at the very top of the page, so it sits
   ABOVE the briefing and pushes it down instead of overlaying its top row. */
.install-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    /* Le creux neutre et son filet, dans les deux thèmes : un bandeau plein
       d'encre ferait, en sombre, la seule grande surface claire de l'écran
       (docs/ergonomie-efb.md, « 3.4 Soleil et nuit, mesurés »). */
    background: var(--surface-2);
    border-bottom: 1px solid var(--line);
    color: var(--text);
    font-size: var(--fs-md);
    line-height: 1.4;
}
.install-banner-text { flex: 1 1 auto; }
.install-icon {
    width: 1.1em;
    height: 1.1em;
    vertical-align: -0.18em;
    margin: 0 1px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.install-icon-fill { fill: currentColor; stroke: none; }
.install-banner-install {
    flex: 0 0 auto;
    border: 1px solid var(--contour);
    border-radius: 999px;
    background: transparent;
    color: var(--text);
    padding: 6px 12px;
    font-weight: var(--fw-semibold);
    white-space: nowrap;
}
.install-banner-close {
    position: relative;
    flex: 0 0 auto;
    border: 0;
    background: transparent;
    color: var(--muted);
    font-size: var(--fs-xl);
    line-height: 1;
    padding: 0 4px;
}
/* La croix est dessinée à 25 × 20 ; sa zone de contact fait 44 de haut. La
   bandeau ne porte rien d'autre à moins de 122 px sous elle, donc le
   débordement ne mord sur aucune cible. */
.install-banner-close::after {
    content: "";
    position: absolute;
    inset: -12px;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--app-font);
    font-size: var(--fs-lg);
    font-variant-numeric: tabular-nums;
    -webkit-font-smoothing: antialiased;
}

/* App splash: a full-screen cover with the centred logo, shown the instant the
   page paints and faded out by main.js once the briefing has loaded. Matches the
   app background so it reads as one continuous surface. */
.app-splash {
    position: fixed;
    inset: 0;
    z-index: var(--z-splash);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    opacity: 1;
    transition: opacity 0.2s ease;
}
.app-splash-logo {
    width: min(58vw, 260px);
    height: auto;
    animation: app-splash-pulse 1.4s ease-in-out infinite;
}
@keyframes app-splash-pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}
.app-splash.is-hidden {
    opacity: 0;
    pointer-events: none;
}
/* LE MOUVEMENT (docs/ui/grammaire.md, « Les couches, le mouvement et les icônes ») : rien ne bouge qui ne dise un
   changement d'état, 200 ms au plus, et TOUT s'arrête sous reduce — le pouls de
   l'écran d'ouverture compris. Une règle pour toute l'app plutôt qu'une par
   composant : un composant animé de plus ne peut pas l'oublier. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        transition-duration: 0s !important;
        animation: none !important;
    }
}

button, input, select, textarea {
    font: inherit;
}
input, select, textarea {
    font-size: var(--fs-lg);
}

button { cursor: pointer; }

/* Mobile: suppress the browser's grey tap flash so pressing a button (or any
   tappable control) doesn't recolour it. */
button, a, label, [role="button"], [data-action], [tabindex] {
    -webkit-tap-highlight-color: transparent;
}

.app-shell {
    position: relative;
    min-height: 100vh;
}

/* =========================
   THE BRIEFING PANEL (planche PANNEAU)
   ========================= */
/* OVER THE PAGE, AT EVERY SIZE (B.1, B.3, B.4): one width (--panel-w), never
   stretched to the screen — only a screen narrower than the panel cuts it —
   from the top of the screen down to above the menu band, so the menu and the
   Briefing's own button stay in reach. The page under it does not move. On
   the page's own ground (docs/ui/grammaire.md, « Le sol et les encres »), a
   hairline at its right edge, no shadow (§ 7.2). Closed, it is slid out and
   hidden, never unmounted: what it holds — values, pages, scroll — is kept
   (B.6). The slide is a change of state, 180 ms, and the global
   prefers-reduced-motion rule stops it like every other. */
.sidebar {
    position: fixed;
    top: 0;
    left: var(--rail-w);
    bottom: 0;
    width: var(--panel-w);
    max-width: calc(100% - var(--rail-w));
    z-index: var(--z-panel);
    /* En verre épais, bordé du liseré à droite (planche HABILLAGE B.7). */
    background: var(--glass-thick);
    -webkit-backdrop-filter: var(--glass-thick-blur);
    backdrop-filter: var(--glass-thick-blur);
    border-right: 0;
    box-shadow: var(--glass-drop-side), inset -1px 0 0 var(--glass-line);
    /* No top padding: the sticky head owns the top edge. At the bottom the
       panel runs under the foot band (B.4) and its content stops above it. */
    padding: 0 14px var(--menu-band);
    overflow-y: auto;
    /* Le fondu des têtes déborde sur les côtés (--head-reach) : il est coupé au
       bord du panneau, jamais une barre de défilement. */
    overflow-x: hidden;
    /* A flex column: the head on top, then the panel's cards stretching to the
       bottom so the schedule footer lands on the panel's edge. */
    display: flex;
    flex-direction: column;
    /* Same thin scrollbar as the briefing (root) scroller. */
    scrollbar-width: thin;
    scrollbar-color: rgba(var(--text-rgb), 0.28) transparent;
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 0.18s ease, visibility 0s linear 0.18s;
}
body.briefing-open .sidebar {
    transform: none;
    visibility: visible;
    transition: transform 0.18s ease;
}
/* THE STRIP A PANEL LEAVES UNCOVERED (B.8): live where it is wide enough to
   use — the panel takes no press outside itself, so the page scrolls and the
   map pans beside it. Too narrow (main.js, LIVE_STRIP_MIN_PX), a transparent
   scrim stands over it and a touch there closes the panel. */
.panel-scrim {
    display: none;
    position: fixed;
    inset: 0;
    z-index: var(--z-panel);
    background: transparent;
}
body.briefing-open.briefing-strip-narrow .panel-scrim { display: block; }
/* FIXED where a Report fits beside it (B.10; the same query, in its own
   words, as FIXED_BRIEFING_QUERY in state.js): the chrome's zone starts at
   the open panel's edge, and at the rail's when it is closed — the panel
   gives its place back, the Report recentres on it and the Profile takes
   it (owner's decision, planche METEO-RAIL R.4). main.js tells what is drawn
   at that width (hwx:free-zone). */
@media (min-width: 69.4375rem) and (min-height: 501px) {
    :root:has(> body.briefing-open) { --free-left: calc(var(--rail-w) + var(--panel-w)); }
}
/* THE PANEL IS A PAGE (B.13): its bar on top — the cross that closes it, its
   name, the account disc as its action — sticky, on the panel's ground. */
.panel-head {
    /* The bar's discs at a page bar's size (.overlay-header): the shared
       ::before zone then reads 44 px from it, no more. */
    --round-btn-size: 36px;
    position: sticky;
    top: 0;
    z-index: var(--z-chrome);
    display: grid;
    grid-template-columns: var(--round-btn-size) minmax(0, 1fr) var(--round-btn-size);
    align-items: center;
    gap: 10px;
    margin: 0 -14px;
    /* The panel stands at the true viewport top (viewport-fit=cover): the
       safe-area inset keeps the bar clear of the status bar. */
    padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 10px;
    background: transparent;
    isolation: isolate;
    --head-fade: var(--glass-thick);
    --head-reach: 40px;
}
.panel-title {
    margin: 0;
    text-align: center;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--heading);
}
.panel-head .round-icon-btn svg {
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* THE HEAD IS THE BRIEFING (planche ENREGISTRER E.3): the name a library
   object wears everywhere (600, the text's ink, like .source-name), its
   chevron that opens the library, and under it what the screen is to the
   record. « … » stands in the bar's third column. */
.panel-record {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    max-width: 100%;
    min-width: 0;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    cursor: pointer;
}
.panel-record-name { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; min-width: 0; font-weight: var(--fw-semibold); }
.panel-record-name > #setupLoadedValue { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel-record-chev { width: 18px; height: 18px; flex: none; fill: none; stroke: var(--muted); stroke-width: var(--icon-stroke); stroke-linecap: round; stroke-linejoin: round; }
.panel-record-state { color: var(--muted); font-size: var(--fs-xs); font-weight: var(--fw-regular); }
.panel-more svg circle { fill: var(--muted); stroke: var(--muted); }
/* ONE BAR AT THE PANEL'S FOOT (E.3): last in the panel's column, held on its
   foot while the cards scroll under it. Sticky reads the panel's own bottom
   padding, which already keeps the foot band clear: 0 lands it on the band,
   never on its back. It names what moved and carries the verbs; the question
   (E.4) takes its place, the same bar, never a box over the page. */
.setup-bar {
    position: sticky;
    bottom: 0;
    z-index: var(--z-chrome);
    margin-top: auto;
    margin-left: -14px;
    margin-right: -14px;
    padding: 10px 14px 12px;
    /* En verre de barre, bordée du liseré (planche HABILLAGE B.6). */
    background: var(--glass-bar);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop-up), inset 0 1px 0 var(--glass-line);
}
.setup-bar-line { display: flex; align-items: center; gap: 8px; }
.setup-ask { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
.setup-bar-text { flex: 1; min-width: 0; line-height: 1.3; }
.setup-bar-text b { display: block; font-weight: var(--fw-semibold); color: var(--text); }
.setup-bar-text span { display: block; color: var(--muted); font-size: var(--fs-xs); }
.setup-bar-btn {
    flex: none;
    min-height: 44px;
    padding: 0 16px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-weight: var(--fw-medium);
    cursor: pointer;
}
/* Plein : l'encre au clair, un gris anthracite au sombre (planche HABILLAGE B.11). */
.setup-bar-btn.is-primary { border-color: var(--solid); background: var(--solid); color: var(--on-solid); }
.setup-bar-btn[hidden] { display: none; }
@media (hover: hover) {
    .setup-bar-btn:not(.is-primary):hover { background: var(--hover-bg); }
}
/* THE POINT ON WHAT MOVED (E.3): after the card's title, or after the name the
   Source card's head shows, or after the label of the line whose page moved;
   and on the rail's Briefing case while the panel is shut, so a draft is never
   forgotten behind it. */
.settings-card.is-drifted > .settings-card-head .settings-card-title::after,
.sheet-row.is-drifted > .sheet-k:not(:has(> span))::after,
.sheet-row.is-drifted > .sheet-k > span:first-child::after {
    content: "";
    display: inline-block;
    width: 8px;
    height: 8px;
    margin: 0 0 1px 8px;
    border-radius: 50%;
    background: var(--text);
    vertical-align: middle;
}
.briefing-toggle-btn.has-draft:not([aria-pressed="true"])::after {
    content: "";
    position: absolute;
    top: 6px;
    right: calc(50% - 18px);
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--text);
}

/* One panel, two sources — Area and Route — swapped by the pilot's own pill
   rail rather than by the view. The class comes from setSidebarPanel /
   syncBodyClasses (briefing-map.js), which fall back to the view's natural
   source until a first explicit pick is stored. Only the cards marked with a
   side follow it; WX Minima and Filters are the account's under both.
   Scoped to each surface: the Briefing page follows the body class the map
   writes, the form follows its own record (.is-route-source). The two hosts
   draw the same card and must be able to sit on two different sides. */
body.sidebar-route-panel #briefingSourceMount [data-source="area"] { display: none; }
body:not(.sidebar-route-panel) #briefingSourceMount [data-source="route"] { display: none; }
.schedule-form.is-route-source #sbSourceMount [data-source="area"] { display: none; }
.schedule-form:not(.is-route-source) #sbSourceMount [data-source="route"] { display: none; }
/* On the route's side the area's rows step aside: the legs' row is the first. */
body.sidebar-route-panel #briefingSourceEditCard [data-route-points],
.schedule-form.is-route-source #sbSourceEditCard [data-route-points] { border-top: 0; }
/* THE SOURCE PAGE'S OWN DRAFT (planche SOURCES SO.7): the briefing bar's dress
   at the page's foot. The route's stands only while one of its verbs does. */
#briefingRouteBar:not(:has(#briefingRouteReloadBtn:not([hidden]), #briefingRouteDriftRow:not([hidden]), #briefingRouteSaveNewBtn:not([hidden]))) { display: none; }
.source-bar-pair:not([hidden]) { display: contents; }
/* PICKING POINTS CLOSES NOTHING (planche SOURCES SO.9): the page's foot says the
   gesture instead of the draft, the floating pill has nothing to bring back
   while the panel shows, and on a phone the page lowers into a sheet — the
   name, the last points, Done — the map live above it. */
body.route-picking #briefingRouteBar { display: none; }
body.briefing-open .route-pick-pill { display: none; }
body.route-picking.briefing-strip-narrow #sidebarPanel {
    top: auto;
    height: calc(min(42dvh, 360px) + var(--menu-band));
    border-top: 1px solid var(--line);
    border-radius: 16px 16px 0 0;
}
body.route-picking.briefing-strip-narrow .panel-scrim { display: none; }
body.route-picking.briefing-strip-narrow.route-pick-raised #sidebarPanel { height: calc(100dvh - 96px); }
/* Its head is the handle, the shell's own grabber on it, the gesture its own. */
body.route-picking.briefing-strip-narrow #briefingSourceMount .page-head { touch-action: none; user-select: none; }
body.route-picking.briefing-strip-narrow #briefingSourceMount .page-head::before {
    content: "";
    position: absolute;
    top: 6px;
    left: calc(50% - 19px);
    width: 38px;
    height: 4px;
    border-radius: 999px;
    background: var(--contour);
}
/* The bar says the gesture of the size it stands at. */
.setup-bar-text :is(.pick-hint-narrow, .pick-hint-raised),
body.briefing-strip-narrow .setup-bar-text .pick-hint-wide { display: none; }
body.briefing-strip-narrow:not(.route-pick-raised) .setup-bar-text .pick-hint-narrow,
body.briefing-strip-narrow.route-pick-raised .setup-bar-text .pick-hint-raised { display: block; }
body.route-picking.briefing-strip-narrow #briefingSourceUsersCard,
body.route-picking.briefing-strip-narrow #briefingSourceEditCard > :not(#briefingRouteCard),
body.route-picking.briefing-strip-narrow #briefingRouteCard > :not([data-route-points], .route-wp-list) { display: none; }
.panel-page[data-page="source"] { flex: 1 0 auto; }
/* THE REPORT PAGE OPENS ON ITS NOTAM FILTERS FROM THE REPORT'S FILTER DISC
   (owner's decision, planche RÉGLAGES-REPORT C.4): the card is the page's last,
   so the page keeps room at its foot to bring it under the head. */
.panel-page[data-page="display"] { padding-bottom: 60vh; }
/* The Time window says what it does, in one line, over its choices (W.1). */
.panel-page > .page-note { margin: 0 4px 10px; }
/* The briefing bar's voice, over the settings' 13 px. */
.source-bar, .source-bar .setup-bar-btn { font-size: var(--fs-lg); }
.source-bar .setup-bar-line { flex-wrap: wrap; }
.source-bar .setup-bar-text { flex: 1 1 160px; }
.source-bar-verbs { display: flex; gap: 8px; margin-left: auto; }
/* The briefing's draft is not the source's (planche SOURCES, scene 4): its bar
   steps aside while the source's page stands — unless it is asking. */
#sidebarPanel:has(.settings-content[data-page-open="source"]) #setupBar:not(:has(#setupAsk:not([hidden]))) { display: none; }

/* The form's Source card shows the Area rows or the Route rows, by the mode
   picked on its first row (L9) — the same pair of words as the sidebar. */
.schedule-form:not(.is-route-source) [data-schedule-source="route"] { display: none; }
/* ===== THE LEGAL PAGES (audit A9): the app's own ground, font and measure,
   a page header with the way back, sections numbered by the stylesheet. ===== */
.legal-page { font-size: var(--fs-md); }
.legal-head {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) 44px;
    align-items: center;
    min-height: calc(56px + env(safe-area-inset-top, 0px));
    padding: env(safe-area-inset-top, 0px) 8px 0;
    border-bottom: 1px solid var(--line);
    background: var(--surface);
}
.legal-head h1 {
    margin: 0;
    text-align: center;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--text);
    text-wrap: balance;
}
.legal-back { display: grid; place-items: center; width: 44px; height: 44px; color: var(--text); }
.legal-back svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.legal-main { max-width: 65ch; margin: 0 auto; padding: 20px var(--main-gutter) 64px; line-height: 1.55; counter-reset: legal; }
.legal-main h2 {
    margin: 28px 0 6px;
    font-family: var(--app-font);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}
.legal-main h2::before { content: counter(legal, decimal-leading-zero) "  "; counter-increment: legal; color: var(--muted); }
.legal-main p, .legal-main li { margin: 6px 0; color: var(--text); }
.legal-main a { color: var(--heading); }
.legal-main .legal-updated { color: var(--muted); font-size: var(--fs-md); }
.legal-main code { font-family: var(--app-font); font-size: var(--fs-md); background: rgba(var(--text-rgb), 0.06); padding: 1px 5px; border-radius: 4px; }

/* Fills the sidebar height as a flex column so the schedule footer can be pushed
   to the very bottom (margin-top:auto) even when the content above is short.
   flex-grow (not min-height) because the shared summary above it takes its own
   slice of the column. */
.settings-accordion {
    background: transparent;
    border: 0;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    flex: 1 0 auto;
}

.settings-content {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    gap: var(--card-gap);
    font-size: var(--fs-md);
    padding-top: 22px;
}
/* THE PAGES OF A PANEL (planche BRIEFING-PANNEAU, panel-pages.js): a line of
   summary opens the page that holds what it sums up. While one stands, every
   other card of its host steps aside, and on the Briefing page the panel's
   head with it — the page's own head, back arrow and title, is the bar. Its
   cards keep the host's one measure between them. */
.panel-page:not([hidden]) { display: flex; flex-direction: column; gap: var(--card-gap); }
[data-page-open] .settings-card:not(.panel-page .settings-card) { display: none; }
#sidebarPanel:has([data-page-open]) > .panel-head { display: none; }
#sidebarPanel:has([data-page-open]) .settings-content { padding-top: 0; }
#scheduleFormOverlay:has([data-page-open]) .overlay-header { display: none; }
/* A page's card with no head of its own starts on its first row: the hairline
   divides two rows, and there is no row above it. */
.panel-page .settings-card:not(:has(> .settings-card-head)) > .sheet-row:first-child,
/* A page whose one setting is the page (Days, Hours: owner's decision) has no
   card: its rows stand on the page's own ground, and start the same way. */
.panel-page > .plan-page-sheet > .sheet-row:first-child,
#routeWpEditMount > .settings-card:not(:has(> .settings-card-head)) > .sheet-row:first-child,
#offlineBody > .settings-card:not(:has(> .settings-card-head)) > .sheet-row:first-child,
/* The Time window's choices open its card (planche RÉGLAGES-REPORT W.2). */
.panel-page .wx-taf-modes > .sheet-row:first-child,
.panel-page .settings-card:not(:has(> .settings-card-head)) > .briefing-filter-body:first-child .wx-dropdown-btn { border-top: 0; }
.page-head {
    --round-btn-size: 36px;
    position: sticky;
    top: 0;
    z-index: var(--z-chrome);
    display: grid;
    grid-template-columns: var(--round-btn-size) minmax(0, 1fr) var(--round-btn-size);
    align-items: center;
    gap: 10px;
    margin: 0 -14px;
    padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 10px;
    background: transparent;
    isolation: isolate;
    --head-fade: var(--glass-thick);
    --head-reach: 40px;
}
.schedule-form .page-head { margin: 0; top: 0; }
.page-title { margin: 0; text-align: center; font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--heading); }
.page-back svg { fill: none; stroke: var(--muted); stroke-width: var(--icon-stroke); stroke-linecap: round; stroke-linejoin: round; }
/* WX config field row: the field takes the full width. Extra bottom margin
   opens up room before the configuration cards below. */
.config-field-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}
/* A ROW WITH NOTHING IN IT TAKES NO ROOM (owner, measured on the panel). The
   picker this row holds is opened by the card's "+" now (.picker-in-panel →
   display:none), so the row is nought pixels tall on every surface that draws
   it — while three separate rules went on giving it bottom margin. Twenty of
   those pixels stood under the WX Minima title as a hole, where every other
   card of the panel puts its first hairline flush against the head.
   Hidden at the row rather than un-margined rule by rule: there is one cause,
   the row has nothing to show, and the day a surface stands its picker in the
   card again the row and all its room come back together. */
.config-field-row:has(> .picker-in-panel) { display: none; }
.config-field-row .config-multiselect { flex: 1 1 auto; min-width: 0; }

/* The glyph a primary button leads with (the auth submit, the webcam player). */
.action-btn-icon {
    width: 16px;
    height: 16px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The build tag, in the Account row — lets the pilot read which build is
   actually loaded, so a stale PWA cache is obvious (it shows an old id). */
.app-version {
    flex: none;
    margin-left: 8px;
    color: var(--muted);
    font-family: var(--app-font);
    font-size: var(--fs-xs);
    letter-spacing: 0.02em;
}
.settings-content button,
.settings-content input,
.settings-content select,
.settings-content .param-value,
.settings-content .block-title,
.settings-content .airport-chip,
.settings-content .airport-option,
.settings-content .phenomena-chip,
.settings-content .phenomena-option {
    /* La voix des réglages : 13 px (décision du propriétaire). */
    font-size: var(--fs-sm);
}
/* Claude-style buttons: full pills, no visible border — the fill and soft
   shadow alone carry the shape. */
.primary-btn, .small-btn, .icon-btn, .danger-btn {
    min-height: 34px;
    border-radius: 999px;
    border: 0;
    background: var(--surface);
    color: var(--text);
    line-height: 1;
    box-shadow: none;
}
.primary-btn {
    background: var(--text);
    color: var(--bg);
    padding: 8px 14px;
    font-weight: var(--fw-semibold);
}
.small-btn { padding: 8px 10px; white-space: nowrap; }
.icon-btn, .danger-btn {
    min-height: 28px;
    width: 28px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
}
.danger-btn { color: var(--muted); font-weight: var(--fw-semibold); font-size: var(--fs-lg); }

/* Airport multiselect */
.airport-multiselect {
    position: relative;
    padding-bottom: 24px;
    border-bottom: 0;
    min-width: 0;
}
.airport-field {
    width: 100%;
    border: 0;
    border-radius: var(--radius-lg);
    background: var(--surface);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
    padding: 7px 10px 8px;
    box-shadow: none;
}
/* Clickable like the WX-configuration field — the whole field opens the picker. */
.airport-field {
    cursor: pointer;
    user-select: none;
}
.airport-chips:empty, .phenomena-chips:empty {
    display: none;
}
.airport-chips, .phenomena-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
}
/* Let chips flow into the same wrapped line as the label and search input
   instead of reserving their own row — avoids an empty second line when the
   chips alone don't fill the field's width. */
.airport-field .airport-chips {
    display: contents;
}
.airport-chip, .phenomena-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 22px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text);
    padding: 1px 9px;
    font-weight: var(--fw-regular);
    white-space: nowrap;
}
/* The small marks keep their drawing and take a 44px hit zone (audit A11):
   the "raw" switch on a card, a phenomenon's chip.
   THE AIRPORT PASTILLE'S CROSS IS NOT ONE OF THEM ANY MORE (owner's decision,
   measured): a pastille is 62 x 22px, so a 44px zone on its cross overflowed it
   by 11px above and below and OVERLAPPED the zone of the chip on the next line
   by 43 x 17px — the fault .claude/rules/ergonomie.md names, two zones stealing
   each other's taps, with the winner decided by document order rather than by
   aim. The whole pastille removes now, so the target is BIGGER than the zone it
   replaces and lies entirely inside what is drawn; the cross is the sign of
   what a tap does, not a second target. */
.phenomena-chip button, .rg-raw-btn { position: relative; }
.phenomena-chip button::before,
.rg-raw-btn::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
.airport-chip button, .phenomena-chip button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: transparent;
    color: var(--muted);
    padding: 0;
    line-height: 1;
    font-size: var(--fs-lg);
}
/* Airport tags: the airport field's chosen values — no contour, the code in
   the heading ink, medium. Same in every airport field (sidebar,
   schedule form, briefing history). */
/* Airport tags are pastilles on a GROUND ALONE (owner's decision): no hairline —
   that was a third contour inside a card that already has a ring and rows on
   filets — and the ground is DARKER than the card it sits on, the very ground
   the active segment of the Area / Route switch stands on. */
/* Un terrain n'est ni un titre, ni un objet de la bibliothèque, ni un minimum :
   sa pastille est en graisse régulière (décision du propriétaire), et c'est son
   fond qui la dessine. */
.airport-chip {
    background: var(--soft-line);
    color: var(--text);
    font-weight: var(--fw-regular);
}
/* Selected phenomena tags: the same neutral (--bg) fill pill, muted text —
   they read quietly and line up with the parameter label beside them. */
.phenomena-chip {
    background: var(--bg);
    color: var(--muted);
}
/* A REORDERABLE LIST IS MARKED BY THE GESTURE ITSELF (.is-reorderable, written
   by reorder.js), never listed by hand here: a list that gains or loses the
   drag cannot then leave its dressing behind, and this held the briefing's own
   chips by id while the picker's copies — the same pastilles, the same binding,
   in the area editor and the schedule form — got nothing.
   touch-action:none is what lets a touch-drag start on a chip at all: without
   it the browser reads the first pixel of travel as a scroll and takes the
   gesture away before the drag can arm. */
.is-reorderable .airport-chip { cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
/* UNDER THE FINGER, A PASTILLE SAYS SO. It carried a rule for this all along,
   on a class nothing sets (`.dragging`, against the `is-dragging` the drag
   writes), so a dragged chip looked exactly like a chip at rest — which is half
   of why the gesture read as broken even once it worked. */
.airport-chip.is-dragging { opacity: 0.45; cursor: grabbing; position: relative; z-index: var(--z-sticky); }
/* Where it would land: the text-ink rule of the waypoint rows, stood UP — a
   wrapping row is reordered along its line, so the mark that means "before this
   one" is vertical, and the end of the list is marked after the last chip. */
.is-reorderable .airport-chip.drop-target::before,
.is-reorderable.airport-chips.drop-end::after {
    content: "";
    position: absolute;
    top: 2px;
    bottom: 2px;
    width: 3px;
    border-radius: 2px;
    background: var(--text);
}
.is-reorderable .airport-chip.drop-target { position: relative; }
.is-reorderable .airport-chip.drop-target::before { left: -6px; }
.is-reorderable.airport-chips.drop-end { position: relative; }
.is-reorderable.airport-chips.drop-end::after { right: -6px; top: auto; bottom: 2px; height: 20px; }
/* A field draws at the fields' 8 px wherever it draws (its focus ring, § 8.9). */
.airport-search {
    flex: 1 1 24px;
    min-width: 24px;
    border: 0;
    border-radius: var(--radius-sm);
    padding: 5px 0;
    font-size: var(--fs-md);
    background: transparent;
    color: var(--text);
}
/* Drawn "+" glyph of the add affordances that are not system rows (the leg
   "+" of the route panel). A text "+" centres on the font's math axis, not the
   geometric middle, so it rides high in a round badge; a symmetric SVG sits
   dead-centre. Sized in em so it tracks each host's font-size, inherits the
   host colour via currentColor. */
.plus-icon {
    width: 0.72em;
    height: 0.72em;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.4;
    stroke-linecap: round;
}
.airport-search::placeholder { color: var(--muted); }
/* Once the field holds chips, collapse the idle search input so it doesn't
   wrap onto an empty second line — the label stays as the add affordance. It
   re-expands on focus for typing (clicking the field focuses it); an empty
   field keeps it visible for the placeholder. */
.airport-chips:not(:empty) ~ .airport-search:not(:focus-visible) {
    flex: 0 0 0;
    min-width: 0;
    width: 0;
    padding: 0;
}
/* Bare airport/config pickers (Settings + schedule form): the field has no
   frame; a leading "+" badge, the inline label, then the field glyph, with the
   search input collapsed until focus. The airport picker also floats its
   selected tags below the field. */
.airport-field-bare .airport-field,
.config-field-bare .config-field {
    width: 100%;
    border: 1px solid var(--bg);
    background: var(--bg);
    box-shadow: none;
    padding: 4px 8px;
    justify-content: flex-start;
    font-weight: var(--fw-medium);
}
/* THE MARK OF A LAYER GROUP: the map's and the chart's settings lead each group
   with the glyph of what it draws — the alphabet of the map, not a decoration.
   The settings pages carry no head icon (owner's call): a card reads by its
   title. The same chrome icon (§ 8.13) marks a scheduled send in the report's
   history. */
.field-lead-icon {
    width: var(--icon-box);
    height: var(--icon-box);
    flex: none;
    fill: none;
    stroke: var(--icon);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* Icon/text optical centering for the icon-led control labels. align-items:
   center centers each label's LINE box against its icon, but a line box reserves
   descender space the text may not use (all-caps "PDF" has none), so the
   letters' optical middle rides above the box middle and the icon looks low
   (measured ~2px). Trimming the box to the cap-height→baseline band makes the
   icon center on the letters instead. Scoped to these short, non-ellipsised
   labels only: on an overflow:hidden element (titles, param names) the alphabetic
   bottom edge would clip descenders. Ignored gracefully where text-box-trim is
   unsupported (falls back to today's line-box centering). */
.block-menu-label {
    text-box-trim: trim-both;
    text-box-edge: cap alphabetic;
}
/* Search stays collapsed so only the label + "+" show at rest; it expands on focus.
   16px text (--fs-lg) stops iOS from auto-zooming on focus; the placeholder is
   kept at --fs-md via ::placeholder so it still reads smaller. */
.airport-field-bare .airport-search {
    flex: 0 0 0;
    min-width: 0;
    width: 0;
    padding: 0;
    font-size: var(--fs-lg);
}
.airport-field-bare .airport-search::placeholder {
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
}
.airport-field-bare .airport-search:focus-visible {
    flex: 1 1 24px;
    min-width: 24px;
    padding: 5px 8px;
}
.airport-field-bare .airport-chips:not(:empty) {
    margin-top: 8px;
}
/* Config search input mirrors the airport one: collapsed until focus, 16px text
   (no iOS zoom) with an --fs-md placeholder. */
.config-field-bare .config-search {
    flex: 0 0 0;
    min-width: 0;
    width: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text);
    font-size: var(--fs-lg);
}
.config-field-bare .config-search::placeholder {
    color: var(--muted);
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
}
.config-field-bare .config-search:focus-visible {
    flex: 1 1 24px;
    min-width: 24px;
    padding: 5px 8px;
}
/* Shared floating-panel recipe for every settings pick-list: white surface,
   soft contour, generous rounding, floating shadow. Position/padding/layout
   stay in each dropdown's own rule below. One recipe for every dropdown in the
   app — the pick-lists below all restate this pair. */
.airport-dropdown, .phenomena-dropdown, .color-menu,
.config-dropdown, .add-param-dropdown {
    position: absolute;
    /* A sheet over the chrome, so a settings pick-list opening near the bottom
       is not clipped behind it, and over the loaded configs' identity dots
       (.color-select, on the chrome layer while open): they sit lower in the
       DOM, so at equal z-index they'd paint over the open dropdown. */
    z-index: var(--z-sheet);
    background: var(--surface);
    border: 1px solid var(--soft-line);
    border-radius: var(--radius-md);
    box-shadow: 0 0 0 1px var(--contour);
    font-size: var(--fs-md);
}
.airport-dropdown {
    left: 0;
    right: 0;
    top: calc(100% + 10px);
    padding: 6px;
}
.airport-list { display: flex; flex-direction: column; gap: 2px; max-height: 240px; overflow-y: auto; }
/* Thin scrollbars in the airport / configuration / phenomena pick lists on
   every breakpoint (the desktop-wide rule already thins scrollers on desktop;
   this reaches mobile too). The zone list is not here: it has no ceiling of
   its own — its page scrolls. */
.airport-list,
.config-list,
.phenomena-dropdown {
    scrollbar-width: thin;
    scrollbar-color: rgba(var(--text-rgb), 0.28) transparent;
}
.airport-list::-webkit-scrollbar,
.config-list::-webkit-scrollbar,
.phenomena-dropdown::-webkit-scrollbar { width: 6px; height: 6px; }
.airport-list::-webkit-scrollbar-track,
.config-list::-webkit-scrollbar-track,
.phenomena-dropdown::-webkit-scrollbar-track { background: transparent; }
.airport-list::-webkit-scrollbar-thumb,
.config-list::-webkit-scrollbar-thumb,
.phenomena-dropdown::-webkit-scrollbar-thumb { background: rgba(var(--text-rgb), 0.4); border-radius: 999px; }
/* UN TITRE DE LISTE COLLE SOUS CE QUI LE SURPLOMBE (décision du propriétaire :
   pays, catégorie, groupe d'état — dans toutes les listes). À zéro, il se colle
   au haut de CE QUI LE DÉFILE : sur la page, c'est SOUS l'en-tête collant de la
   page, donc invisible pendant tout le défilement. Mesuré : en-tête de page
   51 px, tête de feuille 46 px — deux surplombs, un seul nom, et chaque contexte
   pose le sien. */
.airport-group-header, .phenomena-category-title {
    padding: 6px 8px 3px;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    position: sticky;
    top: var(--sticky-head, 51px);
    background: var(--surface);
    z-index: var(--z-sticky);
}
.airport-option, .phenomena-option, .notam-option {
    display: grid;
    grid-template-columns: 54px minmax(0, 1fr) 26px;
    align-items: center;
    gap: 8px;
    min-height: 32px;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
}
/* A HAIRLINE PARTS TWO ROWS of a picker's list (owner's decision), as it
   parts the verbs of an action sheet; a group's title already carries its own. */
:is(.airport-option, .phenomena-option, .notam-option, .config-option)
    + :is(.airport-option, .phenomena-option, .notam-option, .config-option) { border-top: 1px solid var(--row-line); }
/* An airport row leads with its chart symbol and can end with a distance, so it
   carries two columns the phenomena rows do not. */
.airport-option { grid-template-columns: 18px 54px minmax(0, 1fr) auto 26px; gap: 6px; }
.airport-option-type { color: var(--muted); }
.airport-option-distance {
    font-size: var(--fs-xs);
    color: var(--muted);
    white-space: nowrap;
}
/* The kind's tag on the route's add sheet (planche SEL, M3): APT · WPT · RPT ·
   NAV in the distance's column, small capitals in the muted ink. The lead
   column holds the chart sign, or the filed point's colour dot. */
.airport-option-tag { letter-spacing: 0.06em; text-transform: uppercase; }
.airport-option > .color-dot { width: 10px; height: 10px; justify-self: center; }
/* Near me / country / type, above the list. Its own line under the search box,
   on the picker's ground rather than a card of its own — it narrows the list,
   it is not a second control panel. */
.airport-filters:empty { display: none; }
.airport-filters {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 6px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--soft-line);
}
.airport-filter-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
/* The type chips can outrun the picker's width: one line that scrolls sideways
   beats a block that pushes the list off the screen. */
.airport-filter-types {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
}
.airport-filter-types::-webkit-scrollbar { display: none; }
.airport-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: none;
    min-height: 26px;
    padding: 2px 9px;
    border: 1px solid var(--soft-line);
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--muted);
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    white-space: nowrap;
    cursor: pointer;
}
@media (hover: hover) {
    .airport-filter-chip:hover { background: var(--hover-bg); }
}
/* Chosen is the word ringed in ink, never an inverted fill (planche INTER I.7). */
.airport-filter-chip.active {
    border-color: var(--text);
    color: var(--heading);
}

.airport-filter-count { color: inherit; opacity: 0.7; }
.airport-filter-icon {
    width: var(--fs-md);
    height: var(--fs-md);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.airport-filter-country { flex: 1 1 120px; min-width: 120px; }
.airport-favorite-btn {
    width: 26px;
    min-height: 26px;
    padding: 0;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--muted);
    font-size: var(--fs-lg) !important;
    line-height: 1;
}
/* L'étoile est dessinée à 26 px ; sa zone de contact en fait 44. Mesuré au
   doigt : aucune autre cible dans ce carré. */
.airport-favorite-btn { position: relative; }
.airport-favorite-btn::after {
    content: "";
    position: absolute;
    inset: -9px;
}
/* Warm amber tones for starred/favourite items — intentionally not in the brand palette */
.airport-favorite-btn.active {
    background: var(--favorite-bg);
    color: var(--favorite-text);
}
@media (hover: hover) {
    .airport-favorite-btn:hover {
        background: var(--hover-bg);
    }
}
.color-option.active {
    background: var(--hover-bg);
}
@media (hover: hover) {
    .airport-option:hover,
    .color-option:hover {
        background: var(--hover-bg);
    }
    .phenomena-option:hover, .notam-option:hover {
        background: var(--hover-bg);
    }
}
/* LA LIGNE DES DEUX LISTES NOTAM, ET C'EST CELLE DES TERRAINS (décision du
   propriétaire) : le code dans sa colonne, le nom dans la sienne, le contrôle au
   bout — la règle au-dessus est la même pour les trois, donc aucune ne dérive
   seule. Trois écarts, et c'est le contenu qui les impose : une ligne sans code
   (les Special) perd la colonne au lieu de la laisser vide, une rangée maître
   met sa poignée et sa marque d'état à la place du code, et la colonne de queue
   fait la largeur de l'interrupteur. La colonne de code fait 24 px et non les 54
   des terrains : un code NOTAM a DEUX lettres, et une colonne taillée pour les
   quatre d'un code OACI laissait trente pixels de rien entre le code et son mot.

   L'INTERRUPTEUR RESTE L'INTERRUPTEUR (décision du propriétaire) : c'est le
   FORMAT de la ligne qui prend celui des terrains, pas le contrôle. Le bouton
   n'est plus que le rail — son libellé a quitté son ventre pour la colonne qui
   l'aligne —, donc il n'a plus à réserver sa place à droite : la grille la lui
   donne. */
.notam-option { grid-template-columns: 24px minmax(0, 1fr) 48px; }
.notam-option.is-nocode { grid-template-columns: minmax(0, 1fr) 48px; }
.notam-option-group { grid-template-columns: auto minmax(0, 1fr) 48px; }
/* LARGEUR FIXE, PARCE QUE C'EST ELLE QUI ALIGNE LES DEUX ÉTAGES — mais fixe
   PAR PANNEAU, pas dans toute l'app : States porte une marque d'état devant son
   mot (poignée 22 + écart 6 + marque 18), Categories n'en a pas (poignée 22
   seule). Le même nombre pour les deux laissait, côté Categories, vingt-quatre
   pixels de rien entre la poignée et le titre du groupe. Un groupe sans poignée
   — Special — garde la cellule de son panneau, donc son mot tombe à la même
   abscisse que celui de tous les autres. */
.notam-option-lead { display: inline-flex; align-items: center; gap: 6px; width: 22px; }
.notam-option-lead:has(> .notam-icon) { width: 46px; }
.notam-option-group > .notam-option-name { font-weight: var(--fw-medium); }

.airport-option-code, .phenomena-option-code, .notam-option-code { font-weight: var(--fw-semibold); color: var(--text); }
.airport-option-name, .phenomena-option-name, .notam-option-name {
    min-width: 0;
    color: var(--text);
}
/* SEUL LE NOM D'UN TERRAIN SE TRONQUE (docs/ui/grammaire.md, « Ce qui se tronque, et ce qui ne se tronque jamais ») : le code
   ICAO l'identifie, et le nom entier se lit dans la fiche. Un phénomène ou un
   sujet NOTAM n'est pas un nom secondaire : il revient à la ligne. */
/* UNE SEULE FAÇON DE TRONQUER (docs/ui/grammaire.md, « Ce qui se tronque, et ce qui ne se tronque jamais ») : les NOMS
   SECONDAIRES — terrain, route, zone, dossier, preset, briefing, waypoint,
   source — cèdent sur une ligne, points de suspension, et leur nom complet
   reste au toucher ou dans l'éditeur. Jamais un code ICAO, une lecture, une
   heure, un verdict ni un libellé d'action : là, la mise en page cède. */
.airport-name, .settings-card-title, .library-name, .route-wp-code-text, .source-name,
.airport-option-name, .phenomena-option-label, .config-option-title,
.block-title, .block-title-static .block-title-name, .history-schedule span,
.sheet-k:has(.sheet-k-name), .sheet-v, .route-pick-hint, .route-wp-name-text,
.route-group-name, .route-point-menu-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.phenomena-option-name, .notam-option-name { overflow-wrap: anywhere; }
/* A phenomenon's name shares its cell with the "Critical" tag, which reads at
   the right edge of the row: the cell becomes the row, the name keeps the
   ellipsis (its own box now) and the tag is pushed out to the end. */
.phenomena-option-name {
    display: flex;
    align-items: center;
    overflow: visible;
}
.phenomena-option-label {
    min-width: 0;
}
.phenomena-option-name > .phenomena-critical-tag { margin-left: auto; }

.config-multiselect {
    position: relative;
    min-width: 0;
}
.config-field {
    width: 100%;
    border: 0;
    border-radius: var(--radius-lg);
    background: var(--surface);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 7px;
    padding: 7px 10px 8px;
    box-shadow: none;
}
.config-field {
    cursor: pointer;
    user-select: none;
}
.config-dropdown {
    font-size: var(--fs-md);
    left: 0;
    /* Match the field width (sidebar): the multiselect spans the whole row, so
       the dropdown must not spill past it or it overflows the sidebar. */
    right: 0;
    top: calc(100% + 10px);
    padding: 6px;
}
.config-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: 240px;
    overflow-y: auto;
}
.config-option {
    display: grid;
    /* WHAT IT IS | WHAT CAN BE DONE WITH IT. Two columns and no more, whatever
       the row carries: every control lives in the right-hand group, so the star
       cannot be pushed onto a second line by a control added beside it — which
       is exactly what the check did (owner: the star belongs at the right). */
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
    min-height: 32px;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.config-option-main { min-width: 0; }
.config-option-actions { display: flex; align-items: center; gap: 2px; }
/* Section titles mirror the airport dropdown's country headers: small,
   uppercase, letter-spaced, muted, and sticky within the scrolling list. */
.config-section-title {
    padding: 6px 8px 3px;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    position: sticky;
    top: var(--sticky-head, 51px);
    background: var(--surface);
    z-index: var(--z-sticky);
}
.config-empty-option {
    padding: 7px 8px;
    color: var(--muted);
    font-size: var(--fs-md);
}
@media (hover: hover) {
    .config-option:hover { background: var(--hover-bg); }
}

.config-option-title {
    min-width: 0;
    color: var(--text);
}
/* The row's own summary: the thresholds it offers, one line, cut rather than
   wrapped — the card's strip has room to wrap, a 44px row has not. */
.config-option .config-summary {
    flex-wrap: nowrap;
    gap: 0 12px;
    padding: 0;
    overflow: hidden;
}
.config-option .config-summary-item { font-size: var(--fs-xs); white-space: nowrap; }
.config-option .config-summary-value { color: var(--muted); font-weight: var(--fw-regular); }
/* THE ROW'S ACTIONS ARE 44 px TARGETS (§ 5.6): the glyph and its wash stay a
   26 px tile — the wash is clipped to the content box — and the finger gets
   the whole 44, side by side, none stealing its neighbour's. */
.config-favorite-btn,
.config-details-btn,
.config-delete-user-btn {
    width: 44px;
    min-height: 44px;
    padding: 9px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    background-clip: content-box;
    color: var(--muted);
    font-size: var(--fs-lg) !important;
    line-height: 1;
}
.config-favorite-btn.active {
    background-color: var(--favorite-bg);
    color: var(--favorite-text);
}
@media (hover: hover) {
    .config-favorite-btn:hover {
        background: var(--hover-bg);
    }
}
.config-delete-user-btn,
.config-details-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
}
@media (hover: hover) {
    .config-delete-user-btn:hover {
        background: var(--hover-bg);
    }
}
.config-origin-tag {
    display: inline-flex;
    align-items: center;
    margin-left: 5px;
    padding: 2px 8px;
    border-radius: var(--radius-md);
    /* Soft fill, heading ink — the selected-pill treatment, so where a
       configuration comes from reads as a badge rather than a stray word. */
    background: var(--soft-line);
    box-shadow: none;
    color: var(--heading);
    font-size: var(--fs-xs);
    font-weight: var(--fw-regular);
    letter-spacing: 0.02em;
    text-transform: capitalize;
    vertical-align: middle;
}

/* Config blocks (card container is reused as-is by the schedule form). */
.config-block {
    position: relative;
    overflow: visible;
    border: 0;
    border-radius: var(--radius-md);
    background: transparent;
    margin-bottom: 10px;
    box-shadow: none;
}
/* Configuration cards, base look: no fill (the ground shows through), a
   contour — head and body alike, no shadow. The settings sidebar
   (#configBlocks) only stretches them full width — see the SETTINGS SECTION
   CARDS block at the end of this file. */
.config-blocks .config-block {
    /* Capped and centered in its container (the sidebar column, or the
       schedule form's own column) instead of stretching edge to edge. */
    max-width: 300px;
    margin: 0 auto 12px;
    padding: 2px 0 10px;
    border-radius: 0;
    background: transparent;
    border: 0;
    box-shadow: none;
}
/* Collapsed: the summary carries its own bottom padding, so the card adds
   almost none — the space below the last value would read as an empty body. */
.config-block.collapsed { padding-bottom: 2px; }
.config-head {
    display: grid;
    /* title | actions. The grip stands outside the head, beside the whole
       block — see .config-lead. */
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 4px;
    padding: 8px 9px;
    border-bottom: 0;
    background: transparent;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
}
/* Sidebar head: transparent, left-flush, square (no rounding), no shadow. Its
   line — the title badge and the chevron — is vertically centered; a small 2px
   bottom padding sets the summary just under it. */
.config-blocks .config-head {
    /* The title, and nothing else: the chevron stands beside the whole block. */
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    gap: 0;
}
/* THE CHEVRON IS CENTRED ON THE BLOCK IT OPENS (owner's ask), like the grip
   facing it: it stretches the block's full height and centres its glyph inside,
   instead of riding the title line eleven pixels above the middle. No inset on
   its right: it ends where every other card's chevron ends (owner's ask). The summary stops before it now rather than running
   underneath, which is what a value should do next to a control. */
.config-lead > .block-actions {
    align-self: stretch;
    padding-left: 4px;
}
/* A MINIMUM UNDER THE FINGER SAYS SO TOO, in the waypoint rows' own words: the
   block lifts onto the app's ground, and a text-ink rule marks the slot it would
   drop into — above the configuration it would push down, or under the whole
   list for the last place. Without them the drag moved a card that looked
   exactly like a card at rest, and only the release told you whether it had
   worked. */
.config-block.is-dragging {
    position: relative;
    z-index: var(--z-sticky);
    background: var(--bg);
    opacity: 0.85;
}
.config-block.drop-target::before {
    content: "";
    position: absolute;
    top: -2px;
    left: 0;
    right: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--text);
}
.config-blocks.drop-end { position: relative; }
.config-blocks.drop-end::after {
    content: "";
    position: absolute;
    bottom: -2px;
    left: 0;
    right: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--text);
}
/* THE GRIP STANDS BESIDE THE WHOLE BLOCK (owner's ask), not inside its first
   line: the head and the summary are one column, and the grip runs down the
   left of both. Two things come out of it. The glyph is centred on everything
   the minimum says instead of riding the title alone — and the name and its
   thresholds start at the same pixel again, the rule this card has always been
   drawn by (the head and the summary both run to the block's own left edge, so
   nothing here has to inset them). */
.config-lead { display: flex; align-items: stretch; }
/* The 2px settle under the summary is the COLUMN's, not the block's, now that
   the block is a row: the last band a reader of this card sees is this column,
   and a floor left on the block would sit below it — a card ending on 8px where
   every other card of the briefing ends on 6px. Nothing moves on screen: the
   same two pixels, one level down. */
.config-lead-main { flex: 1; min-width: 0; padding-bottom: 2px; }
/* The reorder grip: the same braille glyph as the waypoint rows', in the head's
   own muted, and the same claim on a vertical touch so the sidebar does not
   scroll out from under a drag. It is the ONLY way a minima card is dragged, so
   it takes the FULL height of the block it stands beside — the glyph centred on
   it, the finger given every line of it — instead of the one line of the head,
   which left a target twenty pixels tall for the one gesture that has to work
   with gloves on.

   NO PADDING ON THE LEFT (owner's ask): the GLYPH starts where the hairline
   that separates two configurations starts, measured at the ink and not at the
   box — a padding of its own put the braille dots ten pixels inside a line that
   runs to the block's own edge. The width the finger needs is kept by min-width
   and given back on the right, so the name still clears the grip; centring the
   glyph in that box would put it back in the middle of nowhere, hence
   flex-start. */
.config-grip {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    align-self: stretch;
    /* VINGT-DEUX, ET NON TRENTE (décision du propriétaire) : le glyphe fait
       neuf pixels et la boîte en réservait trente, donc vingt et un de blanc
       séparaient la poignée du nom — assez pour que la carte ait l'air de
       commencer deux fois. La CIBLE ne rétrécit pas pour autant : cette poignée
       prend TOUTE LA HAUTEUR du bloc qu'elle borde (align-self: stretch), donc
       le doigt garde une bande de 22 px sur une soixantaine de haut. Ce qui est
       repris est du blanc, pas de la prise.
       PAS DE ZONE ÉLARGIE VERS LA DROITE : elle passerait sous le titre, et un
       appui sur le titre ouvre la fiche quand un appui sur la poignée n'ouvre
       rien — la bande gagnée serait une bande morte. */
    min-width: 22px;
    padding: 2px 4px 2px 0;
    color: var(--muted);
    font-size: var(--fs-md);
    line-height: 1;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
}
/* The title badge IS the colour picker now, so it takes a button's reset and
   the pointer that says so — the wash and ink still come from the block. */
.config-head .block-title-text {
    border: 0;
    font: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
}
/* LA CARTE DES MINIMA PARLE COMME LES AUTRES CARTES (décision du propriétaire) :
   ses titres et ses seuils dans la voix des réglages, 13 px — le titre dans sa
   couleur, le seuil dans l'encre des titres. */
.config-lead.sheet-row { font-size: var(--fs-sm); }
/* The colour dot vertically centered with the rest of the line. */
.config-blocks .config-head .color-select { align-self: center; }
/* No gap between the colour pastille and the title (the pastille button carries
   its own lateral padding). */
.config-blocks .config-head .block-title { padding-left: 0; }
/* Read-only title (text + "Standard"/operator tag): drop the vertical padding
   and min-height the editable title-input still needs, so its line-box
   matches the round icon buttons' tight 24px row instead of sitting in a
   taller box that centers off from them. */
.config-blocks .config-head .block-title-static {
    padding-top: 0;
    padding-bottom: 0;
    /* Flush left on every breakpoint (the mobile head re-inset it by 2px): the
       identity bar starts on the block's own edge, and the summary below is
       measured from that same edge. */
    padding-left: 0;
    min-height: 0;
}
.config-head .color-select {
    align-self: start;
}
.block-title {
    display: flex;
    align-items: center;
    min-height: 25px;
    width: 100%;
    border: 0;
    border-radius: 999px;
    background: transparent;
    padding: 3px 5px;
    min-width: 0;
    color: var(--muted);
    font-family: var(--app-font);
    font-size: var(--fs-md) !important;
    font-weight: var(--fw-regular);
    letter-spacing: 0;
}
/* Read-only (standard/operator) title: keep the origin tag in the same container as
   the title text but let it wrap instead of being truncated. The title text
   ellipsizes; the tag never shrinks and drops to a second line if there is no
   room beside it. */
.block-title-static {
    flex-wrap: wrap;
    overflow: visible;
    white-space: normal;
    row-gap: 1px;
    column-gap: 5px;
}
/* Le nom long se tronque sur son propre texte, pas sur le bouton : un bouton
   qui rogne son contenu rognerait aussi sa zone de contact. */
.block-title-static .block-title-text {
    flex: 0 1 auto;
    min-width: 0;
}
.block-title-static .block-title-name {
    min-width: 0;
}
.title-input {
    width: 100%;
    min-height: 25px;
    border: 0;
    border-radius: 999px;
    padding: 3px 7px;
    background: transparent;
    font-size: var(--fs-lg);
    font-weight: var(--fw-regular);
}
.config-body { padding: 6px 9px 10px; font-weight: var(--fw-regular); }
/* Configuration cards read as one fill-free block on every breakpoint: the
   head and the parameters panel below it are both transparent inside the
   card's white contour, so head and body read as one card. Shared by the
   settings sidebar and the schedule form — both containers carry
   .config-blocks, so one selector covers both. The head's hardware — grip,
   pencil, "…", the enable switch — is ALL muted: the badge already says which
   configuration this is, in its own colour, and a line that repeated that
   colour five times said it five times over. Only the badge is coloured.
   (--config-ink is still what the full-screen editor's fields read.) */
.config-blocks .config-head {
    /* A card opens with air above its badge, then the row, then a 2px settle
       onto the summary below. NO LATERAL PADDING AT ALL: the head runs edge to
       edge on both sides of its column — flush left with the summary under it
       and with the hairline above, and flush right because the chevron that the
       8px used to inset now stands OUTSIDE this column, beside the whole block,
       and carries that inset itself. The floor comes down with the ceiling:
       leaving it where it was would have re-centred the row inside a box that
       never shrank, giving back half of the 2px taken off the top. */
    padding: 8px 0 2px;
    min-height: calc(var(--round-btn-size) + 2px);
    border-radius: 0;
    border: 0;
    background: transparent;
    position: relative;
    color: var(--muted);
}
.config-blocks .config-head .block-title {
    color: var(--muted);
}
/* The row opens on a tap (audit A10); its grip shows only while the list is
   being reordered (the "Reorder" item of its menu), in the sidebar's list. */
.sidebar .config-block.collapsed .config-head,
/* The row is the button on both surfaces now, so it says so on both. */
.config-block.collapsed .config-summary { cursor: pointer; }
/* Collapsed summary: the minima inline under the head — each parameter as
   its label (muted) with its value (heading ink) right beside it. A pair
   never breaks apart: the label is one no-wrap unit and stays attached to
   its value's first line; the strip wraps between pairs. */
.config-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 14px;
    padding: 2px 10px 8px;
}
.config-summary-item {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    font-size: var(--fs-sm);
    min-width: 0;
}
.config-summary-label { color: var(--muted); white-space: nowrap; }
.config-summary-value {
    color: var(--heading);
    font-weight: var(--fw-medium);
    min-width: 0;
}
.config-blocks .config-body {
    /* Symmetric: the editable parameter rows are full-width fields, so a right
       padding of 0 ran them into the card's own edge. */
    padding: 2px 8px 0;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
}
.param-list { display: flex; flex-direction: column; gap: 0; }
/* EVERY PARAMETER IS A SYSTEM ROW (owner's decision, planche de choix 2):
   the name at the left, the value at the right, the chevron — the same line
   as every other choice of the app. Editable rows open their value list;
   removing the parameter is the last line of that list. */
/* The threshold is active data, so it may take the text ink — at the row's
   own 400, never a heavier value (§ 7.2). */
.param-row .sheet-v.param-value {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
    color: var(--text);
    font-weight: var(--fw-regular);
}
.param-unit { color: var(--muted); font-size: var(--fs-md); font-weight: var(--fw-regular); }
/* An editable row IS the button that opens its list — label, value, caret and
   the blank between them (the row carries the data-action; read-only rows carry
   none and stay inert). Only the controls it holds of its own — the cloud type
   — take their own click first. */
.param-row[data-action] {
    cursor: pointer;
    user-select: none;
}
/* Edit-mode footer of a configuration card: "+ Parameter" takes the free
   width with Done beside it, so leaving edit mode sits right where the last
   edit happened (the head's pencil is hidden while editing). */
.config-edit-actions {
    /* One control wide: "+ Parameter" is the only action the row carries, the
       panel's close being what ends the edit. */
    display: grid;
    grid-template-columns: 1fr;
    align-items: center;
    gap: 8px;
    margin-top: 8px;
}
.config-edit-actions .add-param-wrap { min-width: 0; }
.add-param-wrap {
    position: relative;
    /* No extra gap above "Parameter +": .param-list rows are gap:0 already
       (each relies on its own min-height for rhythm), so this row matches
       instead of standing off from the last parameter name. */
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}
.add-param-dropdown {
    font-size: var(--fs-md);
    left: 0;
    right: 0;
    bottom: calc(100% + 5px);
    top: auto;
    padding: 5px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.add-param-option {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    min-height: 30px;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    color: var(--text);
    font-weight: var(--fw-regular);
    cursor: pointer;
}
@media (hover: hover) {
    .add-param-option:hover {
        background: var(--hover-bg);
    }
}

/* Color chooser */
/* At rest the dot carries no z-index, so it never overlaps the sticky
   sidebar header/footer (the chrome layer) while scrolling. Only once its
   menu is open does it need to clear them, else the open colour menu would
   render trapped behind them. */
.color-select { position: relative; display: flex; align-items: center; }
.color-select:has(.color-menu) { z-index: var(--z-chrome); }
.color-select-button {
    min-height: 26px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 7px;
}
/* La pastille de couleur est dessinée à 14 × 26 ; sa zone de contact fait au moins 44 × 44.
   Mesuré au doigt : aucune autre cible dans ce débord. */
.color-select-button { position: relative; }
.color-select-button::after {
    content: "";
    position: absolute;
    inset: -9px -15px;
}
.color-menu {
    left: 0;
    top: calc(100% + 5px);
    padding: 4px;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 2px;
}
.color-option {
    width: 28px;
    min-height: 28px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}
/* Every identity pastille (config dots, color-picker swatches and airport
   status dots alike) wears the identity's OWN ink, flat: identityDotStyle()
   writes that one colour into all three properties below. The three stay
   separate because the pilot's filed waypoints still use them apart — a wash
   fill inside a walked contour (dotStyle(), route-profile.js), which is right
   for a hex he picked and wrong for a configuration's identity. */
.color-dot, .status-dot {
    display: inline-block;
    width: 14px;
    height: 14px;
    box-sizing: border-box;
    border: 1px solid var(--dot-ink, rgba(var(--text-rgb), 0.14));
    background: var(--dot-fill, var(--dot-color, var(--green)));
    box-shadow: none;
    background-image: none;
}
/* The disc is the colour dot's alone (the closed list of §5.1); the status
   dot is a small mark, so it takes the mark's radius. */
.color-dot { border-radius: 999px; }
.status-dot { border-radius: 999px; }
.color-dot-empty {
    background: transparent;
    border: 0;
}

/* Phenomena multiselect */
.phenomena-multiselect { position: relative; min-width: 0; }
/* The phenomena row: the name at the left, the chosen tags gathered at the
   right before the chevron, wrapping onto further lines when they must. */
.param-row.phenomena > .phenomena-multiselect { flex: 1 1 auto; min-width: 0; }
.phenomena-field {
    border: 0;
    background: transparent;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 10px;
    width: 100%;
    padding: 0;
}
.phenomena-field .phenomena-chips {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 4px;
    margin-left: auto;
    min-width: 0;
}
.phenomena-field .sheet-k + .sheet-chev { margin-left: 0; }
.phenomena-field .phenomena-chips:empty + .sheet-chev { margin-left: auto; }
.param-row-static.phenomena > .phenomena-chips {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 4px;
    margin-left: auto;
}
/* Anchor the picker to the row and cap it at a readable width. */
.param-row.phenomena .phenomena-dropdown { right: auto; width: min(280px, 82vw); }
/* The list's last row removes the parameter: the muted register, no red. */
.phenomena-remove { color: var(--muted); border-top: 1px solid var(--row-line); margin-top: 4px; }
/* The list styling (options, headers, hover/selected) is shared with the
   airport dropdown; only the container padding/anchor is set here to match it. */
.phenomena-dropdown {
    left: 0;
    right: 0;
    top: calc(100% + 10px);
    padding: 6px;
    max-height: 240px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
/* MARQ A.2 : plus de colonne de marque. Ce qui est retenu porte le filet des
   terrains, pas une coche — la liste des phénomènes et celle des terrains sont
   le même geste, on part de rien et on ajoute. */
.phenomena-option { grid-template-columns: 54px minmax(0, 1fr); }
.phenomena-option-preset {
    grid-template-columns: minmax(0, 1fr);
    font-weight: var(--fw-medium);
}
.phenomena-option.disabled {
    opacity: 0.42;
    cursor: not-allowed;
}
.phenomena-option.critical-match {
    background: rgba(var(--bg-rgb), 0.58);
}
.phenomena-critical-tag { color: var(--muted); text-align: right; }

/* =========================
   MAIN / BRIEFING
   ========================= */
.main {
    width: 100%;
    max-width: none;
    margin: 0 auto;
    /* The pad is a :root token (a full-bleed child gives back exactly this
       much; the floating docks measure their width from the gutter). The
       column starts at the chrome's zone (--free-left, planche PANNEAU B.11):
       right of the fixed Briefing's place, whether it is open or closed. */
    padding: 0 var(--col-pad) calc(var(--app-menu-clear) + 32px) calc(var(--free-left) + var(--col-pad));
}
/* The header row floats: it sticks to the top on scroll while the briefing
   slides underneath. On the Report it is an opaque SLAB on the ground (its own
   rule below); on the Map it is a --surface band over the picture. */
.page-header {
    position: sticky;
    top: 0;
    z-index: var(--z-chrome);
    display: flex;
    align-items: flex-start;
    justify-content: flex-end;
    gap: 18px;
    /* Once stuck at the true viewport top (viewport-fit=cover), the extra
       safe-area padding pushes the buttons clear of the notch/status bar
       while the bar's own background still reaches all the way up behind it —
       content scrolls under the clock/battery instead of stopping short with a
       flat seam. */
    padding-top: calc(8px + env(safe-area-inset-top, 0px));
    padding-bottom: 8px;
    /* Tight under the header's last row — the first airport title is what the
       eye goes to; the first card carries its own top padding on top of this
       (see .airport-card:first-child). */
    margin-bottom: 4px;
    background: transparent;
}
/* ---- The Report's sticky slab ----
   ONE object at the top of the Report, and everything the reading needs is a
   line of it: the page rail, then the sources and the view, then the airports,
   then the six column heads. The report scrolls BEHIND it — a pilot keeps the
   column heads and the airport index on screen while the values run under
   them. It has no ground and no rule (planche HABILLAGE B.2): its controls
   float in glass and the report blurs away under them, on the fade below. */
body:not(.map-view-open) .page-header {
    /* À LA MANIÈRE DE CLAUDE SUR IPHONE (planche HABILLAGE B.2) : ni fond ni
       bordure, seuls les boutons flottent ; le fondu de ::after, plus bas, efface
       ce qui défile dessous. */
    background: transparent;
    isolation: isolate;
    /* The slab runs edge to edge behind the content column's pads — a band
       that stopped at the text would leave the report visible beside it. */
    margin-inline: calc(-1 * var(--col-pad));
    padding-inline: var(--col-pad);
}
/* LE FONDU SOUS LES EN-TÊTES (planche HABILLAGE B.2) : ce qui défile se floute et
   s'efface vers le fond de la page (--head-fade, le verre dans une feuille ou un
   panneau), sur une bande qui déborde de 16 px sous l'en-tête. En haut de page,
   rien ne se voit. La carte n'en a pas : elle va jusqu'au bord. L'en-tête d'une
   feuille le porte sur ::before, son ::after servant de cale au titre. La
   ligne d'un bloc ne le porte que pleine, quand elle colle : vide, elle n'a plus
   de boîte et le fondu couvrait tout le bloc. */
body:not(.map-view-open) .page-header::after,
.briefing-zone.is-shut .briefing-zone-head::after,
.briefing-zone:not(.is-shut) .briefing-zone-sub:has(> :not([hidden]))::after,
.panel-head::after,
.page-head::after,
.overlay-header::before {
    content: "";
    position: absolute;
    /* Jusqu'aux bords de ce qui le porte (--head-reach) : un fondu qui s'arrêtait
       au bord de la tête laissait le contenu net de part et d'autre. */
    inset: calc(-1 * var(--head-rise, 0px)) calc(-1 * var(--head-reach, 0px)) -16px;
    /* Sous le contenu de l'en-tête, qui l'isole (isolation: isolate). */
    z-index: calc(var(--z-page) - 1);
    pointer-events: none;
    background: linear-gradient(to bottom, color-mix(in srgb, var(--head-fade, var(--bg)) 88%, transparent) 0%, color-mix(in srgb, var(--head-fade, var(--bg)) 55%, transparent) 50%, transparent 100%);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    /* Un masque ne lit que l'opacité : n'importe quelle encre pleine convient. */
    -webkit-mask-image: linear-gradient(var(--text) 45%, transparent);
    mask-image: linear-gradient(var(--text) 45%, transparent);
}
/* SOUS LE SOMMAIRE DU REPORT, LE FONDU EST PLEIN JUSQU'AU BORD DE L'EN-TÊTE
   (planche SOMMAIRE) : le sommaire se lit net sur ce qui défile dessous, et rien
   ne déborde — la ligne qui colle juste sous l'en-tête (légende des minima,
   étendue NOTAM) ne passe pas dans son voile, elle porte le sien. */
body:not(.map-view-open):not(.route-profile-open) .page-header::after {
    background: linear-gradient(to bottom, color-mix(in srgb, var(--head-fade, var(--bg)) 92%, transparent), color-mix(in srgb, var(--head-fade, var(--bg)) 85%, transparent));
    -webkit-mask-image: linear-gradient(var(--text) calc(100% - 16px), transparent calc(100% - 16px));
    mask-image: linear-gradient(var(--text) calc(100% - 16px), transparent calc(100% - 16px));
}
h1 {
    margin: 0;
    font-size: var(--fs-2xl);
    line-height: 1.05;
    letter-spacing: -1px;
}
.header-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
    /* Header round icon buttons (settings toggle, mobile PDF disc) run bigger
       than the pills beside them: 40px tap targets on the page's primary row.
       Scoped token override — the .round-icon-btn base rule and the mobile PDF
       disc rule both size off --round-btn-size. Declared on the whole header
       block, not just its top row, because the rows UNDER the rail have to
       reserve the same slot to stay on its axis (see the phone rule below). */
    --round-btn-size: 40px;
}
/* Top row: tools (settings, mobile only) on the left; the refresh/"…" actions
   cluster (and, on desktop with room, the WX filter pills right after it)
   pushed to the far right via its own auto left margin — so the gap tracks
   whatever is actually visible on the left. flex-wrap lets the WX filter
   dropdowns (#briefingWxFilters, a child here too) drop to their own
   full-width line without disturbing the other items — see its own rule below
   and the container query that pulls it inline. The report's own two rails do
   not live in this row either — they stand in #reportViewSlot (placeReportRails). */
/* Three zones: settings tools (left), the Briefing|Map view switcher (dead
   centre), the refresh/PDF actions cluster (right). Equal 1fr side columns keep
   the switcher centred in the whole header regardless of what each side holds
   (the settings toggle is desktop-hidden; the WX filter pills are relocated to
   the sidebar at startup, so they never occupy a cell here). */
.header-top {
    display: grid;
    /* Icon phase (the phone): the drawer handle's slot, the rail, then the
       PDF disc's slot. The side slots are held open at the buttons' own width
       even when one is empty (the PDF disc leaves with the Report page), so
       the rail keeps ONE width across all pages. The wider phases put the
       rail back in the middle (min-width: 512px, below). */
    /* The source pill's track can SHRINK: with four discs at the row's end
       on a phone (planche 23) the name inside the pill gives way, never the
       discs — the pill's own ellipsis does the cutting. Three tracks and no
       more: an empty fourth still costs its gap, and the discs stood 10 px
       short of the content's right edge (owner's report). */
    grid-template-columns: minmax(0, auto) 1fr auto;
    align-items: center;
    gap: 10px;
}
/* THE HEADER'S DISCS, one group at the row's end (planches 22 · 23): the PDF
   on Report; on the Map its own four — Weather, Navigation, recentre, the
   ground — in an order that never changes. The reserved column always has
   exactly one occupant: this group. Under 600px the last two are moved into
   the report chrome's own group at the end of ITS line (placeMapTools), so a
   phone folds the four in two and the source pill keeps its width; the group
   left empty is not drawn. */
.header-discs {
    grid-column: 3;
    justify-self: end;
    display: inline-flex;
    align-items: center;
    /* L'écart de la colonne de gauche (--map-stack-gap), qui est celui des
       zones de contact de 48 px : elles s'y touchent sans se chevaucher. */
    gap: 8px;
}
.header-discs:empty { display: none; }
/* ONE COLUMN ON THE RIGHT OF THE MAP (owner's decision, planche VOL A.1,
   validated preview version 5): the header's own group, turned upright and
   held to one disc's height so the row does not grow — Fly, Find, Navigation,
   the ground, recentre, the zoom, top to bottom, on the right edge. In flight
   the discs part by the contact zone's growth (E.1): two 56 px zones meet
   without overlapping, and the drawing of each disc does not change. */
body.map-view-open .header-discs {
    flex-direction: column;
    align-items: flex-end;
    align-self: start;
    height: var(--round-btn-size);
    gap: var(--map-stack-gap);
}
body.map-view-open .header-discs > * { flex: none; }
/* THE COLUMN'S GROUPS (owner's decision, planche VOLERGO B.1, « A corrigé »):
   under Fly, each group ONE glass capsule — Find, or DCT in flight;
   Navigation and the ground; recentre, the zoom and, in flight, Track-up at
   its foot, appearing without pushing anything. The capsule wears the glass,
   the discs inside it none. Off the map a group is no box at all: the
   header's row is unchanged. */
.map-disc-group { display: contents; }
body.map-view-open .map-disc-group {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--map-group-gap);
    padding: var(--map-group-pad);
    border-radius: 999px;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
}
/* A lit disc keeps its state's own ground (the hollow, or Weather's ring). */
body.map-view-open .map-disc-group :is(.wx-header-btn:not(.active, [aria-expanded="true"], [aria-pressed="true"]), #mapZoom) {
    background: transparent;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
/* A capsule with nothing shown in it (Find on the Profile) is not drawn. */
body.map-view-open .map-disc-group:not(:has(> :not([hidden]))) { display: none; }
body.map-view-open .header-discs > .map-disc-group { margin-top: calc(var(--map-group-space) - 2 * var(--map-group-pad) - var(--map-stack-gap)); }
/* Find's capsule, DCT's in flight, glued under Fly (owner's decision, planche
   COLONNE D.2): it goes down with Fly when the band comes. */
body.map-view-open .header-discs > #mapFlyBtn + .map-disc-group {
    margin-top: calc(var(--map-group-space) - var(--map-group-pad) - var(--map-stack-gap));
}
/* THE FOOT'S STACK (owner's decision, planche COLONNE D.1): recentre and the
   zoom stand on Terrain over Weather (planche VOLERGO B.2), a capsule's air
   above them, at the right of the foot's line and rising with it on the band.
   Terrain shows in flight only: on the ground its place is kept under the
   zoom, so no group moves at Fly. Off the map the stack is no box at all. */
.map-foot-discs { display: contents; }
body.map-view-open .map-foot-discs {
    position: fixed;
    right: var(--map-chrome-x);
    bottom: calc(var(--map-foot-bottom) - var(--map-group-pad));
    z-index: var(--z-chrome);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: calc(var(--map-group-space) - 2 * var(--map-group-pad));
}
body.map-view-open .map-foot-discs:has(#mapTerrainBtn[hidden]) > .map-disc-group:has(#mapRecenterBtn) {
    margin-bottom: calc(var(--map-disc) + var(--map-group-gap));
}
/* A column that does not hold above the foot's stack in flight (planche
   VOLERGO B.3, « A2S »; fitColumn, a measure): Find's capsule, Settings in it,
   crosses to the left edge, on the ground as in flight, a disc lower than Fly. */
body.map-view-open.map-column-short .header-discs > .map-disc-group:has(#mapWxBtn) {
    position: fixed;
    left: calc(var(--free-left, 0px) + var(--map-chrome-x));
    top: calc(var(--flight-bar-room, 0px) + 8px + var(--map-disc) + var(--map-group-space) - var(--map-group-pad) + 56px);
    margin: 0;
}
/* FLY (planche VOL B.1): a pill of the discs' height carrying a word;
   lit, it says Flying in the reading ink, ringed as the lit Weather disc is
   (one grammar for a switch that changes the whole picture). Arming the exit
   is said by the word itself. */
.map-fly-btn {
    display: none;
    align-items: center;
    justify-content: center;
    height: var(--round-btn-size);
    padding: 0 14px;
    border-radius: 999px;
    color: var(--muted);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    line-height: 1;
    cursor: pointer;
}
body.map-view-open .map-fly-btn { display: inline-flex; }
/* A pill, not a disc: its width is its word's (the disc rules fix 40 px). */
.header-actions .map-fly-btn { width: auto; padding: 0 14px; }
.header-actions .map-fly-btn[aria-pressed="true"] {
    color: var(--text);
    box-shadow: var(--glass-drop), inset 0 0 0 2.5px var(--text), var(--glass-rim);
}
/* THE NIGHT ON THE PILL (planche ENCART C.1): filled in the reading ink with
   the moon during the night; the hour of a change, half an hour before it. */
.map-fly-btn { gap: 6px; }
/* The recentre disc lights while it follows the position (D.6): its ground is
   every lit map disc's (.header-actions .wx-header-btn), its cross the text's ink. */
#mapRecenterBtn[aria-pressed="true"] svg { stroke: var(--text); fill: var(--text); }
/* THE ZOOM AT THE FOOT OF THE COLUMN (planche VOL A.4), at every width:
   a rail of the discs' width, two faces flat on the thin glass, its hairline
   its edge. Each face carries its own finger pad, growing AWAY from the other
   so the two meet at the seam and never overlap (a tap in an overlap belongs
   to whichever is later in the DOM: zoom in when you meant zoom out). */
#mapZoom {
    display: none;
    flex-direction: column;
    width: var(--round-btn-size);
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
}
body.map-view-open #mapZoom { display: inline-flex; }
.map-zoom-btn {
    position: relative;
    display: grid;
    place-items: center;
    width: 100%;
    height: calc(var(--round-btn-size) - 2px);
    border: 0;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
}
/* The zoom's two glyphs are drawn like every disc's icon (owner's decision). */
.map-zoom-btn svg { width: var(--icon-box); height: var(--icon-box); fill: none; stroke: currentColor; stroke-width: var(--icon-stroke); stroke-linecap: round; }
.map-zoom-btn:first-child { border-radius: 999px 999px 0 0; }
.map-zoom-btn:last-child { border-radius: 0 0 999px 999px; }
.map-zoom-btn::after {
    content: "";
    position: absolute;
    inset: min(0px, var(--map-pad-x));
}
.map-zoom-btn:first-child::after { inset: min(0px, var(--map-pad-y)) min(0px, var(--map-pad-x)) 0; }
.map-zoom-btn:last-child::after { inset: 0 min(0px, var(--map-pad-x)) min(0px, var(--map-pad-y)); }
@media (hover: hover) {
    .map-zoom-btn:hover { background: var(--hover-bg); }
}

/* LE CHOIX DE LA SOURCE EST UNE LIGNE (décision du propriétaire, planche AREA
   ROUTE, V5) : on lit le côté choisi et ce qui y est chargé — son icône, le nom,
   le chevron — et on passe à l'autre côté dans la feuille Source qu'elle ouvre.
   Les deux cas restent écrits, et leurs deux lecteurs (renderSourcePill, le
   formulaire) n'ont pas changé : seul le cas choisi se montre. Le nom est un
   objet de la bibliothèque, donc il est en gras (décision du propriétaire). */
.source-pill {
    display: inline-flex;
    align-items: center;
    /* SA PROPRE HAUTEUR, celle du disque de la tête : la ligne du chrome mesure
       la même chose qu'un disque s'y tienne ou non. */
    --round-btn-size: 40px;
    min-height: var(--round-btn-size);
    max-width: 100%;
    min-width: 0;
    border-radius: 999px;
    color: var(--text);
}
.source-seg:not(.active) { display: none; }
.source-seg {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: 0 1 auto;
    min-width: 0;
    min-height: var(--round-btn-size);
    padding: 0 14px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    cursor: pointer;
}
/* 44 px de contact, quelle que soit la hauteur dessinée. */
.source-seg::after {
    content: "";
    position: absolute;
    inset: min(0px, (var(--round-btn-size) - 44px) / 2) 0;
}
.source-mode { flex: none; display: inline-flex; font-style: normal; color: var(--muted); }
/* The side is an icon (owner's decision): the chrome's one stroke, in its box. */
.source-mode svg { width: var(--icon-box); height: var(--icon-box); fill: none; stroke: currentColor; stroke-width: var(--icon-stroke); stroke-linecap: round; stroke-linejoin: round; }
.source-name { flex: 0 1 auto; min-width: 0; font-weight: var(--fw-semibold); color: var(--text); }
/* THE SAME LINE, READ RATHER THAN SET (owner's ask): the library's card shows
   which side a briefing flies on, in the dress the Briefing page sets it in —
   nothing pressable of its own: the card around it is the door. */
/* LA TÊTE DE LA CARTE SOURCE CHOISIT (planche SOURCES SO.4) : le côté en titre
   de carte, le nom à droite, ⌄ au bout — toute la largeur de la tête est le
   bouton, et il ouvre la liste des zones et des routes. */
.source-choose { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; min-height: 44px; padding: 0; border: 0; background: none; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
.source-choose > .settings-card-title { margin: 0; }
.source-choose > .source-name { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.source-choose > .source-choose-chev { flex: none; }
/* Une page qui porte un objet dit ce qu'il est sous son nom (Area, Route). */
.page-title > small { display: block; color: var(--muted); font-size: var(--fs-xs); font-weight: var(--fw-regular); }
/* Dans une ligne (la bibliothèque), c'est sa valeur : à droite, et elle cède
   avant la ligne — le nom raccourcit, jamais la ligne. */
.sheet-row > .source-pill {
    flex: 0 1 auto;
    min-width: 0;
    margin-left: auto;
    --round-btn-size: 28px;
}
.sheet-row > .source-pill .source-seg { padding: 0; }
.sheet-row:has(> .source-pill) > .sheet-k { flex: none; }
/* THE MENU: the app's pages on one bar floating at the foot of the screen,
   inset from the edges, on the ground with its hairline over whatever scrolls
   under it — the page's own --bg on the Report and the Briefing, --surface over
   the Map and the Profile (§ 7.6) — the one place a
   thumb reaches with the tablet on a knee (owner's call). Four items on a
   phone and a tablet; a desktop keeps its standing settings column and draws
   three. */
/* LE MENU EN LIGNE, ET PLUS BAS (décision du propriétaire, planche MENU, M2) :
   l'icône et le mot côte à côte, posés juste au-dessus de la barre du geste —
   Briefing en tête. 46 px peints (décision du propriétaire) : la surface de vol
   garde ses 44 px au doigt par la zone invisible de chaque bouton, plus bas. */
/* THE BAR AT THE FOOT (planche RAIL R.1, owner's decision): the phone's bar
   from the phone to the laptop — one bar, full width, over the panels and the
   pages (PANNEAU B.4), in three thirds: the Briefing at the left edge, the
   three views centred, the account at the right edge. Its height is the foot
   band (--app-menu-clear), content-box so the home indicator's inset adds
   to it rather than eating it; its contour is a 1 px ring inside it (a bar
   has a contour, not a separating hairline: --contour, § 8.12). Its words
   are at --fs-xs: the 11 px floor is for units and metadata (§ 4.2). */
.app-menu {
    position: fixed;
    z-index: calc(var(--z-panel) + 1);
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-sizing: content-box;
    height: 52px;
    padding: 0 var(--main-gutter) var(--menu-safe);
    /* En verre de barre (planche HABILLAGE B.3) : le liseré sur son bord, l'ombre
       vers le contenu, plus de filet gris. */
    background: var(--glass-bar);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop-up), inset 0 1px 0 var(--glass-line), inset 0 6px 10px -6px var(--glass-glow);
}
/* The three views, one centred group between the two edge cases. */
.app-views {
    display: flex;
    flex: 1 1 auto;
    justify-content: center;
    align-items: center;
    gap: 2px;
    min-width: 0;
}
/* ONE CASE FOR EVERY ITEM OF THE BAR: the icon over its word, 48 px tall (44
   at the finger and more), lit with the menu's segment when it is the view
   or the open panel. The views give way first on a narrow screen (flex 0 1);
   the two edge cases keep their width. */
.app-menu .view-switcher-btn,
.briefing-toggle-btn,
.account-btn {
    position: relative;   /* anchors the Profile segment's plan-lock badge */
    display: inline-flex;
    flex: 0 1 96px;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-width: 0;
    height: 48px;
    padding: 0 6px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    cursor: pointer;
}
.briefing-toggle-btn,
.account-btn { flex: 0 0 auto; min-width: 64px; }
.app-menu .view-switcher-btn.active,
.briefing-toggle-btn[aria-pressed="true"] { color: var(--text); background: var(--hover-bg); }
/* THE ALERTS' BADGE (planche ALERTES P.1): the fields in alert now, written —
   the app's knock-out disc, the account initial's — on the Briefing case's
   shoulder, and after the word on the panel's Alerts tab. */
.alerts-badge {
    position: absolute;
    top: 2px;
    left: calc(50% + 4px);
    min-width: 16px;
    height: 16px;
    box-sizing: border-box;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--solid);
    color: var(--on-solid);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    line-height: 16px;
    text-align: center;
}
@media (hover: hover) {
    .app-menu .view-switcher-btn:not(.active):hover,
    .briefing-toggle-btn:not([aria-pressed="true"]):hover,
    .account-btn:hover { background: var(--hover-bg); }
}
/* THE BRIEFING IS A PANEL, NOT A PAGE (RAIL R.2): the sidebar glyph, its
   pane filled while the panel is open. */
.briefing-glyph-fill { display: none; fill: var(--muted); stroke: none; }
.briefing-toggle-btn[aria-pressed="true"] .briefing-glyph-fill { display: inline; }
/* THE ACCOUNT CASE (RAIL R.3): signed out the silhouette, signed in the
   initial knocked out of a disc the icon's size. A server without auth has no
   account (auth.js, is-absent): the case keeps its place, unseen and
   untouchable, so the views stay centred. */
.account-btn.is-absent { visibility: hidden; pointer-events: none; }
.account-glyph { display: inline-flex; align-items: center; justify-content: center; width: var(--icon-box); height: var(--icon-box); }
.account-initial {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--icon-box);
    height: var(--icon-box);
    border-radius: 999px;
    /* L'encre au clair, un gris anthracite au sombre (planche HABILLAGE B.11). */
    background: var(--solid);
    color: var(--on-solid);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    line-height: 1;
}
/* THE RAIL (planche RAIL A1, owner's decision): wider than tall — the laptop,
   the iPad and the phone on their side — the bar stands at the left edge, full
   height, the Briefing on top, the views under a hairline, the account at its
   foot. It is part of the left zone (--rail-w): the panel, its pages and the
   chrome start after it. No foot band then: what sat above the bar sits at
   the screen's foot. The notch, when the phone lies on its side, widens it. */
@media (orientation: landscape) {
    :root {
        --rail-w: calc(5rem + env(safe-area-inset-left, 0px));
        --app-menu-clear: 0px;
    }
    .app-menu {
        top: 0;
        right: auto;
        width: var(--rail-w);
        height: auto;
        box-sizing: border-box;
        flex-direction: column;
        justify-content: flex-start;
        gap: 6px;
        padding: calc(10px + env(safe-area-inset-top, 0px)) 8px calc(10px + env(safe-area-inset-bottom, 0px)) calc(8px + env(safe-area-inset-left, 0px));
        box-shadow: var(--glass-drop-side), inset -1px 0 0 var(--glass-line), inset -6px 0 10px -6px var(--glass-glow);
    }
    .app-views {
        flex: 0 0 auto;
        flex-direction: column;
        gap: 6px;
        padding-top: 10px;
        border-top: 1px solid var(--row-line);
    }
    .app-menu :is(.view-switcher-btn, .briefing-toggle-btn, .account-btn) {
        flex: none;
        width: 64px;
        min-width: 0;
        height: 56px;
        padding: 0;
        border-radius: 12px;
    }
    .account-btn { margin-top: auto; }
}
/* Taller than wide, from 600 px (the iPad): the five cases fit with their
   word beside the icon. Under it (the phone) they do not — 469 px for 366
   at 390 — and the word stays under the icon. */
@media (orientation: portrait) and (min-width: 37.5rem) {
    .app-menu :is(.view-switcher-btn, .briefing-toggle-btn, .account-btn) {
        flex-direction: row;
        gap: 6px;
        padding: 0 14px;
        font-size: var(--fs-sm);
    }
}
/* UNE ICÔNE DE BOUTON A UNE SEULE ENCRE, celle du disque PDF : --muted
   (décision du propriétaire), menu compris, allumé ou non. */
.app-menu .view-switcher-icon,
.briefing-toggle-btn .view-switcher-icon {
    width: var(--icon-box);
    height: var(--icon-box);
    flex: none;
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.view-switcher-label { line-height: 1; }
/* PANNEAU A.3, the bar never moves with the panel: the Briefing case is as wide
   as its word, and the word goes semibold when the panel opens. Its width is
   the semibold word's from the start, held by an unseen copy under it. */
.briefing-toggle-btn .view-switcher-label { display: inline-grid; }
.briefing-toggle-btn .view-switcher-label::after {
    content: attr(data-text);
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: var(--fw-semibold);
}

/* THE PASTILLES OF A FIELD OUT OF LIMITS (owner's decision, planche APT): a disc
   for the METAR, a ring for the TAF, drawn by ringGlyph (airport-core.js) in
   that configuration's ink. THEIR SIZE IS THE SIZE OF THE CODE beside them (owner's
   call after the first flight on the phone: the cap height read too small) —
   1em, so in a legend it follows the legend's own text. On the map they are
   sized by their pill: 11px in a briefed one, 9px in an unbriefed one. */
.ap-rings { display: inline-flex; align-items: center; gap: 3px; }
.ap-ring {
    display: block;
    flex: none;
    width: 1em;
    height: 1em;
    overflow: visible;
}

/* No hand-made reservation on the phone any more. The rail once ran to the row's
   right edge (the row was two columns: handle, rail) and a right margin of one
   slot pulled it back onto the screen's axis. The row now HAS a third column for
   the right-hand disc — PDF on the report, Windy on the map and the chart — so
   the grid reserves that slot itself, and the margin was reserving it a second
   time: the rail came out one slot too narrow and sat half a slot left of centre.
   Two equal side columns are all the centring the rail needs. */
/* A legend entry under a chart or beside the map: a mark, not a control. */
.minima-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: none;
    border: 0;
    padding: 2px 4px;
    background: none;
    color: var(--muted);
    font-size: var(--fs-2xs);
    letter-spacing: 0.06em;
}
/* The header's left slot: the family's drawer handle (the report button now
   lives at the foot of the sidebar's Briefing card). */
.header-tools { grid-column: 1; justify-self: start; min-width: 0; max-width: 100%; display: inline-flex; flex-direction: column; align-items: center; gap: 6px; }
.header-tools { position: relative; }
.header-tools .source-pill { max-width: 100%; }
/* The pill and, on the map, the magnifier beside it (owner's ask). */
.source-row { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
body:not(.map-view-open) #mapSearchBtn { display: none; }
/* « Times in LT » under the pill (planche VOL A.2): the account's clock,
   in the quiet ink, on the map alone. */
.map-times-line {
    display: none;
    align-self: flex-start;
    padding-left: 4px;
    font-size: var(--fs-xs);
    color: var(--muted);
    white-space: nowrap;
    pointer-events: none;
}
body.map-view-open .map-times-line { display: block; }
/* THE READING UNDER THE CROSSHAIR (owner's decisions, planche AGE B.3–B.6
   and planche VOL A.3): on the foot's line, between the scale and the
   Weather disc; 12 px in --text, the value semi-bold, the source regular; on
   a frosted glass of a 25 % veil and a 5 px blur, 4 px of air on every side
   — an exception the owner decided to the opaque chrome (docs/ui/grammaire.md,
   « Le chrome global »), for this line and the scale alone. */
.map-reading {
    display: inline-flex;
    pointer-events: auto;
    min-width: 0;
    align-items: center;
    gap: 4px;
    padding: 4px;
    border-radius: 999px;
    font-size: var(--fs-xs);
    line-height: 1.2;
    color: var(--text);
    white-space: nowrap;
    background: color-mix(in srgb, var(--card) 25%, transparent);
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
}
.map-reading[hidden] { display: none; }
.map-reading-value { font-weight: var(--fw-semibold); }
.map-reading-source { font-weight: var(--fw-regular); }
.map-reading-mark svg { display: block; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.map-reading-info { display: inline-flex; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; }
.map-reading-info svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
/* Icon phase: the rail takes the whole span between the two handles, its four
   segments sharing it equally — a phone's primary tap targets, not a 159px pill
   adrift in an empty row. Vertically it stays on the discs' middle line (the
   row's own align-items). */
/* The settings-drawer toggle (mobile only) is the sole .round-icon-btn left in
   the header top row — sized by the .header-top --round-btn-size override
   (40px), a bigger tap target than its 24px default. */
/* The two WX filter dropdown pills in the header. Ordered after every other
   header-top child and forced to want the full width by default, so with
   flex-wrap they always land on their own new line below the other, small items.
   This is the default (mobile, and any desktop header too narrow to fit them
   inline); the @container rule below promotes them inline only when the header
   itself — not the viewport — is actually wide enough. Right-aligned on that
   own line, same side as the actions cluster above it. */
#briefingWxFilters { order: 9; flex: 1 1 100%; min-width: 0; justify-content: flex-end; }
.wx-filter-dropdowns { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* The container is .header-actions (see the desktop container-type rule). When
   the header has room, the pills sit inline just right of the actions cluster
   (shrinking to their own content); below that width they fall back to the
   own-line default above. Keyed off the header's real width, so it never goes
   inline when there isn't the space (which a viewport media query can't tell,
   since the header is narrower than the viewport by the sidebar). */
@container (min-width: 640px) {
    #briefingWxFilters { order: 3; flex: 0 1 auto; }
}
/* Real on/off switch controlling the TAF section. */
/* L'interrupteur d'une section — Schedule, Report, Alerts — reste dessiné à
   40 × 20 ; sa zone de contact fait 56 × 44. Sa forme de RANGÉE (.sheet-row) est
   déjà large et n'en prend pas : un débordement sur une rangée de 345 px de
   large irait mordre sur ce qui la suit sans rien gagner. */
.taf-switch {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 24px;
    height: 24px;
    padding: 3px 4px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    box-shadow: none;
    color: var(--heading);
    font-size: var(--fs-md);
    line-height: 1;
}
.taf-switch:not(.sheet-row) { position: relative; }
.taf-switch:not(.sheet-row)::after {
    content: "";
    position: absolute;
    inset: -12px -8px;
}
/* ONE SWITCH ACROSS THE WHOLE APP, the thin track (owner's decision, planche
   INTER I.1): a 6 px line and a 14 px thumb riding over it, lighter than the
   text beside it. Off, the thumb is the surface ringed in the track's own grey;
   on, it takes the text ink and the track darkens. The row stays the target. */
.taf-switch-track {
    position: relative;
    flex: 0 0 auto;
    width: 28px;
    height: 6px;
    border-radius: 999px;
    background: rgba(var(--text-rgb), 0.18);
    transition: background 0.15s ease;
}
.taf-switch-thumb {
    position: absolute;
    top: -4px;
    left: 0;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 0 0 1.5px rgba(var(--text-rgb), 0.22);
    transition: transform 0.15s ease, background 0.15s ease;
}
.taf-switch.active .taf-switch-track,
.wx-filter-toggle[aria-checked="true"] > .taf-switch-track { background: rgba(var(--text-rgb), 0.45); }
.taf-switch.active .taf-switch-thumb,
.wx-filter-toggle[aria-checked="true"] > .taf-switch-track > .taf-switch-thumb {
    transform: translateX(14px);
    background: var(--text);
    box-shadow: none;
}
/* The label stays muted and regular-weight in both states — the switch
   track/thumb alone carry the on/off signal. */
.taf-switch .taf-switch-label { color: var(--heading); font-weight: var(--fw-medium); }

/* ---------------------------------------------------------------------------
   WX filters — checkbox/label toggles (.wx-filter-toggle) and switches
   (.taf-switch), on/off driven by the .active class. Used by the briefing
   header's two dropdown pills and by the schedule form's Briefing /
   Notifications copies — both are .wx-filter-dropdowns (see below).
   --------------------------------------------------------------------------- */

/* --- WX filter dropdowns (briefing header + schedule form) ---------------
   Two compact pills (TAF, Parameters), each a bordered button showing its name
   + the current selection, opening a floating panel below. Reuse the schedule
   .schedule-select shells for the inner selects and .taf-switch / .wx-filter-
   toggle for the switches and checkboxes. */
.wx-dropdown { position: relative; display: inline-flex; }
.wx-dropdown-btn {
    display: inline-flex;
    align-items: center;
    /* 2px tighter on both sides of the caret than the shared shell's default:
       the gap before it and the padding after it (its right edge). */
    gap: 3px;
    min-height: var(--control-h);
    padding: 4px 8px 4px 10px;
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-md);
    cursor: pointer;
}
/* The schedule form's View rail carries no rules of its own: height, contour,
   ground and selected segment all come from "One rail, everywhere" at the end
   of this sheet, the same as every other rail in the app. */
/* Both pills (TAF, Parameters) show their full selection — never ellipsized. The
   button grows to fit; when the row is tight the whole pill wraps to its own line
   via the container query below (.wx-filter-dropdowns is flex-wrap), where there
   is always room. */
.wx-dropdown-value { flex: none; overflow: visible; text-overflow: clip; white-space: nowrap; }
/* Solid muted caret (the "petite flèche pleine"), rotating up while its button
   is open — shared by the pills and the inner day/time/mode selects. */
.wx-drop-caret {
    width: 14px;
    height: 14px;
    flex: none;
    fill: var(--muted);
    transition: transform 0.15s ease;
}
button[aria-expanded="true"] > .wx-drop-caret { transform: rotate(180deg); }
.wx-dropdown-panel {
    position: absolute;
    z-index: var(--z-sheet);
    top: calc(100% + 4px);
    min-width: 15rem;
    padding: 10px;
    background: var(--surface);
    border: 1px solid var(--soft-line);
    border-radius: var(--radius-md);
    box-shadow: 0 0 0 1px var(--contour);
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* A panel that stacks several switch groups: cap the height to the
       viewport and scroll inside rather than overflowing the screen. */
    max-height: min(70vh, 32rem);
    overflow-y: auto;
}
.wx-dropdown-panel[hidden] { display: none; }
/* Never let a panel run off-screen: cap to the viewport width. */
.wx-dropdown-panel { max-width: calc(100vw - 16px); }
/* Section titles inside filter panels: flush left, sticky while scrolling
   (the base .config-section-title is already sticky). */
.wx-dropdown-panel .config-section-title {
    padding-left: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
}
/* Subcategory (Q-subject) switches sit inset under their category row. */
.wx-notam-subjects { padding-left: 18px; }
/* The Parameters panel anchors to its pill's right edge, so it never overflows
   the viewport (it is the right-hand pill). */
#wxParamsDropPanel { right: 0; }
/* The TAF/NOTAM panel lays its two controls side by side: the mode select
   takes the free width, the "to" time field sits right beside it (shown in
   the "time" mode only). Both keep their base --bg fill — they sit on the
   white panel, not on the --bg card ground, so the accordion's white-field
   flip doesn't apply. */
.wx-filter-overlay-body { display: flex; align-items: center; gap: 6px; }
/* A SHEET WEARS THE GROUND OF THE PAGE IT OPENS FROM (owner's decision, planche
   RÉGLAGES-REPORT F.1): the NOTAM filters' sheets open from the Report page,
   whose rows stand in cards — so do theirs. */
.wx-filter-overlay :is(.wx-filter-overlay-body, .wx-notam-list) {
    background: var(--glass-group);
    border-radius: var(--radius-lg);
    padding: 0 12px;
}
/* WX groups stack: "All parameters" over its six. */
.wx-filter-overlay-body.wx-params-body,
/* L'échelle de tri s'empile elle aussi : les rangées qu'on range, puis, sous un
   filet, les clés qui attendent d'y entrer. */
.wx-filter-overlay-body.wx-sort-body,
/* THE TIME WINDOW'S CHOICES (planche RÉGLAGES-REPORT W.2): a column of system
   rows on its page, the chosen one in its capsule. */
.wx-taf-modes {
    display: flex;
    flex-direction: column;
    /* The body is a ROW elsewhere, and centres what it holds. Turned on its
       side without saying so, that centring became a horizontal one: the
       choices stood in a narrow column in the middle of the sheet, off the
       left edge every other row of the app starts on. */
    align-items: stretch;
}
/* THE SCHEDULE'S OWN END WITHOUT A SCHEDULE (owner's decision, planche RÉGLAGES
   B.1, B.2): the choice stays in the list, greyed and unpressable, and the line
   under it says why and leads to the schedule. */
.wx-taf-modes [data-window-mode="endofschedule"]:disabled { background: none; color: var(--quiet); cursor: default; }
.wx-taf-modes[data-scheduled="true"] .wx-taf-unscheduled { display: none; }
.wx-taf-unscheduled { margin: -4px 0 6px; padding: 0 14px; }
.wx-taf-schedule-link {
    position: relative;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}
.wx-taf-schedule-link::after { content: ""; position: absolute; inset: -12px -8px; }
.wx-params-body .wx-param-row { min-height: var(--control-h); }
/* Per-parameter rows: one switch each. */
.wx-param-rows { display: flex; flex-direction: column; gap: 2px; }
.wx-param-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    min-height: 26px;
    padding: 2px 6px;
    border-radius: var(--radius-sm);
}
@media (hover: hover) {
    .wx-param-row:hover { background: var(--hover-bg); }
}
/* UN GROUPE QUI SE RANGE (décision du propriétaire) : une catégorie, une marque
   d'état. C'est la rangée maître PLUS ses lignes de détail qui se déplacent
   ensemble — d'où une boîte autour des deux, sans quoi le glissement emporterait
   un titre en laissant ses soixante lignes derrière lui. */
.wx-notam-group { display: block; }
/* La poignée est celle des cartes de minima, à la taille de ces rangées-ci : une
   rangée de filtre fait 26 px là où un bloc de minima en fait soixante, donc
   elle prend la hauteur de SA rangée et non celle d'un bloc. Le glyphe, son
   encre et son geste ne changent pas. */
.wx-notam-group .notam-option-lead > .config-grip {
    align-self: center;
    min-width: 18px;
    height: 26px;
    padding: 0 4px 0 0;
    font-size: var(--fs-md);
}
/* LE FILET DE DÉPOSE EST CELUI DES MINIMA : la même marque, au même endroit du
   geste, parce que c'est le même geste. */
.wx-notam-group.is-dragging { position: relative; z-index: var(--z-sticky); opacity: 0.85; }
.wx-notam-group.drop-target { position: relative; }
.wx-notam-group.drop-target::before,
.wx-param-rows.drop-end::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--text);
}
.wx-notam-group.drop-target::before { top: -2px; }
.wx-param-rows.drop-end { position: relative; }
.wx-param-rows.drop-end::after { bottom: -2px; }
/* CE QU'UNE RANGÉE MAÎTRE PORTE DEVANT SON MOT : la marque de l'état, à la
   taille de CETTE rangée. Celle de la liste fait 22 px sur une ligne de 18 ;
   ici la rangée en fait 26, donc la même pastille l'étirerait. Et elle se centre
   sur la ligne au lieu de s'aligner en haut : dans la liste elle vise la
   première ligne d'un titre qui peut passer à la ligne, ici il n'y a qu'une
   ligne et le haut la faisait flotter. */
.notam-option-lead > .notam-icon {
    align-self: center;
    width: 18px;
    height: 18px;
    margin-top: 0;
    margin-right: 0;
}
.notam-option-lead > .notam-icon > .notam-mark { width: 11px; height: 11px; }
/* LE RETRAIT SE MESURE SUR LE MOT DU GROUPE, pas sur le bord de la liste : la
   poignée et la marque poussent le titre d'un groupe de leur largeur, donc des
   sous-lignes rentrées de moins partaient plus à GAUCHE que leur propre titre —
   les enfants avaient l'air plus importants que le parent. C'est donc la cellule
   de tête du panneau plus les 8 px de la grille, et la COLONNE DE CODE des
   lignes de détail tombe exactement sous le mot de leur groupe. */
.wx-notam-group .wx-notam-subjects,
#wxNotamSpecialRows .wx-notam-subjects { padding-left: 30px; }
.wx-notam-group:has(.notam-option-lead > .notam-icon) .wx-notam-subjects { padding-left: 54px; }
/* The "to" time dropdown lists the day's selectable 30-minute slots. Anchored to
   the right of its (right-hand) half so it doesn't overflow, and height-capped so
   a full day's worth scrolls rather than running off the panel. */
/* Label-only filter toggles, shared by the parameter/scope/traffic/category/
   special/subject rows (multi-select, role=switch — the semantics are
   unchanged): no checkbox glyph — the SELECTED state is the row itself,
   a neutral --bg fill with a heading-ink medium label. Off rows read muted
   on a transparent ground. */
.wx-filter-toggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    padding: 0;
    font: inherit;
    /* A switch row speaks like the settings (planche INTER I.3). */
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
}
.wx-filter-label { color: var(--muted); }
.wx-param-row:has(.wx-filter-toggle.active) { background: var(--bg); }
.wx-filter-toggle.active .wx-filter-label {
    color: var(--heading);
    font-weight: var(--fw-medium);
}
/* "All parameters" reads stronger than the individual toggles below it, since
   it governs them all — in the sidebar and in the schedule form's own Briefing
   card (data-wx-all). */
[data-param-filter="all"] .wx-filter-label,
[data-wx-all] .wx-filter-label { color: var(--muted); font-weight: var(--fw-medium); }
/* THE WEATHER'S BAND (owner's decision, planche METEO-RAIL R.1): docked to
   the navigation — on the bar in portrait, from the rail's edge in landscape,
   from the open Briefing's edge when it is fixed (--free-left) — and running
   to the screen's right edge. It stands over the layers' sheet, which comes
   out from behind it (R.2), and under the settings drawers and the panel. */
.route-profile-timebar {
    position: fixed;
    left: var(--free-left);
    right: 0;
    bottom: var(--menu-band);
    width: auto;
    margin: 0;
    z-index: calc(var(--z-chrome) + 1);
    display: flex;
}
/* ONE CAPSULE AT THE FOOT OF THE MAP (owner's choice), and it is STOREYS ONLY
   (planche 23): the altitudes, the legend, the clock — each standing only while
   it has something to say, and the capsule itself not drawn when none does.
   The map's own buttons live in the header with the reports' pills, which stay
   in the report chrome on the Map (planche 22): the same line, the same place,
   whichever page is up. Raw / Decoded is set on the Display page alone. */
#mapTimebar:not(:has(> :not([hidden]))) { display: none; }
/* The map's capsule lives OUTSIDE #mapView (see index.html for why: a fixed
   descendant of that section landed below the screen on iOS). Standing outside,
   it no longer inherits `#mapView[hidden]`, so the guarantee that used to be
   structural is written here instead: off the map, it is gone. */
/* The map's own capsule is the one the map view owns: off that view it goes.
   The cross-section wears the same class to get the same dressing (P4), and
   its capsule lives inside the chart's body, which only exists on Profile —
   hiding it by the shared class hid the legend and the hour stepper on the
   cross-section, where nothing else brings them back. */
body:not(.map-view-open) :is(#mapTimebar, #mapWxPop) { display: none; }
/* THE FOUND POINT (owner's decision, planche TROUVE): a solid dot in the text's
   ink, ringed by the ground so it holds on any map, its name beside it written
   as the map writes its names — haloed, no box. The dot is the button; the
   name is read, not pressed. */
.found-pin {
    position: relative;
    display: block;
    width: 12px;
    height: 12px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--text);
    box-shadow: 0 0 0 2.5px var(--surface);
    cursor: pointer;
}
.found-pin::after { content: ""; position: absolute; inset: -16px; }
.found-pin-name {
    position: absolute;
    left: 18px;
    top: 50%;
    transform: translateY(-50%);
    white-space: nowrap;
    text-align: left;
    pointer-events: none;
    font-size: var(--fs-xs);
    line-height: 1.15;
    font-weight: var(--fw-semibold);
    color: var(--text);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo), 0 0 2px var(--map-label-halo);
}
.found-pin-name small { display: block; font-size: var(--fs-2xs); font-weight: var(--fw-regular); }
/* THE LINE OVER THE LEGEND (owner's decision, planche VOL A.3): the
   scale on the left, the reading pill, and at the right the Weather disc —
   the header's node, set here by the rule below. It stands on the weather
   band (--band-h, measured) or on the navigation when there is none; under
   the layers' sheet, which comes out over it. Its left is the chrome's, its
   right stops before the disc. */
#mapFootLine {
    position: fixed;
    left: calc(var(--free-left) + var(--map-chrome-x));
    right: calc(var(--map-chrome-x) + var(--map-disc) + var(--map-stack-gap));
    bottom: var(--map-foot-bottom);
    z-index: calc(var(--z-chrome) - 1);
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 8px;
    pointer-events: none;
}
body:not(.map-view-open) #mapFootLine { display: none; }

/* ===== THE FLIGHT MODE (planche VOL; cahier des charges mode-vol) ===== */
/* THE OWN-SHIP SYMBOL (C.2; V-11, D1; planche VOL-RETOUR): the pilot's icon
   alone, 32 px, in the symbol's ink inside a halo of its paper — no disc —,
   the same at rest, along the last valid track. A fixed size whatever the zoom — it is a marker — turned with the map by
   the engine (rotateWithMap), so it points along the track under either
   orientation. The halo is the icon drawn again under it, so a rotor crossing
   the body draws no seam. */
.map-ownship-marker { width: 37px; height: 37px; pointer-events: none; }
.map-ownship { display: block; width: 37px; height: 37px; overflow: visible; }
.map-ownship-icon { display: none; }
.map-ownship-halo { fill: var(--ownship-paper); stroke: var(--ownship-paper); stroke-width: 2.6; stroke-linejoin: round; }
.map-ownship-ink { fill: var(--ownship-ink); }
.map-ownship-marker[data-shape="helicopter"] [data-icon="helicopter"],
.map-ownship-marker[data-shape="aeroplane"] [data-icon="aeroplane"],
.map-ownship-marker[data-shape="triangle"] [data-icon="triangle"],
.map-ownship-marker[data-shape="arrow"] [data-icon="arrow"] { display: block; }
/* THE GROUND'S POSITION (owner's decision): out of Fly a still dot in the
   symbol's ink on a ring of its paper — no heading to show. */
.map-ownship-dot { display: none; fill: var(--ownship-ink); stroke: var(--ownship-paper); stroke-width: 2.5; }
.map-ownship-marker[data-shape="dot"] .map-ownship-dot { display: block; }
/* THE UNCERTAINTY RING (C.4; V-13), drawn in ground metres: read by the
   engine's probe, in the symbol's own ink, barely filled. */
#briefingMap .map-ownship-ring { stroke: var(--ownship-ink); stroke-width: 1.5; fill: var(--ownship-ink); fill-opacity: 0.08; }
/* THE ROUTE VECTOR (D.7; V-26, D3; planche VECTEUR): a 1 px line in the
   symbol's ink on a thin halo of its paper — discreet, and read on a dark
   relief by day as on a pale road by night; its marks as small pills, the
   unit once, at the last. */
#briefingMap .map-ownship-vector { stroke: var(--ownship-ink); stroke-width: 1; stroke-linecap: round; }
#briefingMap .map-ownship-vector-halo { stroke: var(--ownship-paper); stroke-width: 3; stroke-opacity: 0.7; stroke-linecap: round; }
/* THE TRAIL (planche TRACE A.1): a dot every 10 s in the symbol's ink, on a
   breath of its paper — the past dotted, the vector's line the future. */
#briefingMap .map-ownship-trail { fill: var(--ownship-ink); stroke: var(--ownship-paper); stroke-width: 0.5; stroke-opacity: 0.4; }
/* The unit: the same size (11 px is the floor), a lighter weight, a quieter ink. */
.map-ownship-tick-u { margin-left: 2px; font-weight: var(--fw-regular); opacity: 0.75; }
/* THE RANGE RING (planche VECTEUR): the vector's drawing, never filled — the
   uncertainty ring is the filled one; its radius and N E S W on the reading
   pill's glass (.map-reading, owner's decision). */
/* The rings (planche ANNEAUX R.1): 1 px of the own-ship's ink at 55 %, on no halo — a
   reference on the map, never an object of it. */
#briefingMap .map-ownship-range { stroke: var(--ownship-ink); stroke-width: 1; stroke-opacity: 0.55; fill: var(--ownship-ink); fill-opacity: 0; }
/* The final reserve's ring in dashes (planche ANNEAUX C.2): its shape says « limit », and two
   rings lit together never read alike. */
#briefingMap .map-ownship-fuel-ring { stroke-dasharray: 6 4; }
.map-ownship-range-marker { pointer-events: none; }
/* The final reserve's word (planche PERF E.1): the radius's pill, never bold. */
.map-ownship-tick.map-ownship-fuel-word { font-weight: var(--fw-regular); white-space: nowrap; }
.map-ownship-range-cardinal {
    display: block;
    padding: 0 3px;
    border-radius: 999px;
    color: var(--ownship-ink);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    line-height: 1.3;
}
.map-ownship-range-cardinal,
.map-ownship-tick.map-ownship-range-radius {
    background: color-mix(in srgb, var(--card) 25%, transparent);
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
}
.map-ownship-tick-marker { pointer-events: none; }
.map-ownship-tick {
    display: block;
    min-width: 18px;
    padding: 1px 5px;
    border-radius: 999px;
    background: var(--ownship-paper);
    color: var(--ownship-ink);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    line-height: 1.3;
    text-align: center;
}
/* THE BAND (planches ENCART, VOLERGO C): in flight, ten places across the whole
   top of the map, against its edge — the card's ground, ringed as what rises.
   Two lines of five; one line of ten as soon as the band itself is wide enough
   (its own width, never a kind of device); the point's boxes take two. Each
   box is the METAR's: the name above in the labels' grey, the value in the
   reading size, its unit smaller and lighter. */
.map-flight-bar {
    position: absolute;
    top: 0;
    left: var(--free-left, 0px);
    right: 0;
    z-index: var(--z-chrome);
    container-type: inline-size;
    display: block;
    padding: calc(env(safe-area-inset-top, 0px) + 10px) 6px 8px;
    border: 0;
    background: var(--card);
    box-shadow: 0 0 0 1px var(--contour);
    color: var(--text);
    font: inherit;
    text-align: left;
}
.map-flight-bar[hidden] { display: none; }
/* Two lines of five places on a phone, one line on a tablet; Next and Dest
   take two places and are drawn one and a half boxes wide, each line filling
   the band. A line starts at its break (insetLineStarts), which is the air
   between the lines. */
.map-flight-grid { display: flex; flex-wrap: wrap; }
.map-flight-break { flex-basis: 100%; height: 8px; }
@container (min-width: 720px) {
    .map-flight-grid { flex-wrap: nowrap; }
    .map-flight-break { display: none; }
}
.map-flight-cell {
    position: relative;
    display: block;
    flex: 1 1 0;
    box-sizing: border-box;
    min-width: 0;
    margin: 0;
    padding: 0 4px 0 7px;
    border: 0;
    border-left: 1px solid var(--line);
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.map-flight-cell.is-wide { flex-grow: 1.5; }
.map-flight-lb {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 3px;
    color: var(--quiet);
    font-size: var(--fs-xs);
    line-height: 1.2;
    white-space: nowrap;
}
.map-flight-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Next's name in the route's magenta: the way to the plan (planche v2). */
.map-flight-lb.is-route { color: var(--chart-route); font-weight: 600; }
/* A box with a second value says so by two dots, the shown one filled (C.2). */
.map-flight-dots { display: inline-flex; flex: none; gap: 2px; }
.map-flight-dots i { width: 4px; height: 4px; border-radius: 50%; box-shadow: inset 0 0 0 1px var(--quiet); }
.map-flight-dots i.is-on { background: var(--quiet); }
/* The GPS altitude's accuracy, a dot before the value (C.5). */
.map-flight-tone { display: inline-block; width: 6px; height: 6px; margin: 0 4px 2px 0; border-radius: 50%; vertical-align: middle; }
.map-flight-tone[data-tone="good"] { background: var(--green); }
.map-flight-tone[data-tone="fair"] { background: var(--orange); }
.map-flight-tone[data-tone="poor"] { background: var(--red); }
/* The point's bar, flown share in the route's ink; the PET's ring under it,
   its word in small at its right (C.3, D.3). */
.map-flight-cell:has(.map-flight-pet) { padding-bottom: 13px; }
.map-flight-track { position: relative; display: block; height: 3px; margin-top: 4px; border-radius: 2px; background: var(--line); }
.map-flight-fill { position: absolute; top: 0; bottom: 0; left: 0; width: calc(var(--frac, 0) * 100%); border-radius: inherit; background: var(--chart-route); }
.map-flight-pet {
    position: absolute;
    top: 5px;
    left: calc(var(--pet, 0) * 100% - 4px);
    display: flex;
    align-items: center;
    gap: 2px;
    color: var(--muted);
    font-size: var(--fs-2xs);
    line-height: 1;
}
.map-flight-pet::before { content: ""; width: 8px; height: 8px; box-sizing: border-box; border: 1.5px solid var(--text); border-radius: 50%; }
.map-flight-v {
    display: block;
    font-size: var(--fs-read);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
    white-space: nowrap;
}
.map-flight-u { margin-left: 1px; color: var(--muted); font-size: var(--fs-2xs); font-weight: var(--fw-regular); }
/* Fuel dest under the final reserve (planche PERF D.3): the figure in red, still written. */
.map-flight-v.is-low { color: var(--red); }
/* The band takes the head of the map: the column starts under it, and the
   source and the clock line step aside while it stands (map-flight.js writes
   its height). */
/* The band already carries the status bar's inset: under it, the column needs
   no second one (the field's gap between the band and Flying). */
body.map-view-open:is(.flying, .flight-proposal) .page-header { top: var(--flight-bar-h, 0px); padding-top: 8px; }
body.map-view-open.flying .source-row,
body.map-view-open.flying #mapTimesLine { visibility: hidden; }
/* The night's word stands on Fly's line, centred on its height. */
body.map-view-open.flying .map-night { top: calc(var(--flight-bar-h, 0px) + 8px + (var(--control-h) - 22px) / 2); }
/* A sheet at the right (panels-side, planche DCT E): the band stops at its
   edge, and the column with Terrain and Weather stands against it, every
   disc in reach with the sheet open (owner's decision, LOT 6 A). */
body.panels-side.right-open .map-flight-bar { right: var(--panel-w); }
body.panels-side.right-open.map-view-open .page-header { right: var(--panel-w); }
body.panels-side.right-open.map-view-open .map-foot-discs { right: calc(var(--panel-w) + var(--map-chrome-x)); }
/* THE NORTH, WRITTEN ON THE MAP (V-22; owner's decision, planches NORD 2
   n° 9, MAP N1): in flight and Track-up, at the left under the band — under
   the night's word when it is said —, the charts' true north arrow, its tip
   half filled, turning with the map (#mapNorth), on a halo of the paper; 30 px,
   its N upright at the tip (--north-x, --north-y, set with the turn; V-21). */
.map-north-mark {
    position: absolute;
    left: calc(var(--map-chrome-x) + var(--map-inset-left, 0px));
    top: calc(var(--flight-bar-h, 0px) + 20px);
    z-index: var(--z-chrome);
    width: 30px; height: 30px;
    pointer-events: none;
}
.map-night:not([hidden]) ~ .map-north-mark { top: calc(var(--flight-bar-h, 0px) + 8px + var(--control-h) + 20px); }
.map-north-mark[hidden] { display: none; }
.map-north-arrow { position: absolute; inset: 0; width: 30px; height: 30px; overflow: visible; }
#mapNorth { transform-origin: 24px 24px; }
.map-north-shaft { fill: none; stroke: var(--text); stroke-width: 3; filter: drop-shadow(0 0 1.5px var(--surface)); }
.map-north-east { fill: var(--text); stroke: var(--surface); stroke-width: 1.8; stroke-linejoin: round; }
.map-north-west { fill: var(--surface); stroke: var(--text); stroke-width: 2.4; stroke-linejoin: round; }
.map-north-letter {
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(calc(-50% + var(--north-x, 0px)), calc(-50% + var(--north-y, -21px)));
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    line-height: 1;
    color: var(--text);
    text-shadow: 0 0 3px var(--surface), 0 0 3px var(--surface), 0 0 3px var(--surface);
}
/* THE ROUTE SHEET (planche ENCART B): the route's own rows — thread, numbered
   disc, code — ending on the three chosen columns; their heads choose them. */
.flight-route-head {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 36px;
    padding: 0 12px;
    border-bottom: 1px solid var(--line);
    color: var(--quiet);
    font-size: var(--fs-xs);
}
.flight-route-head > span:first-child { flex: none; width: var(--route-rail-w, 28px); }
.flight-route-head > span:nth-child(2) { flex: 1 1 auto; }
.flight-route-head > button {
    flex: none;
    width: 64px;
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: right;
    cursor: pointer;
}
.flight-route-num {
    flex: none;
    width: 64px;
    font-size: var(--fs-read);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}
.flight-route-row.is-passed { color: var(--muted); }
.flight-route-row.is-next { background: var(--surface-2); }
.flight-route-next {
    margin-left: 6px;
    padding: 1px 6px;
    border: 1px solid var(--text);
    border-radius: 999px;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.05em;
}
/* FLIGHT SETTINGS (planche VOL-RÉGLAGES): Account's grammar — a card per
   subject. The inset and the route sheet's head are drawn inside their card
   as they fly, with no box of their own (no box in the box), each box a
   button; the one whose wheel is up is framed, solid. */
.map-flight-bar.is-replica {
    position: static;
    margin: 2px -4px 0;
    padding: 4px 0 6px;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    cursor: default;
}
/* The replica's boxes are held and dragged along the band (reorder.js); where
   one would land, the reorder's upright rule before it. */
.is-reorderable .map-flight-cell { touch-action: none; user-select: none; -webkit-user-select: none; }
.map-flight-cell.is-dragging { opacity: 0.45; z-index: var(--z-sticky); }
.is-reorderable .map-flight-cell.drop-target::before,
.is-reorderable.map-flight-grid.drop-end::after {
    content: "";
    position: absolute;
    top: 2px;
    bottom: 2px;
    left: -2px;
    width: 3px;
    border-radius: 2px;
    background: var(--text);
}
.is-reorderable.map-flight-grid.drop-end { position: relative; }
.is-reorderable.map-flight-grid.drop-end::after { top: auto; left: auto; right: 0; height: 36px; }
.map-flight-cell.is-picked,
.flight-route-head > button.is-picked { border-radius: var(--radius-sm); box-shadow: inset 0 0 0 1.5px var(--text); }
.flight-route-head.is-replica { margin: 2px -4px 0; border: 0; background: none; box-shadow: none; }
.flight-settings-page > * + * { margin-top: var(--card-gap); }
.settings-card > .schedule-note { margin: 2px 0 8px; }
/* The own-ship's four signs are a segmented like any other: the sign is the
   word, the chosen one underlined (planche VOL-RÉGLAGES). */
.flight-segmented { display: inline-flex; gap: 14px; }
.flight-segmented .view-mode-btn:not(.active) { color: var(--muted); }
.flight-symbol-icon { display: block; width: 22px; height: 22px; fill: currentColor; stroke: none; }
/* Find's kinds, over its one list (planche VOL-RÉGLAGES). */
.view-mode-toggle.dct-tabs.fix-filters { margin: 4px 0 6px; }
.flight-route-foot { display: flex; gap: 14px; padding: 12px 12px 4px; color: var(--muted); font-size: var(--fs-sm); }
.flight-route-foot b { color: var(--text); }
.flight-route-empty { margin: 12px; color: var(--muted); }
/* A point the sheet names but does not fly: the position the first leg
   starts from (planche DCT C). */
.flight-route-row.is-here { color: var(--muted); }
.flight-route-row .sheet-sub { display: block; font-size: var(--fs-xs); color: var(--muted); }
/* THE DIRECT-TO (planche DCT): the disc writes the ICAO abbreviation and lights
   while a direct-to is flown; its sheet's heads name two columns, Brg and Dist. */
.map-dct-word { font-size: var(--fs-xs); font-weight: var(--fw-semibold); letter-spacing: 0.04em; color: var(--muted); }
.map-dct-btn[aria-pressed="true"] .map-dct-word { color: var(--text); }
.flight-route-head > span:nth-child(n+3) { flex: none; width: 64px; text-align: right; }
.view-mode-toggle.dct-tabs { display: flex; width: auto; margin: 4px 12px 8px; }
.dct-tabs > .view-mode-btn { flex: 1 1 0; min-height: 44px; }
.dct-lead { flex: none; width: var(--route-rail-w, 28px); }
.route-to-mark { width: 7px; height: 7px; margin: 0 5px; vertical-align: 0.05em; fill: currentColor; }
/* THE WEATHER IN FLIGHT (planche VMET): a field's METAR pastilles beside its
   code, pressed for the field's sheet; on the map, the TAF's rings step aside. */
.flight-wx { display: inline-flex; align-items: center; margin: -10px 0 -10px 8px; padding: 10px 6px; cursor: pointer; vertical-align: middle; }
/* The flight goes on while another page is read (planche SORTIE B): the Map
   tab says « Flying » and wears a dot in the colour of a fix that holds. */
.view-switcher-btn.is-flying { position: relative; }
.view-switcher-btn.is-flying::before { content: ""; position: absolute; top: 6px; left: calc(50% + 8px);
    width: 9px; height: 9px; border-radius: 50%; background: var(--status-ok); }
body.flying #briefingMap .ap-ring:not([data-ring="metar"]) { display: none; }
/* A METAR FROM A PREVIOUS SLOT (owner's decision, planche LOT 4): its
   pastilles ringed in a solid red, on the map and the flight's route sheet —
   the group, never the code; the field's sheet keeps its written age. */
#briefingMap .ap-rings[data-old],
.flight-wx .ap-rings[data-old] { padding: 2px; border-radius: 999px; box-shadow: 0 0 0 1.5px var(--red); }
/* D13: the Weather disc barred in flight by the operator's rule says so. */
/* THE FLIGHT'S MESSAGES (owner's decision, planche MAP A1): the band's last
   line, edge to edge under a hairline — the gravest, the count of the others,
   a cross when the pilot may put it away; the position's and the terrain's on
   red, with none. A press opens them all. An alert is the Report's summary
   line: the field's dot in its minima's ink, its code, the value that breaks,
   the hour in the quiet ink. */
.map-flight-msg {
    display: flex;
    align-items: center;
    min-height: 44px;
    margin: 8px -6px -8px;
    border-top: 1px solid var(--line);
    color: var(--text);
    font-size: var(--fs-sm);
    white-space: nowrap;
}
.map-flight-msg[hidden] { display: none; }
/* Airborne? on the ground: the band's message line alone (planche v2). */
.map-flight-bar.is-proposal { padding-top: env(safe-area-inset-top, 0px); }
.map-flight-bar.is-proposal .map-flight-grid { display: none; }
.map-flight-bar.is-proposal .map-flight-msg { margin-top: 0; border-top: 0; }
.map-flight-msg.is-red { border-top-color: var(--red); background: var(--red); color: var(--surface); }
.map-flight-msg-open {
    flex: 1;
    min-width: 0;
    min-height: 44px;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 0 12px;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.map-flight-msg-text { min-width: 0; overflow: hidden; font-size: var(--fs-read); font-weight: var(--fw-semibold); letter-spacing: 0.04em; text-overflow: ellipsis; }
/* On a phone the alert line reads at 13 px, on the tablet at 16 (owner's call, planche SETTINGS F2). */
@media (max-width: 599.98px), (max-height: 500px) {
    .map-flight-msg-text { font-size: var(--fs-sm); }
}
.map-flight-msg-more { flex: none; margin-left: auto; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); font-size: var(--fs-xs); font-weight: var(--fw-semibold); }
.map-flight-msg.is-red .map-flight-msg-more { background: color-mix(in srgb, var(--surface) 22%, transparent); }
.map-msg-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--alert-dot, var(--red)); }
.map-msg-v { min-width: 0; overflow: hidden; font-size: var(--fs-read); font-weight: var(--fw-semibold); text-overflow: ellipsis; }
.map-msg-t { flex: none; color: var(--muted); }
.map-msg-show { flex: none; min-height: 44px; padding: 0 12px; border: 0; border-radius: 999px; background: var(--surface-2); color: var(--text); font: inherit; font-weight: var(--fw-semibold); cursor: pointer; }
.map-flight-msg-x { position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; border: 0; background: none; color: var(--muted); cursor: pointer; }
.map-flight-msg-x svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
/* THE NIGHT'S WORD (owner's decision, planche VOL-RÉGLAGES): out of Fly, on
   the left of the map level with it, on the reading pill's glass
   (.map-reading); its hour half an hour before, the moon during it. */
.map-night {
    position: absolute;
    left: calc(var(--map-chrome-x) + var(--map-inset-left, 0px));
    z-index: var(--z-chrome);
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    padding: 4px 10px;
    border-radius: 999px;
    color: var(--text);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    box-sizing: border-box;
    height: 22px;
    line-height: 1.2;
    white-space: nowrap;
    background: color-mix(in srgb, var(--card) 25%, transparent);
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
    pointer-events: none;
}
.map-night[hidden] { display: none; }
/* The moon alone (owner's decision): a round pill of its own height. */
.map-night:not(:has(span)) { width: 22px; padding: 0; justify-content: center; }
.map-night-moon { width: 14px; height: 14px; flex: none; fill: currentColor; stroke: none; }
/* WHERE THE POSITION IS while the follow is suspended (D.6; V-25): an arrow
   at the screen's edge, turned toward it, in the symbol's own inks. */
.map-pos-edge {
    position: absolute;
    z-index: var(--z-chrome);
    width: 28px;
    height: 28px;
    transform: translate(-50%, -50%) rotate(var(--edge-rot, 0deg));
    pointer-events: none;
}
.map-pos-edge[hidden] { display: none; }
.map-pos-edge svg { display: block; width: 100%; height: 100%; fill: var(--ownship-ink); stroke: var(--ownship-paper); stroke-width: 1.5; }
/* On the Profile the same disc keeps the header's order, Weather then the
   gear (owner's decision, planche P1): the DOM holds it after the column. */
body.route-profile-open .header-discs #mapWeatherBtn { order: -1; }
body:not(.map-view-open) #mapTerrainBtn { display: none; }
/* No weather in flight — the operator's rule, or no radar (planche MVOL F.1):
   no Weather disc; Terrain, over it in their group, comes down into its place. */
body.no-flight-weather #mapWeatherBtn { display: none; }
/* THE GLASS IS BEHIND THE WORDS ONLY (owner's decision, planche HEMS C.6):
   the reading's frosted glass, a 25 % veil and a 5 px blur, without a
   contour, under a value or a name of the foot's line — never a box round a
   whole key; the bars stand on the map itself. */
.map-glass-text {
    padding: 0 3px;
    border-radius: 3px;
    background: color-mix(in srgb, var(--card) 25%, transparent);
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
}
/* The line's left: Terrain's key over the scale (planche HEMS C.3). */
.map-foot-left { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
/* THE MAP'S SCALE (owner's ask), in NM, on the line's left (D.4). */
.map-scale {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    font-size: var(--fs-2xs);
    line-height: 1.2;
    color: var(--text);
    pointer-events: none;
}
.map-scale[hidden] { display: none; }
.map-scale-bar {
    width: var(--scale-px, 0px);
    height: 4px;
    border: 1.5px solid currentColor;
    border-top: 0;
}
.map-timebar {
    /* The ground is the BAND's and not each storey's: flat, a hairline on
       top, as the navigation it stands on. The storeys are told apart by a
       hairline and carry their own air (8), which with the band's own makes
       the gutter at each edge. */
    flex-flow: column nowrap;
    align-items: stretch;
    gap: 0;
    padding: 0 calc(var(--main-gutter) - 8px);
    border: 0;
    border-radius: 0;
    /* En verre de barre, bordée du liseré (planche HABILLAGE B.6). */
    background: var(--glass-bar);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop-up), inset 0 1px 0 var(--glass-line), inset 0 6px 10px -6px var(--glass-glow);
}
.map-timebar > * {
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}
/* TWO PLACES ON ONE LINE: the hour on the left, and at the right what the
   lit reading needs set — the rung, or Model | Minima (owner's decisions,
   planches MAP 3 and 5; the cameras' disc left the line, planche CAM). A
   disc's height, so the line stays where it was. */
.map-timebar > .map-clock-row {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 8px;
    padding: 3px 8px;
}
/* The two capsules' last lines are one height (planche CENTRE C.5), and
   since the Profile carries the map's capsule (planche TEMPS L.6) one rule. */
.map-timebar > .map-clock-row { height: calc(var(--control-h) + 8px); }
.map-clock-row > .map-radar-bar { grid-column: 1; justify-self: start; }
/* Play ‹hour› never gives way: the fil takes the width the measure gives it
   (planche TEMPS), the arrows and the hour keep theirs. */
.map-clock-row .map-radar-bar .hour-stepper { width: auto; flex: none; }
.map-clock-row > .map-ramp-switch,
.map-clock-row > .map-alt-step { grid-column: 2; }
.map-clock-row > .map-ct-gear { grid-column: 2; }
/* The segmented's words carry no padding of their own (planche INTER I.5): the
   air from the capsule's rounded edge is the switch's, so its hairline never
   runs into the curve. */
.map-clock-row > .map-ramp-switch { margin-right: 7px; }
/* THE CAPSULE'S RIGHT EDGE (owner's ask): what ends each line — the unit, the
   segmented, the radar's gear — ends 16 px inside the capsule, the left side's
   margin; drawings, not boxes, measured at the browser. */
#mapWxRamps .rp-scale-unit-pick { margin-right: -3px; }
.map-timebar .radar-marks-btn { margin-right: -7px; }
/* THE LAYER BESIDE ITS LEGEND (owner's decision, planche MAP 3): its name in a
   column of its own, the ramp or the badges taking the rest of the storey.
   The ramp's bar ends on the name's bottom edge (planche MAP 4): one line
   under both. */
.map-wx-legend:has(> .map-layer-anchor:not([hidden])) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: end;
    column-gap: 8px;
}
.map-wx-legend > .map-layer-anchor { grid-row: 1; }
.map-wx-legend:has(> .map-layer-anchor:not([hidden])) > :not(.map-layer-anchor) { grid-column: 2; }
.map-layer-anchor { display: flex; }
.map-layer-anchor[hidden] { display: none; }
/* The layer's name, which opens the list: its icon and the word, no chevron
   and no contour (owner's decisions, planches LEGENDE G.2 and ISOLEG C2) — the
   icon and the word are one button; the finger keeps 44 px through ::after. */
.map-layer-pick {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    max-width: 100%;
    line-height: 1;
    /* 5 px on the left: the icon stands 16 px inside the capsule's edge, the
       app's side margin (owner's ask, measured at 13 before). */
    padding: 0 6px 0 5px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    box-shadow: none;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    cursor: pointer;
}
.map-layer-pick::after { content: ""; position: absolute; inset: -14px -4px; }
/* The NAME'S letters end on the ramp's bar (owner's ask): the box ends on it
   already, but a word's letters stand on their baseline, a descender above
   the box's bottom — the word is lowered by that much. */
.map-wx-legend:has(> .map-layer-anchor:not([hidden])) .map-layer-pick { translate: 0 0.2em; }
.map-layer-pick > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-layer-pick svg {
    flex: none;
    width: 16px;
    height: 16px;
    fill: none;
    stroke: var(--text);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.map-layer-pick[aria-expanded="true"] { background: var(--surface-2); }
/* The segmented at the capsule's height: the row's own control height. */
.map-ramp-switch .tile-ramp .view-mode-btn {
    min-height: 28px;
    padding: 3px 0;
}
/* ONE CHEVRON ON THE CAPSULE (owner's decisions, planches MAP 5 and W1): the
   hour's, the rung's and the layer name's are one drawing (a 12 × 6 bend) at
   14 px, drawn close to the value — 24 px wide, the finger keeping 44 through
   the zone's own overhang. */
.map-timebar .hour-step { width: 24px; }
.map-timebar .hour-step::after { inset: -7px -10px; }
.map-timebar .hour-step svg { width: 14px; height: 14px; }
.map-timebar .hour-stepper-value { padding: 0 4px; }
/* The play disc's triangle stands under the legend's layer icon, left edge on
   left edge, 16 px inside the capsule (owner's asks; measured). */
.map-timebar .hour-play { margin-left: -3px; }
/* ONE LINE WHEN IT FITS (planche TEMPS L.1, L.2, written by layoutCapsule),
   in the owner's order (planche METEO-RAIL R.5): the layer and its legend —
   name, ramp, unit — then Model / Minima, a hairline, the time, then the
   altitude or the cloud type's gear. The two storeys dissolve into the band's
   own row (display: contents) and their children take their order. The ramp
   and the fil are one width, the measure's (R.6). */
.map-timebar.is-one-line {
    flex-flow: row nowrap;
    align-items: center;
    gap: 8px;
    min-height: calc(var(--control-h) + 8px);
    padding: 3px var(--main-gutter);
}
.map-timebar.is-one-line > .map-wx-legend,
.map-timebar.is-one-line > .map-clock-row,
.map-timebar.is-one-line .map-wx-ramps { display: contents; }
.map-timebar.is-one-line .map-layer-anchor { order: 1; }
.map-timebar.is-one-line .rp-scale { order: 2; }
.map-timebar.is-one-line .map-ramp-switch { order: 3; }
.map-timebar.is-one-line > .map-wx-legend::before {
    content: "";
    order: 4;
    width: 1px;
    height: 24px;
    background: var(--contour);
}
.map-timebar.is-one-line .map-radar-bar { order: 5; flex: none; }
.map-timebar.is-one-line .map-alt-step { order: 6; }
.map-timebar.is-one-line .map-ct-gear { order: 6; }
/* The ramp's width is the measure's in both forms: the fil's on one line
   (R.6), the clock row's width on two storeys. */
.map-timebar .rp-scale-track { flex: 0 0 var(--ramp-w, 140px); }

/* La zone de contact d'un segment de rail, verticale seulement : deux segments
   voisins se touchent par le côté, donc un débordement latéral les ferait se
   recouvrir — et deux cibles qui se chevauchent se volent les touches. En
   hauteur il y a de la place, et pas la même des deux côtés : 6 px vers le haut
   où la rangée voisine est à 12, 10 vers le bas où il y en a 209. 28 dessinés,
   44 de contact, 6,9 mm sur un iPad mini. */
.view-mode-btn { position: relative; }
/* A segmented's word is drawn at its own width (planche INTER I.9); its target
   reaches halfway into the air on each side, so two never overlap. */
.view-mode-btn::after {
    content: "";
    position: absolute;
    inset: -6px calc(var(--pill-gap) / -2) -10px;
}
/* Segmented control: a neutral (--bg) fully-rounded track holds the buttons
   flush (no gap between them); a 2px inner rim of the track's own fill insets
   the selected pill's fill from the track edge. Inactive pills are transparent
   (text only), the selected one carries a --soft-line fill (no contour) and
   heading-ink text. Shared by the floating briefing view dock and the schedule
   form's View toggle. */
/* The ground and the (absent) contour are the shared rail's, said once with
   the other two rail classes below; only what is this one's own is here. */
.view-mode-toggle {
    display: inline-flex;
    align-items: stretch;
    gap: var(--pill-gap);
    white-space: nowrap;
}
.view-mode-btn {
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--muted);
    min-height: var(--control-h);
    padding: 4px 0;
    /* A segmented speaks like the settings it stands in, at 13 px, on every
       page and in the map's capsule alike (planche INTER I.2). */
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    cursor: pointer;
}
.view-mode-btn.active { color: var(--heading); }
@media (hover: hover) {
    .view-mode-btn:not(.active):hover {
        background: var(--hover-bg);
    }
}
/* Round icon buttons — shared by the settings toggle and the config card
   header ("…"): one class, one size, one shape everywhere. */
/* LA ZONE DE CONTACT N'EST PAS LE DESSIN. Un doigt atterrit où il atterrit ; le
   dessin n'est que ce que l'œil vise. WCAG mesure la zone de contact, et les
   essais en turbulence aussi — d'où le motif employé ici et plus bas : un
   pseudo-élément transparent qui déborde du bouton, sans qu'un pixel peint
   change. Deux conditions, toutes deux vérifiées au navigateur avant d'écrire
   un `inset` : rien ne clippe le débordement sur la chaîne des ancêtres, et
   deux zones élargies ne se recouvrent pas — deux cibles qui se chevauchent se
   volent les touches, ce qui est pire que deux petites. */
.round-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--round-btn-size);
    min-width: var(--round-btn-size);
    height: var(--round-btn-size);
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: var(--soft-line);
    color: var(--muted);
    box-shadow: none;
    cursor: pointer;
}
/* Le disque se dessine de 24 à 40 px selon la surface ; sa zone de contact fait
   44 partout, jamais moins que le disque. Sur ::before, parce que ::after porte
   l'infobulle au survol. Le bouton de la tête a déjà sa zone, taillée à ses
   voisins, et n'en reçoit pas une seconde. */
.round-icon-btn:not(.wx-header-btn) { position: relative; }
.round-icon-btn:not(.wx-header-btn)::before {
    content: "";
    position: absolute;
    inset: min(0px, (var(--round-btn-size) - 44px) / 2);
}
.round-icon-btn svg { width: var(--icon-box); height: var(--icon-box); }
/* The close disc of every sheet and page carries this class: the muted glyph
   of the shared .round-icon-btn. */
.settings-toggle-btn svg,
.report-pdf-btn svg {
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* The PDF Report disc, in the header-top's right slot — the drawer handle's
   mirror. It exports the Report page, so it leaves with it on Map/Profile
   (where that slot stays reserved, keeping the rail centred). */
.report-pdf-btn { position: relative; }
body.map-view-open .report-pdf-btn,
body.route-profile-open .report-pdf-btn { display: none; }
/* The map's discs take the group on exactly the pages the PDF disc gives up.
   Recentre and the ground (.map-tool-btn) are the Map's alone: the chart has
   its own capsule and its own zoom. */
.wx-header-btn { display: none; }
/* The Navigation disc wears a gear on the Profile page, where it opens the
   chart's settings (owner's decision, planche P1). */
.wx-btn-gear,
#mapWxBtn.is-settings .wx-btn-nav { display: none; }
#mapWxBtn.is-settings .wx-btn-gear { display: block; }
body.map-view-open .wx-header-btn,
body.route-profile-open .wx-header-btn { display: inline-flex; }
body.route-profile-open .map-tool-btn { display: none; }
/* UN SEUL DESSIN POUR LES CONTRÔLES DE LA TÊTE (décision du propriétaire) : le
   disque PDF et le bouton replié « Decoded · LT » portent la même classe — même
   hauteur (--round-btn-size), même fond, même anneau d'un pixel, aucune ombre.
   La couche de navigation reste un cran sous les données (planche 14). */
.header-actions .header-ctl {
    /* L'anneau est un DESSIN (inset), pas une bordure : une bordure décalait la
       zone de contact du disque, 42 px au lieu de 44 (§ 5.6). En verre fin, le
       liseré d'Apple et l'ombre diffuse (planche HABILLAGE B.4). */
    border: 0;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
}
/* Les disques de la carte portent un vrai contour de 1 px et aucune ombre
   (§ 8.4) ; leur zone de contact, posée sur la boîte intérieure, gagne ce pixel
   (::after plus bas). */
.header-actions .wx-header-btn {
    /* Le pixel de bordure reste, transparent : la zone de contact (::after) est
       taillée sur lui. */
    border: 1px solid transparent;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
}
/* L'ÉTAT ACTIF D'UN BOUTON DE LA CARTE EST NEUTRE (docs/ui/carte.md, « Weather est un interrupteur, Navigation une feuille ») :
   le creux seul, jamais une teinte de marque — l'icône garde l'encre de tous
   les boutons (décision du propriétaire). */
.header-actions .wx-header-btn:is(.active, [aria-expanded="true"], .map-tool-btn[aria-pressed="true"]):not(.is-settings) {
    background: var(--surface-2);
}
/* WEATHER ON IS WRITTEN IN THE READING INK (owner's decision): the disc turns
   solid black, its cloud white — the one switch whose state changes the whole
   picture says so from across the cockpit. The ink pair flips with the theme. */
.header-actions :is(#mapWeatherBtn, #mapTerrainBtn)[aria-pressed="true"] {
    /* Allumé, il garde son verre, cerclé de 2,5 px d'encre (planche HABILLAGE B.5) :
       la même grammaire que la tuile d'une couche allumée, et pas de disque clair
       et plein dans un cockpit de nuit. */
    box-shadow: var(--glass-drop), inset 0 0 0 2.5px var(--text), var(--glass-rim);
}
.header-actions :is(#mapWeatherBtn, #mapTerrainBtn)[aria-pressed="true"] svg { stroke: var(--text); }
.header-top .report-pdf-btn { color: var(--muted); }
/* LE SOMMAIRE DU REPORT (décision du propriétaire, planches SOMMAIRE D1 et
   COLLANT) : le segmenté de l'app, juste sous les boutons, dans l'en-tête qui
   colle. L'horloge et la fenêtre au bout de sa ligne ; ce qui ne tient pas en
   entier passe dessous, collé, aligné à droite (flex-wrap + marge auto). */
.report-sommaire {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 10px;
    width: 100%;
    /* Remonté contre les boutons : 4 px d'air au lieu des 8 de l'en-tête. */
    margin-top: -4px;
}
.report-tabs { --pill-gap: 14px; flex: none; }
body.map-view-open .report-sommaire,
body.route-profile-open .report-sommaire { display: none; }
/* L'HORLOGE ET LA FENÊTRE S'ÉCRIVENT COMME LES HEURES DU METAR (décision du
   propriétaire) : la taille, l'encre et la graisse de `.rg-src .rg-time`. Un
   appui ouvre la page Display. */
.report-clock {
    margin-left: auto;
    color: var(--quiet);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    white-space: nowrap;
}
body.map-view-open .report-display-btn,
body.route-profile-open .report-display-btn { display: none; }
.header-top .report-display-btn { color: var(--muted); }
.report-display-btn svg { fill: none; stroke: currentColor; stroke-width: var(--icon-stroke); stroke-linecap: round; }
/* A SHORTCUT PILL TAKES THE FULLEST FORM THAT FITS (K.3, fitShortcut): its icon
   and its words, its words alone, or its icon alone — then a disc. It may
   shrink below its words, which is what the measure reads; its words are
   never cut on screen, the measure moves it to a shorter form first. Drawn at
   the disc's height (.header-ctl), 44 px of contact. */
.shortcut-pill {
    position: relative;
    display: inline-flex;
    /* The head's dress (.header-ctl) — thin glass, Apple's rim, the diffuse
       drop — worn on the NOTAM line too. */
    border: 0;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
    align-items: center;
    gap: 6px;
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
    min-height: var(--round-btn-size);
    padding: 0 14px;
    border-radius: 999px;
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    cursor: pointer;
}
.shortcut-pill::after {
    content: "";
    position: absolute;
    inset: min(0px, (var(--round-btn-size) - 44px) / 2) 0;
}
.shortcut-pill svg {
    width: 18px;
    height: 18px;
    flex: none;
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.shortcut-pill[data-form="icon"] { justify-content: center; flex: none; width: var(--round-btn-size); padding: 0; }
/* Drawn at the head discs' size (--round-btn-size of .page-header). */
.zone-tool .shortcut-pill { --round-btn-size: 40px; }
/* THE NAMES THE COLOURS ANSWER TO, sous le titre du bloc météo (planche TETE
   A.2) : les configurations que la page peint en ce moment, chacune dans son
   encre, par la marque que la carte et la coupe posent déjà. Elle a longtemps
   vécu sur la ligne de l'en-tête, loin de ce qu'elle nomme, et se taisait là où
   la barre latérale nommait les mêmes configurations. Dans la tête du bloc, elle
   est dans le fil de la lecture : plus de règle d'écran. Quelle valeur casse
   reste l'affaire de syncMinimaLegend, qui cache le porteur quand rien ne casse. */
.report-minima-legend {
    display: flex;
    flex-wrap: wrap;
    /* The words stand apart on their own, with no pill to separate them: the
       measure between two names is what tells them apart. */
    gap: 12px;
    padding: 0 0 6px;
}
/* THE NAME ITSELF CARRIES THE COLOUR (owner's call), the way the report carries
   it everywhere else — an airport's code, a broken value. No fill: a fill on
   this page means "this value breaks a minimum", and a legend states nothing
   about a value. The ink is the alert's own, written by the pass. Size and
   weight are the bulletin title's, and the case is the PILOT'S OWN: he named
   the configuration, the legend does not re-spell it. */
.report-minima-legend > b {
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.02em;
}
.report-minima-legend[hidden] { display: none; }
/* On the map's Weather tab each name follows the ring it explains. */
.map-minima-legend { padding: 0; }
.map-minima-legend > b { display: inline-flex; align-items: center; gap: 4px; }
/* In the airport's sheet, the pastilles stay 9px whatever text they sit by (owner's call). */
#mapAirportCard .ap-ring { width: 9px; height: 9px; }
/* SUR QUOI LE BLOC NOTAM EST TRACÉ : les filtres qui coupent, séparés par un
   trait. L'écriture est celle de la
   ligne qui nomme une source dans une carte — le même « ce qu'on lit, un cran
   au-dessus ». Mot nu, sans creux ni contour : la page n'a pas de surface ici. */
.zone-scope {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    /* L'air d'une espace, comme entre « LT », « | » et la fenêtre de l'horloge. */
    column-gap: 0.3em;
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    padding: 0 0 6px;
    /* TOUTE LIGNE DE RÉGLAGES S'ÉCRIT COMME L'HORLOGE DU REPORT (décision du
       propriétaire, planche RÉGLAGES-LIGNE R.10) : la taille, l'encre et la
       graisse de `.report-clock`, son « | » dans la même encre. */
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    color: var(--quiet);
}
/* The briefing actions "…" menu (PDF / schedules / history) toggles via the
   hidden attribute; everything else about it is the config card's .block-menu. */
.block-menu-list[hidden] { display: none; }
/* MESSAGES: in the ink of the page, never in the flow of the report. The
   Report's status bar stands just ABOVE the menu — clear of it by the same
   measure the page keeps (--app-menu-clear); the register/upgrade toast
   (auth.js) lies ON the menu (below); the map's status line stands above its
   capsule further down. */
.status-bar,
#hwxToast {
    position: fixed;
    left: 50%;
    bottom: calc(max(var(--app-menu-clear), 12px) + var(--menu-safe));
    transform: translateX(-50%);
    z-index: var(--z-toast);
    width: max-content;
    max-width: min(92vw, 560px);
    padding: 9px 16px;
    border-radius: var(--radius-md);
    /* LEVÉ SANS ALLUMER (§ 2.1, § 3.3, § 5.7) : la feuille (--surface), son
       anneau --line, et le mot en --muted ; un échec en --status-error. Un
       bandeau inversé était un aplat blanc sur le thème sombre. */
    background: var(--surface);
    color: var(--muted);
    box-shadow: 0 0 0 1px var(--contour);
    font-size: var(--fs-md);
    text-align: left;
}
.status-bar.is-error, .map-status.is-error, .load-error { color: var(--status-error); }
.map-status { text-align: left; }
/* CSS multicol masonry, not a fixed column-count: the browser fits as many
   ~480px columns as the briefing width allows — 1 on a phone, 2/3/4+ as the
   screen widens, with no cap. 480px is the floor that keeps a decoded METAR/TAF
   line from wrapping; below it (narrow screens) you simply get one column. Each
   airport block stays whole (break-inside: avoid). Driven by content width, not
   by media breakpoints. */
/* ===== THE GRID OF BREACHES at the head of the report (planche 18) =====
   One card per airport, its code at the head: only what breaks, each value in
   the ink of the configuration it breaks, and nothing else names the
   configuration except the card's own left edge, which carries the worst one.
   Above a value, what it is: "METAR" for the observation, the hour it breaks
   from with its TEMPO / PROB for a forecast. Everything fits in the card — a
   value wraps rather than leaving it, and the cards share the width whatever
   their number. Nothing broken is "Within limits". A card IS the way to that
   airport's own card in the page. */
.briefing-summary { margin: 2px 0 8px; }
/* ENTRE LE RÉSUMÉ ET SA SECTION, LE FILET DE DEUX TERRAINS (décision du
   propriétaire) : le résumé dit ce qui casse, les cartes le détaillent — la même
   rupture qu'entre deux terrains, 18 px d'air au-dessus (la marge du résumé se
   fond dans celle-ci) et 20 en dessous (la première carte en porte 6). Replié,
   le bloc n'a pas de cartes : pas de filet. */
.briefing-summary:not([hidden]) + .briefing-cols {
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}
.sum-grid {
    display: grid;
    /* As many columns as the width holds at 110px each, the rest to the next
       line (owner's choice): three on a phone, six on a tablet, no number
       written anywhere. A column never narrows under what a value can be read
       in. AND TWO ROWS ARE BALANCED (planche PANNEAU E.2): 4 + 4, not 7 + 1.
       The count is measured by balanceSummary (report-rail.js) off this very
       110px and written here as --sum-cols; until it is, auto-fit holds the
       first row. The column itself has no width cap (owner's decision, see
       .sum-col). */
    --sum-min: 110px;
    grid-template-columns: repeat(var(--sum-cols, auto-fit), minmax(var(--sum-min), 1fr));
    justify-items: start;
    gap: 8px;
    align-items: stretch;
}
/* UNE MINI-CARTE PAR TERRAIN, DANS LA ZONE QU'ELLE RÉSUME (décision du
   propriétaire) : les deux ne sont plus empilées sous un code commun en tête de
   page — la météo ouvre la zone Weather, les NOTAM ouvrent la leur. Le code
   revient donc DANS la carte : elle nomme son terrain elle-même, et elle mène à
   SA carte dans la page — la météo d'un terrain et ses NOTAM ne sont pas au même
   endroit, donc une seule destination par terrain ne suffit pas.
   PAS DE PLAFOND DE LARGEUR (décision du propriétaire).
   `width: 100%` et non la largeur du contenu : la grille pose des pistes d'au
   moins 110 px, et sans cela une carte se rétractait sur son texte — mesuré à
   80 px au navigateur, sous ce qu'une valeur se lit. */
.sum-col {
    display: grid;
    width: 100%;
    grid-template-rows: auto 1fr;
    gap: 3px;
    position: relative;
    min-width: 0;
    /* NO BAR OF COLOUR ON THE CARD (owner's call): a verdict is carried by the
       ink of the value that breaks, and by nothing else — so the card has no
       edge to leave room for, and its two sides are equal. */
    padding: 9px 10px 10px;
    border: 0;
    /* UNE MINI-CARTE PAR TERRAIN (décision du propriétaire, planche
       NAVIGATION B) : le voile de groupe du verre, le liseré d'Apple et
       l'ombre diffuse, comme les cartes de réglages. */
    border-radius: var(--radius-md);
    background: var(--glass-group);
    box-shadow: var(--glass-drop), var(--glass-rim);
    font: inherit;
    color: var(--text);
    text-align: left;
    cursor: pointer;
    overflow: hidden;
}
/* Still loading: the card is a dashed outline, and holds the row's height. */
.sum-col.is-empty { color: var(--muted); }
.sum-code {
    /* The code is written the SAME here and at the head of the airport's own
       card in the page (.airport-code-title): the summary's own writing, and
       the page came to it (owner's decision, planche 26 accord 2). */
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text);
    line-height: 1.15;
}
/* Everything on the card reads from the left (owner's call). */
.sum-c {
    display: grid;
    align-content: start;
    justify-items: start;
    gap: 4px;
    min-width: 0;
    text-align: left;
    overflow-wrap: anywhere;
}
/* Within limits is a state, not a value: it is written in the muted register
   the rest of the card's prose is in, never in the values' weight. */
.sum-c.is-within { align-content: center; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--muted); }
/* The source did not answer: the one red the report wears outside a verdict, as a stale age. */
.sum-c.is-unavailable { color: var(--red); }
/* The age beside METAR and TAF (planche ÂGE): the card's words and red, never bold. */
/* A briefing served from its last copy (planche COUPURE B): one red line above
   the summary, not a banner, closed by its one « Retry » (planche ECHEC C). */
.report-copy { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 10px 0 0; color: var(--red); font-size: var(--fs-sm); }
.report-copy-retry { flex: none; min-height: 44px; padding: 0 16px; border: 0; border-radius: 22px; background: var(--surface-2); color: var(--text); font: inherit; font-weight: var(--fw-semibold); cursor: pointer; }
.sum-age { margin-left: 6px; color: var(--red); font-weight: var(--fw-regular); white-space: nowrap; }
.sum-within { display: block; margin-top: 6px; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--muted); }
.sum-c small { font-size: var(--fs-2xs); font-weight: var(--fw-medium); color: var(--muted); letter-spacing: 0.04em; }
/* Le résumé Navigation dit les sens de piste et les fréquences TWR à la taille
   des autres résumés (planche NAVIGATION A.3). */
.sum-nav { font-size: var(--fs-xs); }
/* What the values are, then the values ON THAT LINE when they fit: two things
   broken in one slice read as one piece of news. */
.sum-it { display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: baseline; gap: 1px 6px; line-height: 1.2; }
.sum-w { flex-basis: 100%; font-size: var(--fs-2xs); color: var(--muted); }
.sum-v {
    font-family: var(--app-font);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
}
/* PAS D'ÉTIQUETTE SUR UNE MINI-CARTE (décision du propriétaire) : elle se tient
   dans la zone qu'elle résume, dont le titre dit le mot une fois pour toutes.
   TOUS LES ÉTATS ET PAS DE TOTAL : la somme des marques EST le compte du
   terrain. Elles portent les MARQUES de la liste — la même pastille, le même
   trait, la même encre par état — et non un chiffre teinté : une couleur seule
   nommait un état que rien ne dessinait. */
.sum-tally { display: flex; align-items: center; flex-wrap: wrap; gap: 3px 8px; min-width: 0; }
.sum-mark { display: inline-flex; align-items: center; gap: 3px; }
.sum-mark > b { font-variant-numeric: tabular-nums; font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text); }
/* 22 px est la mesure d'une rangée de liste, où la marque porte SEULE l'état d'un
   message ; ici elle accompagne un chiffre, et trois marques à 22 px ne tiennent
   pas dans une carte de 110 px. */
.sum-mark .notam-icon { width: 18px; height: 18px; border-radius: 9px; margin-top: 0; align-self: center; }
.sum-mark .notam-icon > .notam-mark { width: 11px; height: 11px; }
.sum-c-notam { gap: 3px; }

/* UNE SEULE COLONNE, TOUJOURS : six cases ne se coupent pas en deux. La page a
   porté une masonry qu'une règle plus bas annulait déjà — elle est retirée ici,
   avec le `break-inside` qui n'avait plus de colonnes à empêcher de couper.
   Mesuré au passage : un titre de zone collant ne colle pas à l'intérieur d'une
   multi-colonne, donc la ressusciter aurait coûté la moitié de R4. */
.briefing { display: block; }
.briefing-cols { display: block; }
/* L'ÉTAT VIDE (§ 5.7) : une phrase en --muted et le verbe, sur le sol. */
.empty-state {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 360px;
    margin: 32px auto 0;
}
.empty-state-text {
    margin: 0;
    color: var(--muted);
    font-size: var(--fs-md);
    line-height: 1.45;
}
.airport-card {
    /* No card container (docs/ui/grammaire.md, « Quatre familles d'écran ») : a field is read in one
       go, top to bottom, on the page's own ground. What sets two fields apart
       is the rule below, never a box. */
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 0;
    box-shadow: none;
    /* Left inset shared by the ICAO bar padding and the non-badge text lines
       (twilight, section titles + their metadata) so they all line up under the
       ICAO code. */
    --airport-inset: 8px;
}
/* The first card leads the page: its top padding is the last piece of the gap
   under the header, so it stays small — the header's own bottom spacing already
   sets the airports apart from the legend above them. */
.airport-card:first-child { padding-top: 6px; }
/* ENTRE DEUX TERRAINS, UN FILET DOUX (décision du propriétaire, § 5.3) : le
   gris des filets verticaux du METAR (--line) — 18 px d'air au-dessus, 20 en
   dessous. */
.airport-card + .airport-card {
    margin-top: 18px;
    padding-top: 20px;
    border-top: 1px solid var(--line);
}
/* Large screens: lay the airport blocks out in a CSS multicol masonry, 2 to 3
   columns depending on viewport width — see the 1300px breakpoint below. */
/* Desktop (non-mobile-layout) refinements. `pointer: fine` et pas seulement une
   largeur : le cran typographique vers le bas se justifie par un POSTE DE
   BUREAU — une souris, un écran à sa distance —, jamais par des pixels. Sans
   cette condition, le même iPad passait à la petite échelle en tournant en
   paysage : le même écran, la même distance de lecture, deux tailles de texte. */
@media (min-width: 841px) and (min-height: 501px) and (pointer: fine) {
    :root {
        /* Shift --fs-lg down one step, to --fs-md: the scale has no step
           between 12 and 14 (docs/ui/grammaire.md, « La typographie »), so the body stays at
           14. Mobile keeps 16px inputs, which avoid iOS focus-zoom. */
        --fs-lg: 0.875rem;   /* was 1rem */
        /* Thin scrollbars with no track fill (scrollbar-* are inherited, so
           this reaches every scroll container). */
        scrollbar-width: thin;
        scrollbar-color: rgba(var(--text-rgb), 0.28) transparent;
    }
    /* Narrow bar, but a solid always-visible thumb (a real handle you can grab
       and drag, not a faint hairline) with a minimum length so it stays easy to
       catch on long pages. */
    ::-webkit-scrollbar { width: 6px; height: 6px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb {
        background: rgba(var(--text-rgb), 0.4);
        border-radius: 999px;
        min-height: 40px;
    }
    }

/* LA TÊTE D'UN TERRAIN (docs/ui/grammaire.md, « La typographie ») : le code, le nom et ses
   actions sur le sol de la page — ni fond, ni ombre, ni filet sous elle. */
.airport-title {
    display: flex;
    /* CENTRÉE SUR LES MILIEUX, PLUS SUR LES FONDS DE CASSE (décision du
       propriétaire, mesurée) : une boîte sans texte pose son BAS sur la ligne de
       base, si bien que le mot « raw » et le pin se tenaient à 3,8 px l'un de
       l'autre en hauteur pour un code de 22,4 px. Les trois partagent une ligne. */
    align-items: center;
    gap: 9px;
    flex-wrap: wrap;
    min-width: 0;
    margin-bottom: 4px;
    padding: 0;
    background: transparent;
    border: 0;
    box-shadow: none;
    /* ONE WRITING FOR A CODE (owner's decision, planche 26 accord 2): the same
       size, weight and ink as the summary's own .sum-code — the grid's writing
       is the one that holds, and the page came to it. The same four letters
       were 20px medium here and 14px semibold up there. */
    font-size: var(--fs-md);
    line-height: 1.12;
    color: var(--text);
    font-family: var(--app-font);
    font-weight: var(--fw-semibold);
    letter-spacing: 0;
}
/* The field itself: what it is, how high, its runways and who to call. Map
   sheet only — the briefing page's cards stay about weather. A section of the
   sheet after the field's cards, on the sheet's own ground under one rule:
   never a card inside the sheet (docs/ui/carte.md, « Weather est un interrupteur, Navigation une feuille »). */
.airport-facts {
    margin: 11px 0;
    padding: 9px 0 0;
    border-top: 1px solid var(--line);
    font-size: var(--fs-md);
}
/* LA CARTE NAVIGATION DU REPORT (planche NAVIGATION A.1) : les faits ouvrent la
   carte, sous son titre — pas de filet au-dessus d'eux. */
.nav-facts > .airport-facts { margin-top: 0; padding-top: 0; border-top: 0; }
/* A SECTION OF A CARD'S TAB (owner's decision, planche SECTIONS P2): the
   Report's block title — its ink, its weight, its highlighter — at the card's
   scale, so it never outranks the field's code; nothing at its right. */
.card-section-title {
    container-type: inline-size;
    margin: 18px 0 4px;
    font-size: var(--fs-lg);
    font-weight: var(--fw-black);
    color: var(--heading);
}
.card-section-title .airport-fact-note { position: relative; font-size: var(--fs-xs); font-weight: var(--fw-regular); }
/* Its tabs, when a tab is long enough to need them, bring a section to the top. */
.card-section-jumps { margin: 4px 0 2px; overflow-x: auto; }
/* In a sheet they stay at its top while the tab scrolls under them, on the
   sheet's own ground; a section they bring up lands under them, not behind. */
.map-card-body .card-section-jumps {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    display: flex;
    margin-top: 0;
    background: var(--bg);
}
/* A tab's panel borrows the sheet's class but must not clip, or nothing in it sticks. */
.map-card-body > .map-card-panel { overflow: visible; }
.map-card-body .card-section-title { scroll-margin-top: calc(var(--control-h) + 12px); }
.nav-empty { margin: 0; color: var(--muted); font-size: var(--fs-sm); }
/* The facts of a place, one muted line (owner's decision, planche de choix
   5A): kind, elevation, IATA, city — separated by a middle dot, wrapping
   when the phone is narrow. */
.airport-facts-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px 6px;
    margin: 0 0 8px;
    color: var(--muted);
}
.airport-fact { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.airport-fact-sep { color: var(--soft-line); }
/* The type symbol rides its chip at the text's own size — same stroke
   convention as every other app icon, colour inherited from the chip. */
.airport-type-icon {
    width: var(--fs-md);
    height: var(--fs-md);
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.airport-fact-title {
    margin: 9px 0 3px;
    font-size: var(--fs-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
}
.airport-fact-list {
    margin: 0;
    padding: 0;
    list-style: none;
}
.airport-fact-list li {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 2px 0;
}
.airport-fact-list li + li { border-top: 1px solid var(--soft-line); }
.airport-fact-key {
    flex: 0 0 auto;
    min-width: 68px;
    color: var(--heading);
    font-weight: var(--fw-medium);
}
.airport-fact-val { min-width: 0; flex: 1 1 auto; color: var(--text); }
/* The AIS's own items (the Information tab): a label is a sentence, so it
   stands over its value, a paragraph alone takes the line. */
.airport-fact-list.is-stacked li { display: block; padding: 4px 0; }
.airport-fact-list.is-stacked .airport-fact-key { display: block; }
/* THE RADAR LINE (owner's decision, planche RLIGNE R2): one line-button under
   the METAR, between two hairlines — the radar's dish, the phenomenon in bold and its
   distance, the dBZ and the arrival in grey, « Map › » at the end. */
.radar-line {
    width: 100%;
    display: grid;
    grid-template-columns: 24px 1fr auto;
    align-items: center;
    gap: 6px;
    min-height: 52px;
    margin: 12px 0 4px;
    padding: 6px 2px;
    /* No hairline (owner's decision, planche LEGENDE G.8, R2): the METAR's and
       the TAF's blocks already rule the card. */
    border: 0;
    background: none;
    color: var(--heading);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.radar-line > svg { width: 18px; height: 18px; justify-self: center; }
.radar-line-head { display: block; font-size: var(--fs-md); }
.radar-line-head b { font-weight: var(--fw-semibold); }
.radar-line-head em { font-style: normal; color: var(--muted); white-space: nowrap; }
.radar-line small { display: block; color: var(--muted); font-size: var(--fs-sm); }
.radar-line-go { display: inline-flex; align-items: center; gap: 2px; color: var(--muted); font-size: var(--fs-sm); }
.radar-line-go svg {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.airport-fact-unit, .airport-fact-note { color: var(--muted); }
/* How far a beacon is, or "AD" when the source ties it to this field — pushed
   to the line's right end, where a distance column reads down the list. */
.airport-fact-distance {
    flex: 0 0 auto;
    margin-left: auto;
    font-size: var(--fs-xs);
    color: var(--muted);
    white-space: nowrap;
}
.airport-fact-closed { color: var(--red); font-weight: var(--fw-medium); }
/* Two quiet lines under a section, same voice: the openAIP credit (shown only
   when one of its rows is on screen — CC BY-NC) and the caveat that says what a
   section does NOT contain, which on radio navigation is the difference between
   "no ILS listed" and "no ILS". */
.airport-fact-credit, .airport-fact-caveat {
    margin-top: 8px;
    font-size: var(--fs-xs);
    color: var(--muted);
}
.airport-fact-credit a { color: inherit; }
/* THE LANDING AREA SEEN FROM ABOVE (audit D1, the sheet's mock-up): each
   runway at its true bearing, north up, the METAR's wind blowing in. */
.landing-sketch-row { display: flex; align-items: center; gap: 12px; margin: 4px 0 6px; }
.landing-sketch-row .airport-fact-caveat { margin-top: 0; }
.landing-sketch { flex: 0 0 auto; width: 96px; height: 96px; }
.landing-sketch-bar { fill: var(--map-runway); stroke: var(--heading); stroke-width: 1; }
.landing-sketch-wind { stroke: var(--wx-wind); stroke-width: 2.5; stroke-linecap: round; }
.landing-sketch-head { fill: var(--wx-wind); }
.landing-sketch-north { fill: var(--muted); font-size: var(--fs-2xs); }

/* A FIELD'S CHART, READ IN THE APP (owner's decision, planche VISIONNEUSE):
   full screen over whatever opened it, a page head (back · title · Night),
   the sheet's pages one under the other on their own paper. Night (VISIO C.1)
   inverts the paper with the pages, so the gaps go dark with them in either
   theme. The sheet is the pinch's width (--chart-zoom, chart-viewer.js). */
.chart-viewer {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    display: flex;
    flex-direction: column;
    background: var(--surface);
}
.chart-viewer-head { margin: 0; grid-template-columns: var(--round-btn-size) minmax(0, 1fr) var(--round-btn-size); }
.chart-viewer-head.has-keep { grid-template-columns: var(--round-btn-size) minmax(0, 1fr) var(--round-btn-size) var(--round-btn-size); }
/* A pressed disc — the sheet kept (✓), Night on — is drawn full, in the ink
   (planche CALAGE L.3, N.2). */
.round-icon-btn[data-chart-keep][aria-pressed="true"],
.round-icon-btn[data-chart-row-keep][aria-pressed="true"],
.round-icon-btn[data-chart-night][aria-pressed="true"] { background: var(--text); color: var(--bg); }
/* A CHART ROW (planche CALAGE L.1): the label, then three 36 px discs 8 px
   apart — document, Map, keep —, each with its 44 px contact zone; an empty
   slot keeps its place so the columns stay aligned. The link's own ::after
   stretches over the row, so the whole row opens the viewer; the keep button
   comes after it in the page, so it paints above. */
.chart-row .round-icon-btn svg,
.chart-viewer-head .round-icon-btn:not(.page-back) svg {
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.chart-row { --round-btn-size: 36px; position: relative; gap: 8px; }
.chart-row-open { display: flex; flex: 1; align-items: center; gap: 8px; min-width: 0; color: inherit; text-decoration: none; }
.chart-row-open::after { content: ""; position: absolute; inset: 0; }
.chart-row-open > .sheet-k { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.chart-row-slot { flex: none; width: var(--round-btn-size); }
.chart-viewer-head.has-map { grid-template-columns: var(--round-btn-size) minmax(0, 1fr) var(--round-btn-size) var(--round-btn-size); }
.chart-viewer-head.has-keep.has-map { grid-template-columns: var(--round-btn-size) minmax(0, 1fr) repeat(3, var(--round-btn-size)); }
.round-icon-btn[data-chart-map][aria-pressed="true"] { background: var(--text); color: var(--bg); }
/* A SHEET ON THE MAP (planche CALAGE H.1, G.6): a round cross under its
   bottom edge, set by chart-overlay.js, and a bare vertical slider on the
   left, a small map above it, opaque at the top. */
.round-icon-btn.chart-sheet-close {
    --round-btn-size: var(--map-disc);
    position: fixed;
    left: 0;
    top: 0;
    z-index: calc(var(--z-chrome) - 1);
    transform: translate(calc(var(--cross-x, -100px) - 50%), calc(var(--cross-y, -100px) - 50%));
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
    color: var(--text);
}
.chart-sheet-close svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.chart-opacity {
    position: fixed;
    left: calc(var(--free-left) + var(--map-chrome-x));
    top: 50%;
    z-index: calc(var(--z-chrome) - 1);
    display: flex;
    justify-content: flex-start;   /* the pill on the chrome's left line, the contact area to its right */
    width: var(--flight-tap);
    height: 40vh;
    transform: translateY(-50%);
    touch-action: none;
    cursor: pointer;
}
/* THE GLASS CAPSULE (planche CURSEUR C): 10 px painted, the whole contact
   area answering; filled from the bottom up to the value. */
.chart-opacity-pill {
    position: relative;
    width: 10px;
    height: 100%;
    border-radius: 5px;
    overflow: hidden;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim), 0 0 0 0.5px var(--line);
}
.chart-opacity-fill {
    position: absolute;
    inset: auto 0 0;
    height: calc(var(--chart-opacity, 1) * 100%);
    border-radius: 5px;
    background: var(--text);
    opacity: 0.85;
}
.chart-opacity input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    writing-mode: vertical-lr;
    direction: rtl;
    pointer-events: none;
}
body:not(.map-view-open) .chart-sheet-close,
body:not(.map-view-open) .chart-opacity { display: none; }
/* A SHEET PAST ITS CYCLE SAYS SO IN RED (planche GARDER E.1), under the head
   and off the paper, so Night never inverts it. */
.chart-viewer-expired {
    margin: 0;
    padding: 8px 16px;
    background: var(--red);
    color: var(--on-color-light);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.chart-viewer-head .page-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-viewer-pages {
    flex: 1;
    overflow: auto;
    overscroll-behavior: contain;
    touch-action: pan-x pan-y;
    background: var(--on-color-light);
}
.chart-viewer.is-night .chart-viewer-pages { filter: invert(1) hue-rotate(180deg); }
.chart-viewer-sheet { width: calc(var(--chart-zoom, 1) * 100%); }
.chart-viewer-sheet img { display: block; width: 100%; height: auto; }
.chart-viewer-sheet img + img { border-top: 1px solid var(--line); }
.chart-viewer-count {
    position: absolute;
    right: 14px;
    bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--glass-thick);
    color: var(--text);
    font-size: var(--fs-xs);
    pointer-events: none;
}
.chart-viewer-error { padding: 24px 16px; color: var(--text); font-size: var(--fs-md); }
.chart-viewer-error a { color: inherit; }
/* THE FIELD'S CODE LEADS ITS CARD (owner's call, docs/ui/grammaire.md, « La typographie ») :
   20/600 en tête, le nom du terrain à côté, 14/400 en encre secondaire. */
.airport-code-title {
    /* RANG 2 (docs/ui/grammaire.md, « La typographie ») : 20 px, 600, --heading. */
    font-size: var(--fs-xl);
    font-weight: var(--fw-semibold);
    color: var(--heading);
}
/* CE QUE CE TERRAIN EST POUR CE VOL (planche NOTAM vue 6, OPADD ed. 4.1 §7.7) :
   un LIBELLÉ, jamais une lecture — il nomme le terrain, il ne dit rien de la
   météo — donc l'écriture des libellés de la page (`.airport-fact-title`,
   `.notam-section-name`) : petite, espacée, en capitales, dans l'encre qui nomme
   une lecture, et à la graisse de l'échelle. Écrit en mode route seulement.
   UNE LIGNE À LUI, AU-DESSUS DU CODE (décision du propriétaire) : il COIFFE la
   carte — météo comme NOTAM — au lieu de se ranger dans la ligne qui nomme, où
   il partageait la place avec le nom du terrain, « raw » et la punaise. */
.airport-role {
    display: block;
    line-height: 1.3;
    font-size: var(--fs-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* LE RÔLE RECULE AVEC LES LIBELLÉS DU METAR (décision du propriétaire). */
    color: var(--quiet);
}
/* Airport-title identity dots track the ICAO code size, reduced 20% on desktop
   (see the 30% mobile override) and vertically centred with it (the title row
   already centres its items). */
.airport-title .status-dot { width: calc(var(--fs-xl) * 0.8); height: calc(var(--fs-xl) * 0.8); }
/* LE NOM DU TERRAIN RECULE AVEC LES LIBELLÉS DU METAR (décision du propriétaire) :
   le code ICAO l'identifie. */
.airport-name {
    font-size: var(--fs-md);
    color: var(--quiet);
    font-weight: var(--fw-regular);
    letter-spacing: 0;
    text-transform: none;
}
.twilight {
    font-family: var(--app-font);
    /* Une métadonnée (docs/ui/grammaire.md, « La typographie ») : sous le nom du terrain
       qu'elle suit, jamais aussi fort que lui. */
    font-size: var(--fs-xs);
    color: var(--muted);
    font-weight: var(--fw-regular);
    letter-spacing: 0;
    text-transform: none;
    white-space: nowrap;
}
/* Entry header: the title toggle FIRST, then the icon-only "Show on map" and
   the caret (its own toggle), both at the end of the row.
   THE LINE IS THE TARGET (owner's call): the title toggle used to stop where
   its title stopped — 291 x 29px painted, so a short title was a smaller
   target than a long one — and the caret's own box measured 8 x 0px, its glyph
   painting outside it. The toggle now takes the row's whole free width and its
   two neighbours are 44px squares around the same glyphs. */
/* LA CIBLE N'EST PAS LE DESSIN (décision du propriétaire, mesurée) : la tête
   d'une rangée était peinte sur 44 px pour un titre dont l'encre en fait 15, et
   ces 29 px de vide tombaient ENTRE le titre et son filet — 38 px mesurés de
   l'un à l'autre. La tête prend maintenant la hauteur de sa ligne, et les 44 px
   du doigt sont une zone invisible, comme sur la ligne du terrain.
   AUCUN RECOUVREMENT : entre deux cibles il reste le filet (14 px) et l'air de
   la rangée (16), soit 30 px, quand chaque zone ne déborde que de 12 de chaque
   côté. Deux zones qui se recouvrent se voleraient les touches. La punaise n'en
   fait plus partie : elle est descendue dans le pli, où « map » et « raw » sont
   deux mots sur une ligne et prennent la cible de leur famille. */
.notam-entry-toggle::after {
    content: "";
    position: absolute;
    /* 18 px de ligne + 13 de débordement de chaque côté = les 44 px du doigt.
       Les deux nombres se tiennent : --notam-line qui change demande celui-ci. */
    inset: -13px 0;
}
.notam-entry-toggle {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    flex: 1 1 auto;
    min-height: var(--notam-line);
    min-width: 0;
    padding: 0;
    background: none;
    border: 0;
    cursor: pointer;
    text-align: left;
    font: inherit;
}
/* Inline "Show on map": a bare pin glyph, no background or border, in the title
   row. IN THE ACTIONS' INK (owner's call): the heading ink is what the "raw"
   switch beside it turns when it is OPEN, so a pin in it at rest would wear the
   colour of an active state permanently. --muted is what every action at rest
   carries. */
.notam-map-btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    min-width: 44px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
}
/* "Add to route" inside the airport card, riding the title line after the name.
   A field carries one pill, so its card answers for everything that can be done
   with it: the weather below, the route action here. Pushed to the row's end so
   the ICAO and the name keep the eye's left edge. */
/* The map card hides its redundant "show on map" pin and reserves the space —
   with the action there instead, the row needs its full width back. */
/* The card head's "+ route": the chrome's own disc, at the head's end. */
.card-add-route { margin-left: auto; flex: none; }
.card-add-route svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

/* The layers disc: what it opens is a stack of layers, so it wears the layers
   glyph — hairline like the PDF disc it shares the header's reserved column
   with. State is the stroke's own fade, the way the map's stateful discs say
   theirs. */
/* Les quatre verbes de la carte — Weather, Navigation, recentrer, le sol — sont
   ce que la main touche EN VOL. Serrés à 8 px les uns des autres (décision du
   propriétaire, planche MAP 6) : chaque zone de contact déborde de 4 px au-delà
   du disque, soit 48 px, et deux zones voisines se touchent sans se chevaucher.
   La zone est CARRÉE (décision du propriétaire) : un doigt qui arrive en
   diagonale sur le bord du disque l'atteint encore, au lieu de passer à la carte. */
.wx-header-btn { padding: 0; position: relative; }
.wx-header-btn::after {
    content: "";
    position: absolute;
    /* 4 px de plus que le disque, comptés depuis son bord extérieur : le 1 px
       du contour s'ajoute, l'absolu partant de la boîte intérieure. Sur la
       carte, la zone est celle du vol (--map-pad-x : 48 px au sol, 56 en vol,
       planche VOL E.1). */
    inset: min(0px, var(--map-pad-x, -5px));
}
.wx-header-btn svg {
    width: var(--icon-box);
    height: var(--icon-box);
    flex: 0 0 auto;
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* PLUS AUCUN CHEVRON SUR UN NOTAM NI SUR UNE CATÉGORIE (décision du
   propriétaire) : toute la ligne du titre ouvre l'entrée, toute la ligne de tête
   ouvre la catégorie. Un carré de 44 px qui répète ce que la ligne fait déjà est
   une cible qui vole des touches à la seule qui compte. L'état ouvert se lit à
   la graisse du titre et à ce qui apparaît dessous. */
.notam-entry:not(.notam-expanded) .notam-detail { display: none; }
/* Q-subject subcategories: ordered, never ruled off (owner's call). The rule
   between two sub-groups drew a border around a grouping the pilot never asked
   for; the air between two entries says the same and says it everywhere. */
/* One NOTAM is a ROW of the grid: its rail above, its title under it — the same
   shape a TAF slice has, because "when does this hold" is the same question. */
/* RESSERRÉ (décision du propriétaire). La boîte du rail passe de 22 à 14 px :
   elle doit garder de quoi écrire les deux heures d'un filet coupé, pas 21 px de
   vide au-dessus d'un filet qui n'en écrit aucune — et c'est CE vide qu'on
   prenait pour l'air sous l'entrée précédente. 7 px entre le filet et le titre,
   10 px entre deux NOTAM, et le déplié collé à la ligne 2. */
/* UN FILET SÉPARE DEUX NOTAM, SUR TOUTE LA LARGEUR DE LA CARTE (décision du
   propriétaire, qui renverse la précédente : « aucun filet séparateur, c'est
   l'air qui sépare »). L'air seul tenait tant qu'une rangée faisait deux lignes
   hautes ; resserrée à un titre et sa barre, trois rangées se lisaient comme un
   seul pavé. Le trait d'un pixel qui disait le temps dit maintenant la
   séparation — les deux traits ont échangé leur rôle.
   TOUTE LA LARGEUR : la rangée ressort des 14 px de la liste et les rend en
   retrait, sinon le trait se lirait comme un trait DANS une rangée. Pas de
   filet après la dernière : un séparateur sépare deux choses. Et l'air tombe de
   16 à 12 px de chaque côté, puisque le trait sépare maintenant. */
/* PLUS AUCUN FILET ENTRE DEUX NOTAM (décision du propriétaire) : l'icône ouvre
   chaque rangée et le titre la nomme, donc l'œil a déjà où se raccrocher — un
   trait de plus par rangée faisait une grille là où il ne fallait qu'une liste.
   L'AIR SÉPARE, et il est le seul à séparer. */
.notam-entry {
    margin: 0 0 12px;
    padding: 0 var(--notam-bleed);
    margin-inline: calc(-1 * var(--notam-bleed));
    /* Le segment se place contre SA rangée, jamais contre ce qui l'entoure :
       sans repère à lui, il remonterait au corps de la section et se peindrait
       une largeur de débord trop à gauche. */
    position: relative;
}
.notam-list > .notam-entry:last-child,
.notam-section:last-child .notam-section-body > .notam-entry:last-child {
    margin-bottom: 0;
}
/* LE FILET EST UN DESSIN, PAS UNE CIBLE : il se tient sous la ligne du titre,
   donc il tombe en plein dans la zone de contact de la rangée — et comme il est
   positionné, il la couvrait. Un doigt posé sur le filet ouvre l'entrée, comme
   partout ailleurs sur la ligne. */
.notam-row-line > .rg-rail { height: 14px; margin: 0; pointer-events: none; }
/* Detail body starts where the row starts: the map icon that used to open the
   row — and whose width the inset paid for — now stands at its end. */
.notam-detail { margin-top: 9px; }
/* LA COLONNE DE GAUCHE EST PARTIE AVEC LA PUNAISE (décision du propriétaire,
   planche NOTAM vue 5 § 1). Elle réservait 24 px sur CHAQUE rangée pour un
   bouton qui ne se lève que sur les NOTAM qui ont une géométrie : vide une ligne
   sur deux, et le bord gauche de la liste en dents de scie. « map » se tient
   maintenant dans le pli, au bout de la ligne du numéro, à côté de « raw ».
   Le retrait que cette colonne donnait est repris par la bande de la section,
   qui tient le bord gauche de la liste entière plutôt qu'une réservation par
   rangée. --notam-col est parti avec elle : plus personne ne réserve de place à
   gauche d'une rangée de NOTAM, ni le titre, ni le filet, ni sa fenêtre. */
/* L'ICÔNE OUVRE UNE COLONNE, ET TOUT LA SUIT (décision du propriétaire, par
   conséquence) : le titre commence après elle, donc son filet doit commencer là
   aussi — un filet qui part 20 px à gauche de la première lettre ne se lit plus
   comme celui de CE titre. C'est la relation que la disparition de la gouttière
   avait établie (« le filet part sous la première lettre du titre ») et elle
   tient toujours, à un décalage près qui vaut pour toute la liste. */
/* LA GOUTTIÈRE DU MOT EST UNE MESURE DE LA RANGÉE, pas du rail : la durée s'y
   pose au bout du rail et « NEW » s'y aligne sur la ligne du titre. Déclarée
   UNE fois là où les deux la voient — le rail la lit par --rail-word, qui n'a
   plus de nombre à lui. */
/* LA COLONNE VAUT LA PASTILLE PLUS SA GOUTTIÈRE (22 + 8) : elle était de 20
   quand la marque était un trait de 14. Un seul nombre ici, et le rail comme
   le pli le suivent — c'est ce qui garde le titre, la barre du temps et tout
   le contenu sur le même x. */
.notam-entry { --notam-col-gap: 8px; --notam-line: 18px; --notam-icon-col: 42px; --row-word: 44px; }
/* UNE SEULE VERTICALE COMMANDE LA LISTE (décision du propriétaire) : le titre de
   section y commence et l'icône d'état de chaque rangée s'y pose, donc la tête
   et chaque rangée tombent sur le même x. Déclarée UNE fois, sur la liste —
   deux nombres, c'est deux verticales le jour où l'un bouge. Et la colonne du
   bouton map ferme la ligne du temps, réservée sur TOUTES les rangées — c'est
   ce qui garde une seule échelle de temps d'une ligne à l'autre. */
.notam-list { --notam-bleed: 14px; --notam-map-col: 28px; }
/* L'ICÔNE N'EST PAS DANS LE TITRE (décision du propriétaire) : elle ouvre une
   colonne à gauche, et TOUT ce que la rangée dit s'aligne sur la première
   lettre du titre — la barre du temps comme le pli. */
.notam-entry > .notam-detail {
    margin-left: var(--notam-icon-col);
}
/* LA BARRE DU TEMPS PART DU TITRE (décision du propriétaire, planche « la rangée
   NOTAM », forme B′). Elle commençait une largeur d'heure plus loin, parce que
   la fenêtre gardait de la place à gauche pour écrire une heure de DÉBUT ; mais
   seules les rangées qui n'ont pas encore commencé en écrivent une, et les
   autres payaient 32 px de barre pour rien.
   L'heure de début déborde donc À GAUCHE, sur la ligne du filet, dans la place
   que la colonne de l'icône y laisse vide — l'icône est une ligne plus haut.
   Elle garde son instant : elle finit là où la barre commence. Mesuré au pire
   cas (une rangée qui ouvre au bord même de la fenêtre) : l'heure ouvre 6,6 px
   après le filet du rang, et cette marge ne dépend pas de la largeur de l'écran,
   la barre partant toujours du même x. */
.notam-row-line {
    display: flex;
    align-items: center;
    margin-left: var(--notam-icon-col);
}
/* LA FENÊTRE PART DU BORD DE SA LIGNE : l'heure de début n'a plus de gouttière,
   elle déborde à gauche sous l'icône, qui est une ligne plus haut. Mesuré : 42 px
   de place devant elle pour une heure qui en demande 31 — elle tient, et le pire
   cas (un instant tombant sur le bord même de la fenêtre) garde 11 px d'air avant
   le filet du rang. */
.notam-row-line > .rg-rail { flex: 1 1 auto; min-width: 0; --rail-hour: 0px; }
.notam-map-col {
    flex: 0 0 var(--notam-map-col);
    width: var(--notam-map-col);
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
}
/* LE PIN EST PEINT À LA HAUTEUR DE SA LIGNE, PAS À CELLE DE SA CIBLE (décision
   du propriétaire, mesurée) : le rail fait 14 px, donc le bouton aussi. Le même
   pin peint à 20 px fait passer la ligne du temps de 14 à 20 sur CHAQUE rangée —
   le défaut que le bouton du titre d'un terrain avait déjà, et pour la même
   raison. La cible garde ses 44 px par la zone invisible ci-dessous : une zone
   de contact n'est pas un dessin.
   L'ICÔNE ET NON LE MOT : « map » écrit là se serait posé à côté de « + 44h56 »,
   deux mots gris de la même taille dont l'un est un FAIT et l'autre un VERBE. */
.notam-row-map {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
}
.notam-row-map > .notam-map-icon { width: 14px; height: 14px; }
/* La cible du doigt, invisible, et BORNÉE À SA LIGNE : la rangée entière est
   déjà une cible de 44 px qui ouvre le pli, et deux zones élargies qui se
   recouvrent se volent les touches. Elle déborde donc de sa colonne, où il n'y
   a rien, et pas sur la ligne du titre, où il y a le pli à ouvrir. */
.notam-row-map::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 34px;
    transform: translate(-50%, -50%);
}
.notam-entry-head { display: flex; align-items: flex-start; gap: var(--notam-col-gap); }
.notam-l1 { display: flex; align-items: baseline; gap: 8px; width: 100%; }
/* L'ICÔNE OUVRE LA RANGÉE, LE RANG LA SUIT (décision du propriétaire) : l'icône
   d'état se pose sur le bord du contenu, la verticale des titres de section, et
   le point de rang se loge dans les 20 px qui la séparent du titre. Réservés sur
   toutes les rangées, classées ou non : le titre garde un seul x. */
.notam-l1 > .notam-icon { margin-right: 12px; }
/* L'ICÔNE D'ÉTAT, DEVANT LE TITRE (décision du propriétaire). Elle porte ce que
   le rouge et le vert du texte portaient, et le pilote choisit laquelle pour
   chaque code d'état. Dans la boîte d'une lecture, alignée sur la première
   ligne du titre — jamais sur la ligne de base, qui la ferait flotter sous un
   titre qui passe à la ligne. Aucune place gardée quand il n'y a pas d'icône :
   c'est le rendu qui n'écrit rien, donc le titre part du bord. */
.notam-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 11px;
    align-self: flex-start;
    margin-top: -2px;
    /* LE FOND EST L'ENCRE DE LA MARQUE, DILUÉE : une seule couleur est déclarée
       par état (les règles .is-* ci-dessous), et la pastille la lit par
       currentColor. Deux déclarations par état — le trait et le fond — feraient
       deux choses à tenir d'accord au premier changement de teinte. */
    /* LE DISQUE D'ÉTAT EST TEINTÉ À 12 % (décision du propriétaire) : c'est la
       couleur des NOTAM sur la carte, et le disque fait le lien. L'ambre y tient
       5,6:1 de jour et 6,0:1 de nuit (§ 11.2). */
    background: color-mix(in srgb, currentColor 12%, transparent);
}
.notam-icon > .notam-mark {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.notam-icon.is-closed { color: var(--red); }
.notam-icon.is-hazard,
.notam-icon.is-caution { color: var(--notam-hazard); }
.notam-icon.is-restored { color: var(--notam-gives); }
.notam-icon.is-work,
.notam-icon.is-info { color: var(--muted); }
/* « aucune icône » garde la place et ne dessine rien : les titres d'une liste
   commencent tous au même x, règle déjà tranchée quand la gouttière du pin est
   partie. Un <i> vide, donc rien à peindre et rien à lire. */
.notam-icon.is-none { background: none; }
.wx-notam-icon-row .sheet-row { width: 100%; }
/* Le glyphe d'une marque dans la feuille de choix porte SON encre : on choisit
   une icône, et une rangée qui la montrerait en gris ferait choisir un mot. Les
   mêmes encres que la liste, déclarées une fois de plus et pas une de deux —
   c'est la même palette, lue par la même classe. */
/* L'ENCRE SE DÉCLARE EN `color`, PAS EN `stroke` : depuis que la marque porte
   une pastille, la même teinte sert deux fois — le trait et le fond dilué — et
   currentColor est ce qui les tient ensemble. */
.sheet-lead-icon .notam-mark { stroke: currentColor; }
.notam-mark-pad { fill: color-mix(in srgb, currentColor 12%, transparent); stroke: none; }
.sheet-lead-icon .notam-mark.is-closed { color: var(--red); }
.sheet-lead-icon .notam-mark.is-hazard,
.sheet-lead-icon .notam-mark.is-caution { color: var(--notam-hazard); }
.sheet-lead-icon .notam-mark.is-restored { color: var(--notam-gives); }
.sheet-lead-icon .notam-mark.is-work,
.sheet-lead-icon .notam-mark.is-info { color: var(--muted); }
/* LE TITRE EST EN GRAS ET PORTE LA CRITICITÉ (décision du propriétaire). L'encre
   arrive en ligne (notamInkAttr) : la couleur dépend du NOTAM et du rangement de
   la liste, pas d'une classe. `color` reste déclaré ici pour le cas sans encre —
   ni critique, ni important — qui est le plus fréquent. */
/* SANS GRAS REPLIÉ, EN GRAS DÉPLIÉ (décision du propriétaire) : plié, un NOTAM
   est une ligne d'une liste qu'on parcourt, et vingt titres gras à la suite ne
   hiérarchisent plus rien. Ouvert, il devient le titre de ce qu'on lit, et le
   gras dit où ce qu'on lit commence. Le poids reste donc un canal — mais il dit
   « voici ce que tu as ouvert » au lieu de le dire de tout le monde à la fois. */
.notam-title-main { flex: 0 1 auto; min-width: 0; color: var(--text); font-size: var(--fs-md); font-weight: var(--fw-regular); }
.notam-expanded .notam-title-main { font-weight: var(--fw-semibold); }
/* NEUF POUR MOI = LE POIDS DU TITRE (décision du propriétaire, planche NOTAM
   vue 5). C'est la convention du courrier non lu : elle ne s'apprend pas, et le
   poids est le seul canal que rien d'autre n'utilise sur cette ligne — la place
   dit le rang, la couleur dit le sens, la frise dit le temps. */
/* LE POIDS NE DIT QU'UNE CHOSE : ce que le pilote a ouvert (décision du
   propriétaire). Trois règles de gras se disputaient la ligne — le sujet, le
   « neuf pour moi », et le pli — et trois canaux sur un seul attribut n'en
   laissent lire aucun. Il ne reste que le pli. Le « neuf » garde son mot au
   bout de la ligne, qui le dit sans peser. */
/* SORT — L'ESCALIER NUMÉROTÉ (décision du propriétaire, format S1 sur quatre).
   Chaque cran est une cible de 44 px : son RANG écrit, une poignée pour tirer,
   le nom, la croix de l'app pour retirer. La rangée du RECOURS n'a ni rang, ni
   poignée, ni croix — « Newest published first » est le fond d'une échelle, pas
   une marche qu'on arrange. Ce qui reste à prendre suit dessous, en lignes
   système, le « + » à la place qu'un numéro occupera. */
.wx-sort-body { align-items: stretch; }
.notam-sort-ladder { padding: 4px 0; }
.notam-sort-row {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 0 4px;
    border-top: 1px solid var(--soft-line);
}
.notam-sort-ladder > .notam-sort-row:first-child { border-top: 0; }
/* LE RANG S'ÉCRIT (décision du propriétaire, format S1) : la colonne de gauche
   dit à quel cran on en est, et « pas encore de cran » y est le « + » de l'app.
   Sa largeur EST celle du « + » — c'est ce qui fait que les deux tombent au
   même endroit, et c'est à cette condition seulement que la colonne dit le rang
   plutôt que de porter deux signes qui se ressemblent de loin. */
.notam-sort-rank {
    flex: none;
    /* The column of the "+" beside it, a chrome icon's box (§ 8.13). */
    width: var(--icon-box);
    text-align: center;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}
/* LA POIGNÉE DU TRI EST CELLE DES CARTES DE MINIMA (décision du propriétaire).
   Elle était un SEMIS DE POINTS peint en fond — un motif qui n'existait nulle
   part ailleurs dans l'app, donc un dessin à apprendre pour un geste déjà connu.
   Le moteur, lui, était déjà le bon : c'est bindCardReorder des deux côtés.
   Une seule règle la redresse ici : la rangée du tri fait 44 px de haut. */
.notam-sort-row > .config-grip { padding-left: 4px; min-width: 26px; }
/* LE RECOURS N'EST PAS UN CRAN, donc il n'en a ni le numéro ni la poignée — et
   pas d'aplat non plus : l'aplat en faisait un objet à part, une boîte dans la
   boîte, quand c'est une rangée de la même liste, simplement atténuée. Sans
   poignée, son mot commence plus à gauche que les autres, et c'est juste : il
   n'occupe pas une marche. */
.notam-sort-row.is-source .notam-sort-name { color: var(--muted); }
.notam-sort-name { flex: 1 1 auto; min-width: 0; color: var(--text); }
/* La note du recours est une VALEUR de rangée : à droite, en atténué, comme
   toute page de réglages de l'app se lit. */
.notam-sort-note {
    flex: 0 1 auto;
    margin-left: auto;
    padding-left: 12px;
    color: var(--muted);
    font-size: var(--fs-xs);
    text-align: right;
}
/* RETIRER UN CRAN PORTE LA CROIX DE L'APP, le jumeau du chevron — le « × » du
   clavier était un dessin de plus pour un geste que l'app nomme déjà. */
.notam-sort-drop {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    padding: 0;
    background: none;
    border: 0;
    cursor: pointer;
}
/* Ce qui n'est pas dans l'échelle attend dessous, en lignes système : même
   gouttière de gauche que les crans, pour que le « + » tombe sur le numéro. */
.notam-sort-rest { padding: 0 4px; }
/* CE QUE LE 1er TRI TITRE, ET CE QUE LE 2e OUVRE (décision du propriétaire).
   Le titre de section a la forme qu'avaient les en-têtes de catégorie — petites
   capitales espacées, du texte et pas une boîte — et l'encre du rang quand la
   clé EST la criticité. Aucune couleur inventée : celle que le pilote a punaisée
   est la seule qu'un rang ait jamais portée. */
/* UNE SECTION DE NOTAM (décision du propriétaire, planche NOTAM vue 1). Le RANG
   de l'émetteur se dit par la PLACE — la section où l'on tombe, l'ordre, le pli —
   et par rien d'autre : la couleur est prise, elle dit le sens du changement et
   le dit sur toutes les surfaces. Le titre de section n'était qu'un mot sur une
   ligne ; il lui manquait tout ce qui fait une section : un compte, un pli, et un
   cadre qui dise où elle commence et où elle finit. */
/* La bande ne RENTRE pas la liste : elle se superpose au bord gauche. Un filet
   entre deux NOTAM doit courir sur toute la largeur de la carte — rentré, il se
   lit comme un trait DANS une rangée et non entre deux. Seule la tête s'écarte
   d'elle, parce que ses lettres, elles, la toucheraient. */
.notam-section { position: relative; }
/* PAS DE FILET ENTRE DEUX SECTIONS (décision du propriétaire) : le titre, en
   petites capitales espacées avec son air au-dessus, dit déjà qu'une section
   commence — et le trait coupait en travers la colonne que les filets verticaux
   des rangées tiennent d'un bout à l'autre de la carte. */
/* LE RANG DU NOTAM EST UN POINT (décision du propriétaire, planche REPORT P3) :
   plus aucune verticale — ni le filet qui courait le long de la liste, ni le
   trait par NOTAM, qui surchargeaient la colonne. Le rang que l'émetteur écrit
   sur la ligne Q se dit par un point de 7 px APRÈS l'icône (décision du
   propriétaire : l'icône ouvre la rangée), centré dans l'air entre l'icône de
   22 px et le titre, sur la hauteur de l'icône ; à la dose du rang mélangée
   depuis --text vers --card (la mécanique de --bar) ; un NOTAM sans rang n'en
   porte pas. */
.notam-entry.is-ranked::before {
    content: "";
    position: absolute;
    left: calc(var(--notam-bleed) + 22px + (var(--notam-icon-col) - 22px - 7px) / 2);
    top: 6px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--text) var(--band-dose, 100%), var(--card));
}
/* LA TÊTE EST UN BANDEAU, PAS UNE RANGÉE : le fond le dit avant que les lettres
   soient lues, et c'est ce qui manquait quand la tête ne se distinguait d'un
   titre de NOTAM que par une graisse. Les capitales espacées sont déjà la voix
   des titres de section de l'app ; en encre de titre et non en gris, parce que
   ces cinq mots-là sont la structure de la liste et non une note en marge. */
.notam-section-head {
    display: flex;
    align-items: center;
    gap: 8px;
    /* ELLE RESSORT DES 14 px DE LA LISTE, comme une rangée. Ce débord n'est plus
       là pour peindre — la tête ne peint rien — mais pour que TOUTE la largeur
       de la ligne soit tappable : c'est la décision « pas de chevron, un tap
       n'importe où sur la ligne ». Le retrait est rendu en padding, donc les
       lettres ne bougent pas d'un pixel. */
    width: auto;
    margin-inline: calc(-1 * var(--notam-bleed));
    min-height: 30px;
    /* LE TITRE PART DU BORD DU CONTENU DE LA LISTE (décision du propriétaire) :
       une seule verticale commande la liste, et elle est déclarée une fois. Dix
       pixels d'air en dessous : un titre appartient à ce qui le suit, encore
       faut-il qu'on voie où il s'arrête. */
    padding: 6px 24px 10px var(--notam-bleed);
    /* AUCUN APLAT SOUS LE TITRE (décision du propriétaire, planche A3) : la tête
       portait un fond qui débordait jusqu'aux bords de la carte — un bandeau,
       donc — et c'est précisément celui qui ne se voyait pas. Le titre ressort
       par sa TAILLE. Un fond sous un titre de 16 px ferait deux effets pour une
       seule idée, et le point de rang dit déjà l'importance par son encre. */
    background: none;
    border: 0;
    color: var(--heading);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
/* LA VOIX DE LA CARTE, UN TON EN DESSOUS (décision du propriétaire, planche
   « le titre de section », forme T1). Il ressortait par sa taille — 16 px en
   700 — et se retrouvait plus gros ET plus gras que « NOTAM », la tête de la
   carte qui le contient : le deuxième niveau criait plus fort que le premier.
   Il prend donc une graphie de tête (12 px, 600, capitales à 0,14em) dans
   l'encre qui NOMME au lieu de celle qui LIT. Même alphabet, un ton en dessous.
   C'est aussi le seul canal qui reste libre : sur la ligne d'un NOTAM le gras
   dit déjà ce que le pilote a OUVERT, donc un titre de section à 13 px en 600
   se serait lu exactement comme un message déplié. */
.notam-section-name {
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}
/* LE COMPTE REDEVIENT UNE VALEUR (décision du propriétaire). La pastille pleine
   répondait à un titre de 16 px, où un nombre de la même famille se lisait comme
   la fin du titre ; sous un libellé de 12 en capitales, elle pesait plus que ce
   qu'elle compte. Nu et gris, il est ce que toute valeur de l'app est : à droite
   de son nom, dans l'encre qui nomme. Chiffres tabulaires : une section qui
   passe de 9 à 10 ne doit pas déplacer ce qui suit. */
.notam-section-count {
    color: var(--muted);
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
}
/* Repliée, la section n'a rien à griser de plus : son titre et son compte sont
   déjà tous les deux dans l'encre qui nomme. Ce qui dit l'état, c'est ce que la
   section montre — ses rangées — et `aria-expanded` pour qui ne voit pas. */
.notam-section-body:not(:empty) { padding-bottom: 4px; }
/* Sans tête (une clé de tri du pilote qui découpe la liste en plusieurs mots),
   le titre reste ce qu'il était : un mot sur une ligne. */
.notam-section-title {
    display: flex;
    align-items: center;
    min-height: 30px;
    margin-top: 8px;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.notam-list > .notam-section-title:first-child { margin-top: 0; }
/* LE NUMÉRO FERME LE PLI (décision du propriétaire) : on le relève APRÈS avoir
   lu, pas avant, et la ligne du titre est rendue au titre et à « New ». Le
   « raw » de CE NOTAM se tient au bout de cette ligne — la même place que sur la
   rangée du titre d'un terrain : au bout de la ligne qui nomme ce qu'il déplie. */
.notam-num {
    display: flex;
    /* Les deux boutons sont des surfaces, plus des mots : ils s'alignent sur le
       numéro par leur milieu, pas par la ligne de base de leur texte. */
    align-items: center;
    gap: 10px;
    margin-top: 7px;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
}
/* LES DEUX BOUTONS DU PLI PORTENT UN CONTOUR (décision du propriétaire, planche
   C2), et EUX SEULS : le « raw » de la ligne d'un terrain reste un mot nu — sur
   cette rangée-là, qui est une ligne de type, une surface serait la seule, et
   c'est une décision déjà prise (planche A2). La même classe sert les deux
   places ; c'est donc la RÈGLE qui distingue, pas le balisage.
   UNE SEULE MARGE AUTOMATIQUE : les deux boutons la déclarent, et deux marges
   `auto` dans une même rangée se PARTAGENT l'espace libre — « map » se serait
   posé au milieu de la ligne, loin du « raw » qu'il doit toucher. La première
   pousse la paire à droite, la seconde est annulée. */
.notam-num > .rg-raw-btn {
    color: var(--heading);
    border: 1px solid var(--contour);
    border-radius: 7px;
    padding: 4px 9px;
    background: var(--card);
}
.notam-detail > .rg-raw { margin-top: 7px; }
/* LES DEUX LIGNES DE FICHE DU PLI (décision du propriétaire) : « Schedule |
   Validité », puis « Catégorie | IFR/VFR | limites ». Une case vide se FERME —
   le fabricant ne pose aucune barre orpheline — donc la feuille n'a qu'à
   habiller la barre, jamais à deviner quels items un NOTAM porte. Chiffres
   tabulaires : deux validités l'une sous l'autre se comparent à la colonne. */
.notam-meta { color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.notam-meta + .notam-meta { margin-top: 3px; }
.notam-meta-sep { color: var(--icon); padding: 0 5px; }
/* THE OBJECT CARRIES THE WEIGHT (owner's call): "Taxiway B closed" — one step
   above its line, so the word the eye hunts for is found without reading the
   sentence. One step and not two: open, the whole title is already semibold and
   the object stops standing apart, which is right — an open entry is being read,
   not scanned. */
/* L'OBJET PORTE LA GRAISSE, un cran au-dessus de sa ligne — et sa ligne est
   maintenant en 600 partout, pliée comme dépliée, donc le cran est le 700. */
/* Le sujet ne pèse plus non plus : c'est la même règle. */
.notam-title-object { font-weight: inherit; }


/* NO PASTILLE ON A NOTAM (owner's call): criticality is worn by the filet,
   which already takes its ink, and named in words by the section head. A dot
   in front of the title said the same thing a third time, indented the titles
   that had one, and drew nothing at all for the pilot who never pinned a
   Q-subject — which is everyone by default. */
/* Validity, Q-line, F/G limits and description all sit flush at the entry's left
   edge (under the title) — no extra inset. */
.notam-text {
    font-size: var(--fs-md);
    line-height: 1.42;
    overflow-wrap: anywhere;
    /* Decoded view keeps the NOTAM's deliberate line breaks (soft telex
       wraps are rejoined by reflowNotamText before rendering). */
    white-space: pre-line;
}
/* Raw view: the same category cards / caret / badges as the decoded view; each
   entry's detail block is the NOTAM's own wording, keeping the breaks it means
   (soft telex wraps are rejoined by reflowNotamText, as in the decoded view). */
.notam-raw-body {
    font-size: var(--fs-md);
    line-height: 1.42;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

/* Pin glyph inside every icon-only map opener. */
.notam-map-icon { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; }
/* Icon-only map opener on the airport-card title: the bare pin
   (.notam-map-btn-icon) pushed to the right end of the line.
   IL EST PEINT À LA HAUTEUR DE SA LIGNE, PAS À CELLE DE SA CIBLE (décision du
   propriétaire, mesurée) : sa boîte de 44 px imposait sa hauteur à toute la
   rangée — 44 px peints pour un code qui en fait 22,4 — d'où 12 px d'air
   au-dessus du code, 9,7 en dessous, et 18,7 px de vide entre lui et la carte
   météo une fois la marge de la rangée ajoutée. La cible garde ses 44 px par la
   zone invisible que le « raw » à côté de lui porte déjà : une zone de contact
   n'est pas un dessin. */
.section-map-btn {
    position: relative;
    margin-left: auto;
    width: 20px;
    min-width: 20px;
    height: 20px;
}
.section-map-btn::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
/* The radar's dish is drawn in strokes (planche RLIGNE L.12). */
.radar-line > svg {
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* CE QUI EST NOUVEAU SUR LE TERRAIN, sur la tête de section, en mots et en
   encre (décision du propriétaire) : « 2 new · 1 changed ». Même taille que les
   rangs à côté, mais l'encre ordinaire — c'est la seule question qu'on pose à un
   briefing déjà lu une fois, et elle mérite de répondre avant la liste. Aucune
   couleur : ici une couleur est un verdict contre un minimum. */
.rg-notam-news {
    color: var(--text);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    font-variant-numeric: tabular-nums;
}
/* LE MOT D'ÉTAT, SEULE COULEUR QU'UN NOTAM PORTE (décision du propriétaire,
   planche NOTAM vue 2). La couleur dit UN fait dans cette app, et le même
   partout : quelque chose t'est RETIRÉ, ou t'est RENDU. C'est ce qui réconcilie
   le rouge d'un minimum rompu et le rouge d'une piste fermée — la même phrase,
   contre toi. Le RANG de l'émetteur se lit à la place, section et pli, jamais à
   une encre : deux échelles de rouge sur une carte, c'est le défaut qu'on ne
   reproduit pas.

   Le mot neutre n'a pas d'encre à lui : il hérite du titre. 47 % du flux réel ne
   dit ni l'un ni l'autre, et une troisième couleur les rendrait toutes muettes.

   --red porte une valeur par thème. --green n'en a qu'une, sous le seuil d'un
   TEXTE sur le blanc (3,30). D'où --notam-gives, une valeur par thème à la cible
   des encres d'état (docs/ui/report.md, « Les neuf encres d'état ») : teinte de --green partout,
   seule la clarté bouge. */
/* ET LE MOT D'ÉTAT N'EN PORTE PLUS AUCUNE (décision du propriétaire) : il garde
   l'encre du titre, l'icône devant la rangée disant ce que la teinte disait.
   --notam-gives et --red restent, lus par l'icône et par la forme de la carte —
   c'est le TEXTE qui ne porte plus de couleur, pas l'app. */

/* Et sur la ligne : JUSTE APRÈS LE TITRE (décision du propriétaire) — l'œil lit
   le sujet, puis apprend aussitôt si celui-ci est neuf pour lui. LE MOT SANS SA
   BOÎTE : le filet était le seul rectangle d'une liste qui n'en a aucun autre, et
   c'est lui qui accrochait l'œil, pas le mot. Jamais un point coloré — la
   pastille a quitté le NOTAM pour de bon, et un point à côté d'un titre dit
   « quelque chose à propos de cette ligne » sans dire quoi. */
/* « NEW » PREND LA COLONNE DE LA DURÉE (décision du propriétaire) : la durée se
   tient au bout du rail, dans la gouttière que le rail lui réserve
   (--rail-word), et le mot se posait juste à côté sans s'aligner sur elle. Même
   largeur, aligné à gauche : les deux forment une colonne au lieu de deux bouts
   de ligne qui se ressemblent. */
/* L'ENCRE DU TITRE, ET RIEN DE PLUS (décision du propriétaire, planche B4) :
   dans sa colonne, « New » est le seul mot en encre pleine à côté d'une durée
   grise, et un mot noir contre un mot gris se voit à 11 px sans aide. PAS DE
   GRAS AJOUTÉ : le gras ne dit qu'une chose sur cette liste, ce que le pilote a
   ouvert, et « neuf » est fréquent quand le pli est rare — la marque forte irait
   sur l'état commun. */
.notam-news {
    flex: 0 0 auto;
    margin-left: auto;
    min-width: var(--row-word, 0px);
    text-align: left;
    color: var(--text);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
}
/* ----------------------------------------------------------------------------
   LA PAGE EN DEUX ZONES, ET CE QU'UNE CARTE NOTAM PORTE SUR SA LIGNE DE TITRE
   ---------------------------------------------------------------------------- */
/* LE TITRE DE ZONE COLLE PENDANT QU'ON TRAVERSE SA RÉGION (décision du
   propriétaire, forme R4) : savoir où l'on est était le seul vrai argument du
   commutateur, et il s'obtient sans rien cacher. CHAQUE ZONE EST SON PROPRE
   BLOC — deux collants dans un même conteneur se superposent au lieu de se
   chasser, mesuré. Le fond est celui de la page : la carte qui passe dessous
   ne doit pas se lire à travers le mot. */
.briefing-zone { position: relative; }
/* ENTRE DEUX SECTIONS, LA RUPTURE LA PLUS FORTE (§ 5.3) : 30 px d'air avant
   la section suivante, et le surligneur du titre de section (plus bas). */
.briefing-zone + .briefing-zone { margin-top: 30px; }
/* LE TITRE NE COLLE QUE REPLIÉ (décision du propriétaire, planche COLLANT) :
   le sommaire de l'en-tête dit déjà dans quel bloc on lit, donc un bloc ouvert
   laisse partir son titre et ne colle que sa ligne de lecture — la légende des
   minima sur la météo, l'étendue et le filtre sur les NOTAM, rien sur la
   navigation. Replié (Summary), le titre colle : il nomme le résumé qu'il garde.
   Sous l'en-tête tel qu'il est MESURÉ (layoutTafBands), sommaire compris. */
.briefing-zone-head {
    position: relative;
    background: transparent;
    isolation: isolate;
    --head-reach: var(--col-pad);
}
.briefing-zone.is-shut .briefing-zone-head,
.briefing-zone:not(.is-shut) .briefing-zone-sub:has(> :not([hidden])) {
    position: sticky;
    top: var(--page-header-h, var(--sticky-head, 51px));
    z-index: var(--z-sticky);
}
.briefing-zone-sub {
    display: flex;
    align-items: center;
    gap: 10px;
    isolation: isolate;
    --head-reach: var(--col-pad);
}
/* LE BOUTON DU BLOC SE TIENT AU BOUT DU TITRE ET COLLE AVEC SA LIGNE (décision
   du propriétaire, planche RÉGLAGES-LIGNE R.9) : un rail sans hauteur, premier
   enfant du bloc, collé au seuil de la ligne et au-dessus d'elle. Au repos, le
   disque est centré sur la ligne de titre (4 px + 44 px) ; collé, il part du
   haut de la ligne (le rail remonte de sa marge) et déborde dans son fondu :
   la ligne reste compacte (décision du propriétaire) et lui garde son bout. */
.zone-tool {
    position: sticky;
    top: calc(var(--page-header-h, var(--sticky-head, 51px)) - 6px);
    z-index: calc(var(--z-sticky) + 1);
    height: 0;
    display: flex;
    justify-content: flex-end;
}
.zone-tool > * { margin-top: 6px; }
.briefing-zone.has-tool .briefing-zone-sub { padding-right: 50px; }
.briefing-zone-title {
    /* LE FOND ET LE COLLANT SONT CEUX DE LA TÊTE, PAS LES SIENS : hérités de
       `.config-section-title`, ils posaient une BARRE BLANCHE (`--surface`) sur
       le fond de la page, et un second collant dans le même conteneur. */
    background: none;
    position: static;
    /* La largeur de la ligne est ce que le fondu du surligneur mesure (cqw). */
    container-type: inline-size;
    /* AUCUN RETRAIT LATÉRAL : le nom du bloc part du même x que le code d'une
       carte, et le commutateur de fin de ligne finit au même bord que la punaise
       d'une carte. Le titre était le seul élément de l'écran hors du fil à plomb.
       LA LIGNE PLIE SON BLOC (planche PLI) : le pli prend toute la place que le
       commutateur de fin de ligne ne prend pas, et la ligne tient le plancher de
       44 px — c'est une cible, pas un filet de texte. */
    padding: 4px 0 2px;
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    /* RANG 1 DE L'ÉCHELLE (docs/ui/grammaire.md, « La typographie ») : le contenant domine son
       contenu — 24 px contre les 20 du code ICAO, en 700, dans l'encre des
       titres, sans capitales forcées. */
    font-size: var(--fs-2xl);
    font-weight: var(--fw-black);
    letter-spacing: normal;
    text-transform: none;
    color: var(--heading);
}
/* LE SURLIGNEUR (décision du propriétaire, planche SURLIGNEUR F2) : une bande
   à 12 % de l'encre des titres sur la moitié basse du nom, puis la même bande
   qui s'efface jusqu'au bout de la ligne. Aucune longueur écrite : le fondu part
   du bout du mot et prend ce qui reste de la ligne (100cqw − le mot). Il passe
   sous le chevron et sous le bouton de fin de ligne : positionnés eux aussi et
   plus loin dans la page, ils se peignent par-dessus, sans z-index. */
.briefing-zone-title .zone-name,
.card-section-title .zone-name {
    position: relative;
    background: linear-gradient(transparent 47%, rgba(var(--heading-rgb), 0.12) 47% 91%, transparent 91%);
}
.briefing-zone-title .zone-chev { position: relative; }
.briefing-zone-title .zone-name::after,
.card-section-title .zone-name::after {
    content: "";
    position: absolute;
    left: 100%;
    top: 47%;
    bottom: 9%;
    width: calc(100cqw - 100%);
    background: linear-gradient(90deg, rgba(var(--heading-rgb), 0.12), rgba(var(--heading-rgb), 0));
    pointer-events: none;
}
.zone-fold {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    align-self: stretch;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    cursor: pointer;
}
/* Le pli dessine 38 px de haut dans une tête de 44 : 3 px de débord de chaque
   côté lui donnent la tête entière, sans toucher le commutateur de fin de ligne. */
.zone-fold { position: relative; }
.zone-fold::after {
    content: "";
    position: absolute;
    inset: -3px 0;
}
/* LE CHEVRON SUIT LE NOM, ET IL NE SE VISE PAS (décision du propriétaire) :
   devant le nom, mesuré, il poussait le titre 29 px hors du fil à plomb de la
   page — le seul élément indenté de l'écran. C'est un TÉMOIN d'état ; la cible
   est la ligne, comme sur une tête de section NOTAM. */
.zone-chev {
    width: 15px;
    height: 15px;
    flex: none;
    fill: none;
    stroke: var(--muted);
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform 120ms ease;
}
.is-shut .zone-chev { transform: rotate(-90deg); }
/* CE QUE LE TERRAIN A : les marques et leur compte, ce qui est neuf, et ce que
   le briefing ne montre pas (sa place : plus bas, sous le code). */
.notam-run {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px 9px;
    min-width: 0;
    font-size: var(--fs-xs);
    color: var(--muted);
}
/* CE QUE LE TERRAIN A, JUSTE SOUS SON CODE (décision du propriétaire) : le
   compte des états et « filtered », alignés à gauche sur le code, presque collés
   à lui : 2 px d'air (décision du propriétaire). L'air vient après, avant la
   liste. */
.airport-card.notam-card:has(> .rg-blk > .notam-run) > .airport-title { margin-bottom: 0; }
.airport-card.notam-card:has(> .rg-blk > .notam-run) > .rg-blk { margin-top: 0; }
.rg-blk > .notam-run { justify-content: flex-start; margin: 2px 0 10px; padding: 0; }
.notam-run-mark { display: inline-flex; align-items: center; gap: 3px; }
.notam-run-mark .notam-icon { width: 18px; height: 18px; border-radius: 9px; margin-top: 0; align-self: center; }
.notam-run-mark .notam-icon > .notam-mark { width: 11px; height: 11px; }
.notam-run-mark b { font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); color: var(--text); }
/* LE COMPTE DE CE QUI N'EST PAS MONTRÉ MÈNE À LA SECTION QUI LE PORTE : c'est
   l'idiome de la page — de la navigation, jamais un mode. Mot nu, l'encre de
   toute action au repos ; la cible touchable est rendue par le pseudo-élément,
   pour que le dessin reste une ligne de type. */
.notam-run-hidden {
    position: relative;
    padding: 0;
    background: none;
    border: 0;
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-xs);
    cursor: pointer;
}
.notam-run-hidden::after { content: ""; position: absolute; inset: -12px -8px; }
@media (hover: hover) {
    .notam-run-hidden:hover { background: var(--hover-bg); }
}
/* NOTAM card: the Report's, and the map's sheet wears the same one
   (notamMapCardHtml) — the state icon, the title, the validity rail, the fold. */
.notam-card { padding: 16px 0; }
/* Dans le Report, la carte NOTAM d'un terrain suit le rythme des terrains : sous
   elle, l'air est celui du filet (§ 5.3), pas un fond de carte en plus (§ 11.7). */
.airport-card.notam-card { padding-bottom: 0; }
/* Sur la Map, la tête de la feuille porte le titre : le bloc des rangées ouvre la
   carte, et comme il rogne son contenu (.rg-blk), il coupait le haut de la zone
   de la première rangée (.notam-entry-toggle::after, 13 px). Il reprend ces 13 px
   en dedans et les rend au-dessus : la rangée reste à sa place, sa zone entière. */
.notam-card > .rg-blk:first-child { margin-top: -5px; padding-top: 13px; }
/* Radar transport controls (play/pause, frame scrubber, frame time) — hosted
   by the unified map's floating .map-radar-bar. The play button sits ON the bar
   track, so it stays flat (no fill, no shadow of its own — the bar carries the
   Claude shadow) and fits inside the bar's pill height. It is lifted OUT of the
   row's flow and laid over the ruler's left end (the ruler runs the full width
   of the bar underneath it), so the hours keep scrolling under the button
   instead of stopping beside it — dimmed there by the ruler's own fade. */
/* The map's own two buttons — recentre and the ground — holding the two ends of
   the capsule's bottom storey, so that swapping the reports for the clock
   between them never moves either one. Flat: the capsule around them carries
   the ground, and a disc inside a disc reads as two controls. In the map
   column's resting ink rather than the transport's full text ink, because they
   move the MAP and not the clock. */
.radar-side-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Never squeezed: it holds an end of the row, and the middle is what gives
       way. The browser recipe caught it at 23px of a 30px button. */
    flex: none;
    width: var(--control-h);
    height: var(--control-h);
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
}
@media (hover: hover) {
    .radar-side-btn:hover { background: var(--hover-bg); }
}
/* Stroked, never filled: these are drawn glyphs, and a filled path is a blot. */
.radar-side-btn svg {
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* ONE HOUR AT A TIME (planche 20): two arrows and the hour between them, the
   capsule's whole transport on the map and on the cross-section. The ruler it
   replaced put twelve hours under a finger that covers three, so the hour you
   landed on was the hour the finger allowed. */
.hour-stepper {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 2px;
    width: 100%;
    /* The MIDDLE is what gives way, never the discs holding the row's ends. */
    min-width: 0;
}
/* Le pas horaire se lit sur la carte, donc en vol. La flèche garde sa taille ;
   sa zone de contact fait 44 px. Elle déborde vers l'intérieur sur
   .hour-stepper-value, qui est un afficheur et non une cible : rien ne s'y perd. */
.hour-step {
    position: relative;
    width: var(--control-h);
    height: var(--control-h);
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text);
    cursor: pointer;
}
.hour-step::after {
    content: "";
    position: absolute;
    inset: -7px;
    border-radius: 999px;
}
.hour-step svg {
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
@media (hover: hover) {
    .hour-step:hover:not(:disabled) { background: var(--hover-bg); }
}

/* The hour itself, in the measured font so the digits never dance as it walks.
   A press on it opens the list of every hour (openMiniWheel). */
/* What qualifies the hour — the radar's "+20 min est." — in the quiet ink. */
.hour-stepper-mark { color: var(--muted); font-size: var(--fs-sm); }
/* The play disc, left of the arrows: the same disc as an arrow; pressed, it
   shows the pause. */
.hour-play .hour-play-off { display: none; }
.hour-play[aria-pressed="true"] .hour-play-on { display: none; }
.hour-play[aria-pressed="true"] .hour-play-off { display: inline; }
.hour-stepper-value {
    min-width: 0;
    /* « 1,000 ft » and « 13:00 LT » never break (planche METEO-RAIL R.7). */
    white-space: nowrap;
    padding: 0 6px;
    text-align: center;
    font-family: var(--app-font);
    font-size: var(--fs-md);
    font-variant-numeric: tabular-nums;
    color: var(--text);
    user-select: none;
    -webkit-user-select: none;
    cursor: pointer;
}
/* The rung of the route (planche ROUTEALT.6): « Route » over the height, so
   the line keeps the width a bare height takes. */
.hour-stepper-value.is-route {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    line-height: 1.05;
}
.map-alt-route {
    font-size: var(--fs-xs);
    color: var(--muted);
}
/* THE LIST OF EVERY CHOICE (owner's decision): every value at once over the
   value — rungs stacked, hours in a row — the one in force on the recess. What
   does not fit spans the screen and scrolls (openMiniWheel sets the room). */
.mini-wheel {
    position: fixed;
    z-index: var(--z-sheet);
    border: 1px solid var(--contour);
    border-radius: var(--radius-lg);
    background: var(--surface);
    overflow: hidden;
}
.mini-wheel-list {
    display: flex;
    overflow: auto;
    scrollbar-width: none;
    overscroll-behavior: contain;
}
.mini-wheel-list::-webkit-scrollbar { display: none; }
.mini-wheel.is-y .mini-wheel-list { flex-direction: column; width: 104px; }
.mini-wheel.is-x .mini-wheel-list { flex-direction: row; }
.mini-wheel-item {
    flex: none;
    display: grid;
    place-items: center;
    padding: 0 10px;
    border: 0;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-md);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    cursor: pointer;
}
.mini-wheel.is-y .mini-wheel-item { height: 32px; }
.mini-wheel.is-x .mini-wheel-item { min-width: 56px; height: 36px; }
.mini-wheel-item[aria-selected="true"] { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }


/* Sidebar "Briefing" card body (the head is the shared .settings-card-head
   toggle, defined with the settings cards at the end of this file): a
   two-column grid — a right-aligned label column shrunk to the longest label,
   and a field column filling the rest. WX Parameters / TAF sit at the top,
   then the NOTAM sub-fields. */
/* Top padding stays at the card's own 2px — same rhythm as Airports/WX
   Minima, whose content sits right under the card's 12px top padding with no
   extra wrapper padding of its own. */
.briefing-filter-body { padding: 2px; position: relative; }
/* The report button opens the Briefing card; a clear breath separates it from
   the filter rows below, so it reads as the card's action, not their first row. */
#briefingFilterCard .report-export-btn { margin-bottom: 12px; }
/* Each filter row is a single full-width field: its label sits INSIDE the field
   pill (the button) on the left in medium weight; the value + caret are pushed to
   the right edge (content right-aligned, title left). The rows stack full width
   and the dropdown panels flow in-card just below their field, so opening one
   grows the card instead of floating out of it. */
.briefing-filter-body .wx-filter-dropdowns {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}
/* Row + NOTAM wrapper carry the layout directly; each row holds one full-width field. */
.briefing-row { display: contents; }
.briefing-notam-block { display: contents; }
/* Scope and Traffic are multiselects rendered two ways from one state: the
   pills sitting INSIDE the field beside its label when the card is wide enough
   for them, and the ordinary labelled dropdown (collapsed summary + caret,
   switches in a panel) when it is not. The card itself is the container, so the
   choice tracks the real room a row has — not the viewport, which overshoots by
   the sidebar. Narrow is the default; the @container rules below promote each
   row once it fits, and Scope needs more room than Traffic. */
.briefing-filter-body .wx-filter-dropdowns { container-type: inline-size; }
.briefing-notam-block[hidden] { display: none; }
.briefing-row[hidden] { display: none; }
/* Label inside the field: left, regular weight (400), muted ink, shared by
   the settings Briefing card and the schedule form's field rows. */
.briefing-filter-body .briefing-row-label,
.map-layers-group .briefing-row-label {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    white-space: nowrap;
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    color: var(--muted);
}
.briefing-filter-body .wx-dropdown[hidden] { display: none; }
/* The field is a full-width pill: the button carries the fill and holds the label
   on the left; the value + caret are pushed to the right edge (the value's
   margin-left:auto eats the free space). Its panel flows just below it in-card. */
.briefing-filter-body .wx-dropdown { display: block; width: 100%; min-width: 0; }
/* A FILTER IS A SYSTEM ROW (planche 20), on the sidebar and in the briefing
   form alike. It used to be a rounded pill in the form and a row in the
   sidebar — one field written twice, and the form read as an older screen.
   The row is the app's own: 44px on a hairline, the name in the text ink, the
   value muted at the right, the hairline chevron for the sheet it opens — the
   SAME reading as .sheet-k / .sheet-v, which is the whole point: one page of
   settings, one way round. */
.briefing-filter-body .wx-dropdown-btn {
    width: 100%;
    justify-content: flex-start;
    gap: 6px;
    min-height: 44px;
    padding: 4px 0;
    border: 0;
    border-top: 1px solid var(--row-line);
    border-radius: 0;
    background: none;
    font-size: var(--fs-md);
}
.briefing-filter-body .briefing-row-label {
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    color: var(--text);
}
.briefing-filter-body .wx-dropdown-btn > .wx-dropdown-value {
    margin-left: auto;
    font-size: var(--fs-sm);
    color: var(--muted);
    font-weight: var(--fw-regular);
}
/* Schedule-form Briefing / Notifications sections carry this class too, so
   their fields read exactly like the rows above. The time selects (Issue
   Time / Start / End) match the format through their own shell: label
   inside-left, value + caret pushed right, one full-width pill per field
   (Start/End split their shared row). Only the top-level selects — the inner
   day/mode selects inside the panels keep the shared shell's spread. */
.briefing-filter-body .schedule-field > .schedule-select { width: 100%; }
.briefing-filter-body .schedule-field > .schedule-select > .schedule-select-button {
    justify-content: flex-start;
    gap: 6px;
    padding: 4px 12px;
}
.briefing-filter-body .schedule-field > .schedule-select > .schedule-select-button > .schedule-select-value { margin-left: auto; }
/* PAS DE BOÎTE DANS LA BOÎTE (règle du propriétaire) : la liste se pose sur le
   fond de sa feuille, comme celle des terrains. Elle portait un fond, un contour
   et un rayon — une carte — d'un temps où Categories s'ouvrait DANS la carte
   NOTAM filters et où il fallait l'en détacher. Les deux panneaux sont des
   feuilles plein écran depuis, et la feuille est déjà la boîte. */
/* UN SEUL DÉFILEUR, ET C'EST LA FEUILLE. La liste portait son propre plafond —
   480 px mesurés pour un contenu de 3 780 et une feuille de 726 — donc elle
   s'arrêtait 182 px avant le bas de l'écran, et ce qui restait dessous était du
   vide. Deux défileurs emboîtés, aussi : celui du dedans avalait le geste, et la
   tête de la feuille est déjà collante, donc rien ne se perd à la laisser
   défiler seule. */
.wx-notam-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.wx-notam-list .wx-param-row, .wx-notam-list .notam-option { min-height: var(--control-h); }
/* Panels flow in normal flow just under their field, so opening one grows the
   card (never floats outside it); only the two side-by-side altitude step lists
   still float (anchored to their dropdown). Categories is neither — its list
   runs far longer than the rest, so it opens as a full overlay
   (.wx-filter-overlay). */
.briefing-filter-body .wx-dropdown-panel {
    position: static;
    width: 100%;
    margin-top: 4px;
    box-shadow: none;
    min-width: 0;
    max-height: none;
    overflow: visible;
    background: var(--surface);
}
/* Category/special row with no loaded NOTAM: greyed, switch unchecked and
   disabled. */
/* CE QUE LE BRIEFING DU JOUR NE PORTE PAS : atténué, et RÉGLABLE (décision du
   propriétaire). La rangée était désactivée — donc le curseur disait « rien à
   faire ici » —, ce qui convenait tant qu'un réglage ne valait que pour l'écran
   du moment. Un preset réutilisable ne peut pas se poser sur les seules données
   du jour, donc l'absence est une information, plus un interdit.

   MARQ I — LE MOT PÂLIT, LE CONTRÔLE NON (décision du propriétaire) : la rangée
   entière passait à 45 % d'opacité, et un interrupteur allumé à 45 % se lit
   comme un interrupteur allumé — mesuré, la rangée avait l'air normale. Le gris
   porte donc sur le code et le mot, et l'interrupteur garde son encre : on voit
   que la rangée est réglable, et dans quel sens elle l'est.

   ET IL NE DIT QUE ÇA : éteint ne pâlit plus, puisque l'interrupteur le dit
   déjà. Deux faits sous un seul signe faisaient lire une rangée absente mais
   allumée comme une rangée éteinte. */
.notam-option.is-absent > .notam-option-code,
.notam-option.is-absent > .notam-option-name {
    color: var(--muted);
    font-weight: var(--fw-regular);
}

.alert-badge {
    display: inline-block;
    border-radius: 999px;
    padding: 2px 9px;
    transition: box-shadow 0.15s ease;
    /* Small vertical margin so the tinted fills of consecutive decoded rows
       don't touch top-to-bottom. */
    margin-block: 1px;
    line-height: 1.3;
    font-weight: var(--fw-regular);
}

/* =========================
   MOBILE
   ========================= */
/* The phone layout. Width alone used to decide it, and it put every recent
   iPhone held sideways on the wrong side: 844px (14) through 932px (15 Pro Max)
   all clear 840px, so a screen 390px TALL was given a 350px settings column and
   what was left of the briefing beside it. Height decides it too — a full-height
   column of settings is only worth its width when there is height to read it in.
   PHONE_LAYOUT_QUERY (state.js) carries this same pair for the handlers that
   drive the drawer; the two must stay in step. */
@media (max-width: 840px), (max-height: 500px) {
    /* Mobile briefing ground matches desktop: ivory (--bg), with the report
       blocks near-white (--surface-2) on top of it. */
    body { background: var(--bg); }
    .settings-content { padding-top: 8px; }
    .airport-field { padding: 6px 7px 7px; }
    .airport-list { max-height: 210px; }
    .config-head {
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 4px;
        padding: 7px 8px;
    }
    .config-head .block-title { padding-left: 2px; }
    .config-body { padding: 5px 8px 9px; }
    .small-btn { padding-left: 9px; padding-right: 9px; }

    :root { --main-gutter: 12px; }
    /* The column ends above the floating menu, so the last line under the data
       (the report's foot, audit A8) is read and not covered. */
    .main { padding-bottom: calc(var(--app-menu-clear) + 16px); }
    .page-header { flex-direction: column; align-items: stretch; gap: 10px; margin-bottom: 6px; }
    /* Mobile: the header line stands in the middle of its slab, 5 px above
       (under the status bar) and 5 px below, every view (planche CENTRE C.4;
       the desktop rule pushes it back down). */
    .page-header { padding-top: calc(5px + env(safe-area-inset-top, 0px)); padding-bottom: 5px; }
    .header-actions { width: 100%; gap: 8px; }
    .briefing { gap: 0; }
    .airport-title { gap: 7px; margin-bottom: 9px; }
    /* Mobile: identity dots reduced 30% (vs 20% on desktop). */
    .airport-title .status-dot { width: calc(var(--fs-md) * 0.7); height: calc(var(--fs-md) * 0.7); }
    .config-head .block-actions { gap: 7px; }
    .block-actions .danger-btn {
        position: relative;
        width: 30px;
        min-height: 30px;
        border-radius: 8px;
    }
    .block-actions .round-icon-btn { position: relative; }
    /* Zone tactile 36×36 : débord invisible de 3px autour du bouton visible de 30px */
    .block-actions .round-icon-btn::before,
    .block-actions .danger-btn::before {
        content: "";
        position: absolute;
        inset: -3px;
    }
}

/* Interaction refinements */
.block-actions {
    display: inline-flex;
    justify-content: flex-end;
    align-items: center;
    gap: 1px;
    min-width: max-content;
}
/* All config-card controls are grouped into this round "…" menu; only the
   filled dots are icon-specific. */
.block-menu { position: relative; display: inline-flex; }
.block-menu-list {
    position: absolute;
    /* On the chrome layer, with the sticky settings summary — otherwise a
       config's action menu opens behind the floating button and its lower
       items (Remove…) can't be reached. */
    z-index: var(--z-chrome);
    right: 0;
    top: calc(100% + 5px);
    min-width: 168px;
    padding: 5px;
    /* White panel on every breakpoint (was ivory --bg); items wash to --hover-bg
       on a real pointer (desktop), see the (hover: hover) block. En verre
       épais (planche HABILLAGE B.7). */
    background: var(--glass-thick);
    -webkit-backdrop-filter: var(--glass-thick-blur);
    backdrop-filter: var(--glass-thick-blur);
    border: 0;
    border-radius: var(--radius-md);
    box-shadow: var(--glass-drop-big), var(--glass-rim);
    display: flex;
    flex-direction: column;
    gap: 2px;
}
/* First row of the menu: where this configuration comes from (Standard, the
   operator's name, or User) — the same badge as in the picker, sitting above
   the actions rather than crowding the card head. */
.block-menu-list > .config-origin-tag {
    align-self: flex-start;
    margin: 0 0 2px;
    padding: 2px 9px;
    border-radius: var(--radius-sm);
}
.block-menu-item {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    min-height: 34px;
    padding: 6px 9px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    /* Icon + text options: heading-ink label, muted icon (below). */
    color: var(--heading);
    font-size: var(--fs-md);
    text-align: left;
}
/* The option icon reads muted in every icon + text dropdown; the same glyphs
   reused on round action buttons keep currentColor. */
.block-menu-item .block-menu-icon { stroke: var(--muted); }
.block-menu-icon {
    width: 14px;
    height: 14px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.block-menu-label { min-width: 0; }
.icon-btn {
    color: var(--muted);
    font-weight: var(--fw-semibold);
}
@media (hover: hover) {
    .icon-btn:hover {
        background: var(--hover-bg);
    }
    .danger-btn:hover {
        background: var(--hover-bg);
    }
}
/* Schedule cards reuse .block-actions for their remove (×) button. */
.block-actions .danger-btn {
    border: 0 !important;
    background: var(--bg) !important;
    box-shadow: none;
    border-radius: 8px;
    font-size: var(--fs-xl);
}
.config-delete-user-btn svg {
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
@media (hover: hover) {
    .block-actions .danger-btn:hover {
        background: var(--hover-bg);
    }
}


/* ---------------------------------------------------------------------------
   Account / auth panel (auth.js)
--------------------------------------------------------------------------- */

.auth-overlay {
    position: fixed;
    inset: 0;
    background: transparent;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-sheet);
    padding: 16px;
}

/* display:flex above would defeat the hidden attribute — keep it authoritative. */

.auth-panel {
    position: relative;
    width: 100%;
    max-width: 400px;
    max-height: 90vh;
    overflow-y: auto;
    overflow-x: hidden;
    /* Same thin scrollbar as the briefing (root) scroller. */
    scrollbar-width: thin;
    scrollbar-color: rgba(var(--text-rgb), 0.28) transparent;
    /* Claude treatment: no contour — the shadow alone separates the panel from
       the dimmed page. The ground is the page's own (--bg), so the white fields
       and lists a panel holds stand out of it the way they do in a card. */
    /* En verre épais (planche HABILLAGE B.7) : le liseré et la grande ombre. */
    background: var(--glass-thick);
    -webkit-backdrop-filter: var(--glass-thick-blur);
    backdrop-filter: var(--glass-thick-blur);
    border: 0;
    border-radius: var(--radius-lg);
    box-shadow: var(--glass-drop-big), var(--glass-rim);
    padding: 24px;
    font-size: var(--fs-md);
}


.auth-logo { height: 34px; display: block; margin: 0 auto 14px; }
.auth-title { margin: 0 0 12px; color: var(--heading); font-size: var(--fs-lg); }

/* The entry page's two fields are rows (planche 16): the name at the left,
   the field itself the row's value, no box of its own — and where a field
   draws anything (its focus ring), it draws it at the fields' 8 px (§ 8.9). */
.sheet-row-input { cursor: text; }
.sheet-row-input input,
.auth-view .sheet-row-input input {
    flex: 1 1 auto;
    min-width: 0;
    width: auto;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    text-align: right;
    font: inherit;
    color: var(--text);
}
/* The other paths, as a note under the button: the current mode's own line
   is not offered (the one it would lead to is). */
.auth-note { margin: 12px 0 0; text-align: center; font-size: var(--fs-md); color: var(--muted); line-height: 1.8; }
.auth-note .auth-path:has(.active) { display: none; }
.auth-note-sep { margin: 0 4px; }

/* Scoped to the auth form views (sign-in / recovery), not the whole panel, so the
   history overlay — which is also an .auth-panel — keeps its own compact field styles. */
.auth-view input {
    width: 100%;
    box-sizing: border-box;
    margin: 5px 0;
    padding: 9px 14px;
    border: 0;
    border-radius: 999px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
}


/* THE BUTTONS (owner's call, planche 3, system C): a rounded rectangle at the
   card's own radius, never a pill — a pill is a filter (METAR · TAF · NOTAM)
   or the source capsule, and a button must not look like either. Three roles,
   three dresses: the PRIMARY in the full ink (one per screen, the thing that
   writes), the SECONDARY tonal on the soft-line ground with no contour, the
   TERTIARY a bare word in the heading ink. The destroying verb is a plain row
   (.sheet-row-muted), not a button — the app has no red outside a verdict.
   Full width, stacked, 44px under the thumb. */
.auth-primary-btn {
    width: 100%;
    margin: 8px 0 0;
    padding: 0 16px;
    border: none;
    border-radius: var(--radius-md);
    background: var(--text);
    color: var(--bg);
    font: inherit;
    font-weight: var(--fw-semibold);
    cursor: pointer;
}

/* A sheet's one verb (action-sheet.js `primary`, the offline map's proposal):
   the primary button, inset in its card like a row. */
.action-sheet-rows > .auth-primary-btn { width: auto; min-height: 44px; margin: 8px 12px; }
.action-sheet-lead { margin: 4px 16px 10px; }

.auth-secondary-btn {
    width: 100%;
    margin-top: 8px;
    padding: 0 16px;
    border: 0;
    border-radius: var(--radius-md);
    background: var(--soft-line);
    color: var(--text);
    font: inherit;
    font-weight: var(--fw-medium);
    cursor: pointer;
}
@media (hover: hover) {
    .auth-secondary-btn:hover { background: var(--hover-bg); }
}



.auth-link-btn {
    display: inline-block;
    border: none;
    background: transparent;
    color: var(--heading);
    font: inherit;
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    cursor: pointer;
    padding: 2px 0;
}

.auth-divider {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 14px 0 10px;
    color: var(--muted);
    font-size: var(--fs-md);
}

.auth-divider::before,
.auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--soft-line); }

/* A provider is a secondary with its mark in front. */
.auth-provider-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    margin: 8px 0 0;
    padding: 0 16px;
    border: 0;
    border-radius: var(--radius-md);
    background: var(--soft-line);
    color: var(--text);
    font: inherit;
    font-weight: var(--fw-medium);
    cursor: pointer;
}

@media (hover: hover) {
    .auth-provider-btn:hover { background: var(--hover-bg); }
}
.auth-provider-btn svg { width: 18px; height: 18px; flex: none; }

/* Unify auth controls with the app: comfortable tap targets and shared motion. */
.auth-view input,
.auth-primary-btn,
.auth-secondary-btn,
.auth-provider-btn { min-height: 44px; }

.auth-primary-btn,
.auth-secondary-btn,
.auth-provider-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}
.auth-primary-btn { font-weight: var(--fw-semibold); }
.auth-title { font-size: var(--fs-md); }

.auth-message { margin: 10px 0 0; font-size: var(--fs-md); color: var(--heading); }
/* Clickwrap line under the sign-in / sign-up panel: accepting the Terms of Use
   (and their operational disclaimer) is part of entering the service. */
.auth-consent {
    margin: 14px 0 0;
    font-size: var(--fs-xs);
    line-height: 1.45;
    color: var(--muted);
    text-align: center;
}
.auth-consent a { color: var(--muted); }
/* A GATE: the disclaimer at every opening, the sign-in when there is no
   account. Full screen and opaque, over the bar and every page — nothing of
   the app is reachable behind it (auth.js). */
.auth-overlay.is-gate { z-index: var(--z-overlay); background: var(--bg); }
/* THE OPENING DISCLAIMER IS A PAGE, NOT A CARD (owner's decision): the logo
   at the top, the words centred in the middle with air between their
   sections, the button at the foot, the phone's whole height between them.
   Wider, the page takes the height of what it says and stands centred, one
   fixed air between its three parts, so they never drift apart. */
#disclaimerOverlay {
    padding: calc(40px + env(safe-area-inset-top, 0px)) 16px calc(24px + env(safe-area-inset-bottom, 0px));
}
#disclaimerOverlay > .auth-panel {
    display: flex;
    flex-direction: column;
    gap: 48px;
    height: auto;
    max-height: 100%;
    padding: 0;
    border-radius: 0;
    /* The page's own ground, no panel on it (owner's ask): the glass box
       drew a grey frame inside the page. */
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
}
#disclaimerOverlay .auth-logo { flex: none; height: 24px; margin: 0 auto; }
#disclaimerOverlay .disclaimer-body { margin: auto 0; text-align: center; }
#disclaimerOverlay .disclaimer-body .settings-card-title { margin-top: 28px; }
#disclaimerOverlay .disclaimer-body .settings-card-title:first-child { margin-top: 0; }
#disclaimerOverlay .disclaimer-body .confirm-text { margin-top: 6px; }
#disclaimerOverlay .disclaimer-foot { flex: none; }
@media (max-width: 599.98px) {
    #disclaimerOverlay > .auth-panel { height: 100%; gap: 24px; }
}
.account-delete .sheet-k { color: var(--status-error); }
.auth-message.error {
    color: var(--status-error);
    background: color-mix(in srgb, var(--status-error) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--status-error) 25%, transparent);
    border-radius: var(--radius-md);
    padding: 10px 12px;
}
.auth-message.success {
    background: rgba(var(--green-rgb), 0.1);
    border: 1px solid rgba(var(--green-rgb), 0.3);
    border-radius: var(--radius-md);
    padding: 12px 14px;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--success-text);
    line-height: 1.45;
}

/* L'adresse EST le titre de la carte : encre de titre, corps de titre. */
.account-email { font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--heading); word-break: break-word; }

/* LE FORFAIT EST UNE VALEUR, PAS UNE CARTE (§ 7.2, § 8.11) : écrit comme toute
   valeur de ligne, sans fond ni contour. */
.account-plan-pill {
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    color: var(--muted);
}

.account-operator { margin: 4px 0; font-size: var(--fs-md); color: var(--text); }

/* THE ACCOUNT IN THREE CARDS (owner's ask, planche 19), the page's own cards
   (the .is-page rules, with the form's). */
/* Written on the PAIR: the account's cards stand inside the page's view, so a
   margin on the first of them collapses out of that view and would push the
   page's first card down by a card gap instead of its bar's own inset. */
.account-card + .account-card { margin-top: var(--card-gap); }
/* A row that happens to be a link is still a row: the underline belongs to
   prose, not to a line with a chevron at its end. */
a.sheet-row { text-decoration: none; }

#billingSection h4 { margin: 16px 0 6px; color: var(--heading); font-size: var(--fs-lg); }
#billingSection h4:first-child { margin-top: 0; }
.billing-trial { margin: 0 0 6px; font-size: var(--fs-md); color: var(--muted); }
.billing-buttons { display: flex; flex-direction: column; gap: 8px; }
.billing-buttons .auth-primary-btn, .billing-buttons .auth-secondary-btn { margin: 0; }

/* THE PASSING MESSAGE LIES ON THE PAGE BAR (docs/ui/grammaire.md, « Les couches, le mouvement et les icônes »): at
   the foot, on its own layer, over the bar — the bar's own foot and width, so
   it never covers a value being read. */
#hwxToast {
    bottom: calc(4px + var(--menu-safe));
    box-sizing: border-box;
    /* Posed on the foot band — the Briefing button and the menu — centred on
       the chrome's zone (planche PANNEAU B.11), inside that band's box. */
    left: calc(var(--free-left) + (100% - var(--free-left)) / 2);
    width: min(420px, calc(100% - var(--free-left) - 2 * var(--main-gutter)));
    min-height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    /* A flex item drops its trailing space: the gap keeps the message and its
       action ("ready. Reload") apart. */
    gap: 0.35em;
    transform: translateX(-50%) translateY(20px);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s, transform 0.2s;
}
#hwxToast.show { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
/* EVERY BUBBLE CLOSES (owner's decision, planche TÉLÉCHARGEMENT): the message
   and its verb in the middle, the sheets' cross at the right end, a 44 px
   target eaten into the bubble's padding so the bubble keeps its height. */
.hwx-toast-text { flex: 1 1 auto; min-width: 0; text-align: center; }
.hwx-toast-close {
    flex: none;
    width: 44px;
    height: 44px;
    margin: -9px -14px -9px 0;
    padding: 0;
    border: 0;
    background: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
.hwx-toast-close svg {
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
}
#hwxToast.err { color: var(--status-error); }
/* Action link inside the upgrade/register toast. */
.hwx-toast-link {
    border: 0;
    background: transparent;
    color: var(--muted);
    font: inherit;
    font-weight: var(--fw-semibold);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    padding: 0;
}


/* --- Account view: identity head, nav-row card, billing card --- */

.account-identity {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px 4px 12px;
}
/* L'AVATAR EST LA MARQUE DE LA CARTE (propriétaire) : l'initiale de l'adresse
   sur le disque plein des boutons, à l'encre du texte, et non sur un gris de filet. La carte qui
   porte QUI TU ES était la plus discrète de la page. */
.account-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 44px;
    height: 44px;
    border: 0;
    border-radius: 999px;
    background: var(--text);
    color: var(--bg);
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
}

/* Navigation entries (Settings, schedules, history, admin) grouped on one
   white card; each row is a quiet full-width button with a trailing chevron
   and a neutral hover wash. */
/* Billing block (innerHTML from auth.js): the Plan card's own rows and
   offers, never a card inside the card (§ 7.2, § 8.10). */

/* --- Briefing history --------------------------------------------------- */

/* No top padding: the sticky .overlay-header carries the top spacing itself and
   sits flush at the panel's top edge, so scrolled content never shows through a
   transparent band above it. */
.history-panel { max-width: 520px; padding-top: 0; }

/* Airports block on top, the From/To date pair below it. */
#historyOverlay .auth-title { margin: 0; }

/* The filter block is a white settings-card (head + airport picker + From/To
   row) — the flex-wrap layout below arranges the card's content; the head's
   width:100% puts it on its own first row. */
.history-filters {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    flex-wrap: wrap;
}

/* THE FILTERS ARE ROWS (planche 20): the fields on a tall one, each date on
   its own — the page's own grammar, not a card of fields. */
.history-filter { font-size: var(--fs-md); }
/* The date reads as the field's value: right-aligned, heading ink, medium —
   the field row itself carries the fill and the focus ring. */
.history-date-input {
    flex: 1 1 auto;
    min-width: 0;
    box-sizing: border-box;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--heading);
    font: inherit;
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    text-align: right;
}
/* No calendar glyph: the field row is the affordance (a click opens the
   native picker, see initReportHistory). */
.history-date-input::-webkit-calendar-picker-indicator { display: none; }

/* The Airports filter keeps NO dress of its own (owner's rule): it is the
   Briefing page's row, mounted, so it looks the way that row looks — a second
   set of sizes here is what let the two drift apart in the first place. */

.history-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 4px 0 12px;
}

.history-empty { margin: 8px 0; color: var(--muted); font-size: var(--fs-md); }


/* ONE REPORT, ONE ROW (§ 8.8): the stamp and the format on top, then what was
   briefed under it in the muted 12 px — the schedule, the fields with their
   dots, the minima — one line that wraps rather than lose a code. */
.history-when { color: var(--heading); font-weight: var(--fw-semibold); }
.history-mode {
    margin-left: 8px;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-regular);
    text-transform: capitalize;
}
.sheet-sub.history-sub {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px 10px;
}
.history-schedule {
    display: flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}
.history-schedule .field-lead-icon { width: 12px; height: 12px; }
.history-airports { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.history-airport { display: inline-flex; align-items: center; gap: 4px; }
.history-airport-sep { width: 1px; height: 12px; background: var(--line); }
.history-airport .status-dot { width: 9px; height: 9px; }
.history-configs { display: flex; flex-wrap: wrap; gap: 6px; }

/* PDF button: the row's bare glyph, a chrome icon (§ 8.13), round under the
   pointer as it was before the redesign (owner's call: the old radii
   everywhere). Its contact zone is 44 px, like the disc's (§ 5.6). */
.history-open-btn {
    position: relative;
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--round-btn-size);
    height: var(--round-btn-size);
    margin-left: auto;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--icon);
    cursor: pointer;
}
@media (hover: hover) {
    .history-open-btn:hover { background: var(--hover-bg); }
}
.history-open-btn::before {
    content: "";
    position: absolute;
    inset: min(0px, (var(--round-btn-size) - 44px) / 2);
}
.history-open-icon {
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
@media (hover: hover) {
}

/* --- Scheduled briefings & notifications ---------------------------------- */

/* The title + close button stay pinned to the top of the (scrolling) panel of
   the schedules and briefing-history overlays. The panel drops its own top
   padding (.history-panel) so this bar sits flush at top:0 and no transparent
   band shows above it as content scrolls under; the bar supplies the top space
   itself and the side negative margins let it span the panel's 24px side
   padding. Title fills the row; the close (×) sits after it and both are
   vertically centred on the same line. */
.overlay-header {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    margin: 0 -24px 12px;
    padding: 20px 24px 12px;
    /* Ni fond ni bordure (planche HABILLAGE B.2) : le fondu de ::after efface ce
       qui défile dessous, dans le verre de la feuille. */
    background: transparent;
    isolation: isolate;
    --head-fade: var(--glass-thick);
    --head-reach: 40px;
    /* Au-dessus d'elle, la poignée de la feuille : le fondu remonte jusqu'au
       bord de la feuille, sinon la poignée restait sur une bande d'une autre
       teinte. */
    --head-rise: 24px;
    display: flex;
    align-items: center;
    gap: 10px;
}
/* Sticky action footer, the mirror of the header: the panel's own actions ride
   the panel's ground while the list scrolls away underneath. Hidden by its own
   view when it has nothing to hold — a scrim over nothing reads as a bug. */
.overlay-footer {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 16px -24px 0;
    padding: 14px 24px 18px;
    background: var(--glass-bar);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop-up), inset 0 1px 0 var(--glass-line);
}
/* A sticky footer can't leave its containing block, so the panel's own bottom
   padding would sit below it and the list would scroll into view in that strip:
   the panel gives the padding up and the footer carries it instead. With no
   footer showing (history without a next page, schedules while signed out) the
   panel keeps its padding. */
.history-panel { padding-bottom: 0; }
.history-panel:has(> .overlay-footer[hidden]) { padding-bottom: 24px; }
/* The Briefing card's full-screen filter panels — Categories and the TAF/NOTAM
   validity window — in the same shell as the schedules and history pages
   (.auth-overlay + .auth-panel.history-panel), from the sidebar and the schedule
   form alike. No sticky footer, so they keep their own bottom padding, like a
   history page with no next one to load. */
.wx-filter-overlay > .auth-panel { padding-bottom: 24px; }
/* THE MAP'S AND THE CHART'S SHEETS STAND AT THE RIGHT where the strip beside
   a panel is live (planche PANNEAU D.1; the body class main.js writes from the
   one threshold, B.8): the panel's width, from the top down to the menu band,
   the map lit and live beside them. Where the strip is not (the phone in
   portrait, D.4), nothing here applies and they come up from the foot as
   they always have: what they set is judged on the map above them. */
body.panels-side :is(#mapAirportCard, #mapSettingsCard, #mapPickCard, #mapRouteCard, #mapDirectCard, #ctClassesCard, #wxInfoCard, #profileSettingsCard) {
    /* On the panels' plane, the page bar one above it (B.4): over the bar,
       it took Map and Profile out of reach on the iPad. */
    z-index: var(--z-panel);
    inset: 0 0 0 auto;
    width: var(--panel-w);
    max-width: 100%;
    align-items: stretch;
    padding: 0;
}
body.panels-side :is(#mapAirportCard, #mapSettingsCard, #mapPickCard, #mapRouteCard, #mapDirectCard, #ctClassesCard, #wxInfoCard, #profileSettingsCard) > .auth-panel {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-radius: 0;
    border-left: 1px solid var(--line);
    box-shadow: none;
    transition: none;
    /* Under the foot band too (B.4): the content stops above it. */
    padding-bottom: var(--menu-band);
}
/* No handle: a side panel is not pulled down. */
body.panels-side :is(#mapAirportCard, #mapSettingsCard, #mapPickCard, #mapRouteCard, #mapDirectCard, #ctClassesCard, #wxInfoCard, #profileSettingsCard) > .auth-panel::before { display: none; }
/* The Report beside the map (planche FINALE R): the Report itself, borrowed
   into the same shell but standing at the left, in the Briefing's place, so
   the disc column at the right stays in view; the chrome moves right of it
   as it does of the fixed Briefing (--free-left). Drawn as on its page. */
:root:has(> body.report-side) { --free-left: calc(var(--rail-w) + var(--panel-w)); }
/* It stops above the foot bar (0 where the rail replaces it): the bar's
   Briefing, Report and Map stand under its width, and Report closes it. */
body.panels-side #reportSideCard {
    inset: 0 auto var(--menu-band) var(--rail-w);
    width: var(--panel-w);
    max-width: calc(100% - var(--rail-w));
    align-items: stretch;
    padding: 0;
}
body.panels-side #reportSideCard > .auth-panel {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-radius: 0;
    border-right: 1px solid var(--line);
    box-shadow: none;
    transition: none;
}
body.panels-side #reportSideCard > .auth-panel::before { display: none; }
body.map-view-open #reportSideBody > #briefing,
body.map-view-open #reportSideBody .briefing-summary:not([hidden]) { display: block; }
#reportSideCard > .auth-panel { padding-top: 0; }
#reportSideHead { flex-wrap: wrap; }
body.map-view-open #reportSideHead > .report-sommaire { display: flex; order: 3; margin-top: 4px; }
/* Map settings (#mapSettingsCard), in that very shell: the weather's own list
   and every layer a flight is drawn with are a list to read, and beside a disc
   they were a 110px-tall card that scrolled. Footerless too, so the bottom
   padding comes back; its widest row is the three-pill mode rail, which the
   520px panel holds without wrapping. */
.map-data-panel { padding-bottom: 24px; }
/* AND IT IS A SHEET (owner's call). The map settings used to open as a card in
   the middle of the screen over a dimmed map, which is the one thing this panel
   must not do: every press in it changes what the GROUND is wearing, and a
   pilot has to see that happen without closing anything.

   So it comes up from the bottom over two thirds of the height, the map is left
   lit above it — no veil at all — and it is pulled back down by its handle.

   And the band above it is REACHABLE, not merely visible (owner's call): the
   overlay takes no press of its own, so the map under it pans and zooms with
   the sheet up, and the pilot moves the ground to the place he is judging. A
   press that did not move the map closes the sheet — the gate
   that tells the two apart is map-engine.js, onTapGate. */
/* LOT F (planche PANNEAU F.1): where the strip is live, a confirmation stands
   at the centre of the screen, and an action menu hangs off the button that
   opened it — a popover, no veil, the page still read; a menu whose press
   left no anchor stands at the centre too. The phone in portrait keeps its
   sheets: one body class decides (D.1), never a media query. */
body.panels-side :is(#confirmOverlay, .action-sheet:not(.is-anchored)) {
    align-items: center;
    padding: 16px;
}
body.panels-side :is(#confirmOverlay, .action-sheet) > .auth-panel {
    height: auto;
    max-height: calc(100vh - 32px);
    width: var(--panel-w);
    border-radius: var(--radius-lg);
}
body.panels-side :is(#confirmOverlay, .action-sheet) > .auth-panel::before { display: none; }
/* A SHEET OPENED FROM THE BRIEFING PANEL STANDS IN THE PANEL (owner's
   decision; sheet.js marks it): the panel's place and width, the map or the
   Report live beside it. */
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel] {
    inset: 0 auto 0 var(--rail-w);
    width: var(--panel-w);
    max-width: calc(100% - var(--rail-w));
}
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel] > .auth-panel { max-width: none; }
/* ONE THAT WOULD TAKE MOST OF THE PANEL TAKES ALL OF IT (owner's decision,
   planche PANNEAU F.2; sheet.js fitPanelSheet marks it): it is a page — the
   panel's ground top to bottom, no handle, no drag, and the page's bar: the
   way back at the left, the title centred in the page's own writing. */
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] {
    align-items: stretch;
    background: var(--bg);
    border-right: 1px solid var(--line);
}
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] > .auth-panel {
    height: 100%;
    max-height: none;
    border-radius: 0;
    box-shadow: none;
    padding-bottom: var(--menu-band);
}
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] > .auth-panel::before { display: none; }
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] .overlay-header {
    touch-action: auto;
    padding-top: 10px;
    padding-bottom: 10px;
}
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] .overlay-header::after {
    content: "";
    flex: 0 0 var(--round-btn-size);
}
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] .overlay-header .overlay-close {
    order: -1;
    margin-left: 0;
    border: 0;
    background: none;
    box-shadow: none;
    color: var(--text);
}
body.panels-side.briefing-open .auth-overlay.is-sheet[data-in-panel][data-panel-full] .overlay-header .auth-title {
    flex: 1 1 auto;
    text-align: center;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    letter-spacing: normal;
    text-transform: none;
    color: var(--heading);
}
/* WHAT IS OPENED FROM THE MAP STANDS AT THE RIGHT (owner's decision; sheet.js
   marks it): a sheet or a page born of a press on the map, its point menu or
   its cards takes the map cards' place — the panel's width, top to bottom —
   and the briefing panel, open or not, keeps its own. */
body.panels-side .auth-overlay[data-map-side] {
    inset: 0 0 0 auto;
    width: var(--panel-w);
    max-width: 100%;
    align-items: stretch;
    padding: 0;
    border-right: 0;
    border-left: 1px solid var(--line);
}
body.panels-side .auth-overlay.is-page[data-map-side] { visibility: visible; }
body.panels-side .auth-overlay.is-sheet[data-map-side] > .auth-panel {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-radius: 0;
    box-shadow: none;
    padding-bottom: var(--menu-band);
}
body.panels-side .auth-overlay.is-sheet[data-map-side] > .auth-panel::before { display: none; }
body.panels-side .action-sheet.is-anchored {
    background: transparent;
    padding: 0;
}
body.panels-side .action-sheet.is-anchored > .auth-panel {
    position: absolute;
    left: var(--anchor-x);
    top: var(--anchor-y);
    width: var(--panel-w);
    max-height: calc(100vh - var(--anchor-y) - 16px);
}
/* ===== THE TWO SHELLS (owner's call, planche 16) =====
   Every overlay of the app is one of two things. A SHEET comes up from the foot
   of the page and leaves the page lit above it: what is settled while staying
   in the page — a picker, a filter, the map's settings, a yes/no. It has three
   heights (data-sheet: s 36vh, m 62vh, l 86vh), a handle drawn by the shell,
   and it is pulled back down by its head (sheet.js). A PAGE takes the whole
   screen with a bar at its top — back, title, action — and no menu: what is a
   piece of work in itself (a briefing to write, the account, the history).
   The centred window is gone. */
/* A sheet opened from the chart viewer (GARDER C.4) rises over it: the viewer
   is the overlay layer, and the sheet comes after it in the body. */
.chart-viewer ~ .auth-overlay.is-sheet { z-index: calc(var(--z-overlay) + 1); }
.auth-overlay.is-sheet {
    align-items: flex-end;
    padding: 0;
    /* No veil (owner's decision): what is under a sheet stays as lit as it
       was; a press on the bare overlay is the press that closes the sheet. */
}
.auth-overlay.is-sheet > .auth-panel {
    pointer-events: auto;
    width: 100%;
    /* On a wide screen a full-bleed sheet reads as a page: it stops at the
       panel's own measure and stays centred on the content column. */
    max-width: 520px;
    height: 62vh;
    max-height: none;
    padding-top: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    /* The shadow reaches UP, over the page the sheet leaves visible. */
    box-shadow: var(--glass-drop-big), var(--glass-rim);
    /* No transform AT REST: a transformed ancestor becomes the containing
       block of every position:fixed descendant, and a select menu opened in
       the sheet was drawn off its field and clipped by the panel's scroll.
       Under the thumb the sheet IS the finger — the drag writes --sheet-drag
       (sheet.js) and the transform reads it, and no animation runs. */
    transition: transform 0.18s ease;
}
.auth-overlay.is-sheet[data-sheet="s"] > .auth-panel { height: 36vh; }
.auth-overlay.is-sheet[data-sheet="l"] > .auth-panel { height: 86vh; }
/* AS HIGH AS ITS CONTENT, and no higher than the middle notch (planches 22 ·
   23): the map's and the chart's settings sheets. A short list is a short
   sheet; a long one stops at 62% and scrolls inside. */
.auth-overlay.is-sheet[data-sheet="fit"] > .auth-panel { height: auto; max-height: 62vh; max-width: none; }
/* On a phone a sheet is full-bleed. Written AFTER the measure above, which it
   overrides: an earlier copy of this rule lost to it on every phone and every
   tablet in portrait (measured: 520px on a 744px iPad mini). */
@media (max-width: 840px), (max-height: 500px) {
    .auth-overlay.is-sheet > .auth-panel { max-width: none; }
}
.auth-overlay.is-sheet > .auth-panel.is-dragging {
    transform: translateY(var(--sheet-drag, 0px));
    transition: none;
}
/* The handle, drawn by the shell on every sheet. */
.auth-overlay.is-sheet > .auth-panel::before {
    content: "";
    display: block;
    width: 38px;
    height: 4px;
    margin: 8px auto 2px;
    border-radius: 999px;
    background: var(--contour);
}
/* The head row a drag may start on keeps the gesture for itself; everything
   below scrolls as it did. */
.auth-overlay.is-sheet .overlay-header { touch-action: none; }
/* THE SHEET HAS NO TITLE BAR (owner's call, planche 2, 3-A): its name is
   written the way every card head in the app is — the small caps in the
   muted ink — and the first row follows at once, the close disc at the right
   of the same line. The row keeps its element: it is the handle the sheet is
   pulled down by (sheet.js). The airport card's head keeps its own rail. */
.auth-overlay.is-sheet .overlay-header:not(.map-card-head) {
    min-height: 36px;
    margin: 0 -24px 4px;
    padding: 4px 24px 6px;
}
.auth-overlay.is-sheet .overlay-header .overlay-close { margin-left: auto; }
.auth-overlay.is-sheet .overlay-header .auth-title {
    font-family: var(--app-font);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}
/* THE MAP IS LIVE UNDER ITS SHEET (owner's call): the overlay takes no press of
   its own, so the map pans and zooms with the sheet up; a press that did not
   move the map closes it — the gate is map-engine.js, onTapGate. */
#mapSettingsCard,
#mapPickCard { background: transparent; pointer-events: none; }
#mapSettingsCard > .auth-panel,
#mapPickCard > .auth-panel { pointer-events: auto; }
/* FROM THE TRUE BOTTOM, THE WHOLE COLUMN WIDE (owner's decision, planches 22 ·
   23): the map's and the chart's settings sheets start at the content column's
   edge and cover the menu and the capsule — what they set is judged on the map
   above them, not at the foot. The content column, so a standing settings
   column is never bitten into (measured before: 43px over it at 1133×744). */
#mapSettingsCard,
#mapPickCard,
#profileSettingsCard { left: var(--free-left); }
/* THE ROUTE AS ONE LINE (route-line.js) — how a library card says WHICH route.
   The thread is the route's own ink and every disc is .route-wp-marker, the
   mark the map and the Briefing page's list of points already wear: one route,
   one drawing, wherever it is read.

   The pitch is FIXED: a route of two points and one of five are then drawn at
   the same scale, so the eye compares routes instead of rescaling at every
   card.

   IT IS A ROW OF THE CARD, AND WEARS THE ROW'S RULE (owner's ask): the hairline
   every .sheet-row carries on top, so the drawing reads apart from the title
   above it instead of hanging off it. And it keeps AIR ON ITS LEFT (owner's
   ask, twice): the first code is wider than the disc it names and used to lean
   out of the card's own padding — the thread starts inset now, and the code
   under the first disc stands inside the card like every other word in it.

   Nothing here wraps: past five points the builder elides, and the gap cell
   takes the slack (flex: 1) so the last point lands at the card's right edge. */
/* A LIBRARY IS A LIST (§ 7.4, § 8.8): one system row per record, its name on
   one line — a secondary name, so it gives way (§ 4.5) — and what it holds
   under it, which wraps rather than lose a code. */
/* A record reads on a line or two (§ 8.8): its row keeps a tighter inset than a
   setting's sentence, so a summary that wraps once still fits 64 px. */
.library-row .sheet-k:has(.sheet-sub) { padding: 4px 0; }
/* Le nom d'un enregistrement est un objet de la bibliothèque : en gras
   (décision du propriétaire), contre son résumé gris. */
.library-name { display: block; font-weight: var(--fw-semibold); }

.route-line {
    --route-line-pitch: 64px;
    --route-line-disc: 18px;
    display: flex;
    align-items: flex-start;
    /* One inset on each side, half a code wide: the first and the last code are
       centred on discs pushed in by just enough that the label under each stays
       inside the card, whether the line is whole or elided. */
    /* The code sits UNDER its disc, so the line needs room for both before the
       next row's hairline — 4px left it touching that rule (owner's ask). */
    padding: 10px 16px 12px;
    /* The row's own rule, as every .sheet-row wears it. */
    border-top: 1px solid var(--line);
    min-height: 52px;
}
.route-line-pt {
    position: relative;
    flex: none;
    width: var(--route-line-pitch);
    height: var(--route-line-disc);
}
.route-line-pt.is-last { width: var(--route-line-disc); }
/* The thread between two drawn points: from the disc before to this one. */
.route-line-pt + .route-line-pt::before,
.route-line-gap + .route-line-pt::before {
    content: "";
    position: absolute;
    right: 100%;
    top: calc(var(--route-line-disc) / 2 - 1px);
    width: calc(var(--route-line-pitch) - var(--route-line-disc));
    height: 2px;
    background: var(--chart-route);
}
.route-line-gap + .route-line-pt::before { width: 0; }
.route-line-pt > .route-wp-marker {
    position: absolute;
    left: 0;
    top: 0;
    font-style: normal;
}
/* The code CENTRED ON ITS DISC (owner's ask), not on the cell it shares. */
.route-line-pt > b {
    position: absolute;
    left: calc(var(--route-line-disc) / 2);
    top: calc(var(--route-line-disc) + 6px);
    transform: translateX(-50%);
    font-family: var(--app-font);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-medium);
    /* A line-height of its own: the code's box must not grow with the face's
       ascenders and run into the next row's rule. */
    line-height: 1.2;
    letter-spacing: 0.04em;
    color: var(--text);
    white-space: nowrap;
}
/* The points the line does not draw: the thread goes dotted and carries how
   many were swallowed. It takes the slack, so the last point sits at the edge. */
.route-line-gap {
    position: relative;
    flex: 1 1 auto;
    min-width: 44px;
    height: var(--route-line-disc);
}
.route-line-gap::before {
    content: "";
    position: absolute;
    inset: calc(var(--route-line-disc) / 2 - 1px) 0 auto 0;
    height: 2px;
    background: repeating-linear-gradient(90deg, var(--chart-route) 0 3px, transparent 3px 7px);
}
.route-line-gap > i {
    position: absolute;
    left: 50%;
    top: calc(var(--route-line-disc) / 2 - 6px);
    transform: translateX(-50%);
    padding: 0 6px;
    background: var(--bg);
    font-family: var(--app-font);
    font-style: normal;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-medium);
    color: var(--muted);
    white-space: nowrap;
}

/* THE ADD PILL OF A LIBRARY PAGE (owner's decision, planche « New briefing »,
   forme D). Writing a new object started at the very END of the list: on a page
   of tall cards that is a scroll away, and the thumb is at the bottom of the
   screen. So the page's ONE add verb floats there instead — named, because a
   bare "+" on a page holding several kinds of row says which one it adds only
   by luck.

   Its ground is the app's own surface and what detaches it from the list it
   floats over is the hairline ring of a raised surface, never a shadow
   (docs/ui/grammaire.md, « Le sol et les encres », « Les couches, le mouvement et les icônes »).

   Sticky, not fixed: the pill is the last child of the page's own scroller, so
   it floats above the list while there is list left to scroll and then comes to
   rest after the last card — nothing is ever hidden underneath it. And the
   panel becomes a column so `margin-top: auto` drops the pill to the foot of
   the screen on a page holding two cards, where it would otherwise sit halfway
   up with nothing under it.

   ONE INSET UNDER IT, 12 px above the home indicator (owner's ask: it stood far
   too high). A sticky box is held off the scroller's PADDING edge, so its
   `bottom` adds to the panel's own bottom padding: the two insets, each with
   the safe area, stacked to ~110 pt on an iPhone. The panel alone says it now,
   and the pill sits on it, whether it floats or rests after the last card. */
.auth-overlay.is-page > .auth-panel:has(> .page-add-pill) {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
}
.page-add-pill {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    align-self: center;
    margin-top: auto;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    width: max-content;
    max-width: 100%;
    min-height: 46px;
    padding: 0 20px;
    border: 0;
    border-radius: 999px;
    background: var(--surface);
    box-shadow: inset 0 0 0 1px var(--contour);
    color: var(--text);
    font-family: var(--app-font);
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    cursor: pointer;
}
/* The "+" is the row's own glyph, the twin of the chevron — one drawing for the
   verb, wherever it is worn. */
/* The waypoint library's verb is the one node that wears BOTH dresses — a row in
   the route's add sheet, this pill on its own page (waypoints-page.js). Its row
   writes the name first and the "+" last; the pill wants them the other way
   round, and turning the line over costs less than a second node. */
.page-add-pill > .sheet-k { order: 1; color: inherit; font: inherit; }
/* Nothing left to add: the pill goes, it never stands greyed at the foot of the
   screen promising a door the plan has closed. The page's quota line says why. */
.page-add-pill:disabled { display: none; }

/* THE PAGE. */
.auth-overlay.is-page {
    align-items: stretch;
    padding: 0;
    background: var(--glass-thick);
    -webkit-backdrop-filter: var(--glass-thick-blur);
    backdrop-filter: var(--glass-thick-blur);
    /* UNDER THE BAR (planche RAIL R.4): a page is the panel's plane, later in
       the document than the panel so it stacks over it, and the foot band
       stays under the finger. */
    z-index: var(--z-panel);
    /* IN THE BRIEFING PANEL'S BOX (planche PANNEAU C.1): a page of the
       briefing or of the account stacks over the panel, the same width, from
       the top down to the screen's bottom edge, under the foot band (B.4),
       with its way back in its bar. It is hidden with the panel and comes
       back with it (B.6): the stack is kept. */
    inset: 0 auto 0 var(--rail-w);
    width: var(--panel-w);
    max-width: calc(100% - var(--rail-w));
    box-shadow: var(--glass-drop-side), inset -1px 0 0 var(--glass-line);
}
body:not(.briefing-open) .auth-overlay.is-page { visibility: hidden; }
/* What a page holds ends above the foot band: its last card, its sticky
   footer and its add pill all stop where the band begins (the panel's own
   padding, below). */
.auth-overlay.is-page .overlay-footer,
.auth-overlay.is-page .page-add-pill { bottom: var(--menu-band); }
.auth-overlay.is-page > .auth-panel {
    max-width: none;
    max-height: none;
    height: 100%;
    border-radius: 0;
    /* La page porte le verre ; son panneau ne le double pas. */
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
    padding: 0 16px calc(var(--menu-band) + 24px);
}
/* ONE AIR BETWEEN TWO CARDS, ON EVERY PAGE (owner's ask). It was scattered:
   the Briefings list breathed 10px, the Areas, Routes and Waypoints lists 2px,
   and a card standing straight on a page — the area editor, the route editor's
   three — touched the one above it. One mechanism now, on the page itself, so
   a new page cannot be born cramped: every pair of neighbours takes the same
   measure, whether it is two cards or a list and a card, and that measure is
   the Briefing page's (--card-gap), which is the app's reference. Margins, not
   a flex gap: four of these panels are already flex columns (the floating verb
   needs `margin-top: auto`) and the rest are blocks. The floating verb is the
   one child this rule must not reach: its `margin-top: auto` is what drops it
   to the foot of a short page, and a fixed measure here left it hanging 429px
   above the screen's bottom (measured). */
.auth-overlay.is-page > .auth-panel > * + *:not(.page-add-pill) { margin-top: var(--card-gap); }
/* The two waypoint pages draw their cards into a mount (waypointCardsHtml):
   those cards are the mount's children, not the panel's, so the rule above
   never reached them and they stood touching. Same measure, one level down. */
#routeWpNewMount > * + *,
#routeWpEditMount > * + * { margin-top: var(--card-gap); }
/* THE BAR IS NOT A CARD: what a page leaves under its own bar is its top
   inset, not the air between two cards, so it keeps the 10px it has always
   had — the same order as the Briefing page, which opens on 8px under its
   header. Without this line the cards' new measure pushed every page's first
   card down with them. */
.auth-overlay.is-page > .auth-panel > .overlay-header + *:not(.page-add-pill) { margin-top: 10px; }
/* The account is the one page whose cards stand inside a VIEW — sign-in,
   account, password recovery — and the two it is not showing stand HIDDEN
   before it, so the rule above reads one of them as the card above and gave
   the page a card's air under its bar. A view is not a card, it is the page's
   whole content: it takes the bar's inset like every other page's first card. */
.auth-overlay.is-page > .auth-panel > .auth-view { margin-top: 10px; }

/* The bar: back at the left, the title centred, the action at the right — the
   same three tracks on every page, so the thumb finds them without looking. */
.auth-overlay.is-page > .auth-panel > .overlay-header {
    display: grid;
    grid-template-columns: var(--round-btn-size) minmax(0, 1fr) minmax(var(--round-btn-size), auto);
    align-items: center;
    gap: 6px;
    /* No air of its own under it: the page's one spacing rule below gives it,
       like it gives every other pair on the page. */
    margin: 0 -16px;
    padding: calc(10px + env(safe-area-inset-top, 0px)) 16px 10px;
}
.auth-overlay.is-page > .auth-panel > .overlay-header .overlay-close { grid-column: 1; grid-row: 1; }
/* UN TITRE DE PAGE, UNE ÉCRITURE : 16/600 dans l'encre de titre, sur toutes
   les pages — il variait de 14 à 16 px et de 500 à 600 selon la page. */
.auth-overlay.is-page > .auth-panel > .overlay-header .auth-title {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    text-align: center;
    font-family: var(--app-font);
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--heading);
}
/* The page's ACTION, at the right of the bar (planche 16): a word in the
   heading ink, no box — Save on the briefing form. */
.overlay-action {
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    min-height: var(--round-btn-size);
    padding: 0 4px;
    border: 0;
    background: none;
    font: inherit;
    font-weight: var(--fw-semibold);
    color: var(--heading);
    cursor: pointer;
}
/* L'action de la barre est un mot dans une boîte de 36 px ; sa zone de contact
   fait 44. Mesuré au doigt : aucune autre cible dans ce débord. */
.overlay-action { position: relative; }
.overlay-action::after {
    content: "";
    position: absolute;
    inset: -4px -2px;
}
/* THE PAGE'S OTHER WAY OUT (owner's decision): a page opened from the account
   stands over it, so the back arrow returns there — and the × in the action
   slot closes every page at once and gives the report back (main.js). */
.overlay-dismiss { grid-column: 3; grid-row: 1; justify-self: end; }
/* The answers at the foot of a sheet (the confirm): stacked, the one that
   writes first. */
.sheet-actions { display: flex; flex-direction: column; gap: 8px; margin: 14px 0 4px; }
.sheet-actions .auth-primary-btn,
.sheet-actions .auth-secondary-btn { margin: 0; }
/* The history's months: a group heading of a list, 12/600 in --muted with no
   capitals and no tracking — the time zone's own (§ 8.10). */
.history-month {
    margin: 14px 0 6px;
    font-family: var(--app-font);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    color: var(--muted);
}
.history-sub .config-badge { font-size: var(--fs-xs); }
/* The form's guide, under the Schedule switch: a note, in the ink of the page. */
/* The way back is a bare chevron, not a disc: it is the page's edge, not a
   control standing on it. */
.auth-overlay.is-page .overlay-header .overlay-close {
    border: 0;
    background: none;
    box-shadow: none;
    color: var(--text);
}
/* The picker overlay — the Airports, WX Minima and phenomena fields, lifted into
   the same shell (openFieldOverlay, render.js). Same footerless panel, and the
   lifted field's list stops floating under it: inside the panel it flows, takes
   the full width and scrolls on its own. */
.field-overlay > .auth-panel { padding-bottom: 24px; }
/* ===== THE ROWS OF A SHEET (owner's call, planche 16, L2) =====
   Inside a sheet every choice is one system row: 44px, a hairline over it, the
   name at the left in the app font, and at the right what the choice IS — a
   check for one-of-n, a switch for yes/no, a star or a distance for a list
   entry. No pill, no card, no wash: the sheet's ground is the only ground.
   Scoped to the sheet, so the same controls keep their look wherever else
   they stand (the schedule form, the map panel). */
.auth-overlay.is-sheet .airport-option,
.auth-overlay.is-sheet .config-option,
.auth-overlay.is-sheet .notam-option,
.auth-overlay.is-sheet .phenomena-option,
.auth-overlay.is-sheet .schedule-select-option,
#timezoneOverlay .schedule-select-option,
.auth-overlay.is-sheet .wx-param-row {
    min-height: 44px;
    padding: 2px 0;
    border-radius: 0;
    border-top: 1px solid var(--row-line);
    background: none;
}
.auth-overlay.is-sheet .airport-list,
.auth-overlay.is-sheet .config-list,
.auth-overlay.is-sheet .wx-param-rows,
#timezoneOverlay .schedule-tz-options { gap: 0; max-height: none; }
/* The page is a list (§ 8.10): the sentence of what the choice means stands
   above the search row with the air a card's note would have had. */
.tz-page > .schedule-note { margin-bottom: 10px; }
.tz-page > .settings-card + .settings-card { margin-top: var(--card-gap); }
/* The group titles speak the settings' grammar: small caps, no ground.
   ET ILS COLLENT SOUS LA TÊTE DE LEUR FEUILLE, jamais à zéro (décision du
   propriétaire : « que les titres de catégorie, pays, groupe, soient sticky »).
   Ils se collaient à zéro — là où la tête de la feuille est collée elle aussi,
   et deux fois plus haute — donc ils passaient dessous et n'existaient plus
   pendant tout le défilement. Mesuré : tête de feuille 46 px.

   UN SEUL NOMBRE, lu par toutes les listes : une liste qui s'en écarterait
   collerait son titre à une autre hauteur que sa voisine, et c'est le genre
   d'écart qu'on ne voit qu'en comparant deux écrans. */
.auth-overlay.is-sheet .auth-panel {
    --sheet-head-h: 46px;
    /* Dans une feuille, ce qui surplombe une liste est la tête de la feuille. */
    --sticky-head: var(--sheet-head-h);
    /* ET CE QU'ON AMÈNE EN VUE S'ARRÊTE SOUS EUX : sans ce retrait, « aller à
       cette rangée » la pose DERRIÈRE le titre collant, donc ni lisible ni
       touchable. */
    scroll-padding-top: calc(var(--sheet-head-h) + 44px);
}
/* SUR LE BUREAU, LA FEUILLE N'EST PLUS UNE FEUILLE : son panneau ne défile pas,
   c'est la PAGE qui défile, donc un titre collé à 46 px se colle à 46 px du haut
   de la FENÊTRE — au milieu de la liste, par-dessus ses rangées. Mesuré : le
   titre « Spain » recouvrait la rangée de LEPA et interceptait le clic. Le
   retrait ne vaut donc que là où la tête de feuille est réellement au-dessus. */
@media (min-width: 841px) and (min-height: 501px) {
    .auth-overlay.is-sheet .auth-panel { --sheet-head-h: 0px; }
}
.auth-overlay.is-sheet .airport-group-header,
.auth-overlay.is-sheet .config-section-title,
.auth-overlay.is-sheet .phenomena-category-title,
#timezoneOverlay .tz-group-header {
    padding: 14px 0 4px;
    font-family: var(--app-font);
    /* Un en-tête de groupe dans une liste : 12/600 en --muted, sans capitales
       ni chasse (docs/ui/grammaire.md, « Les feuilles, les pages courtes et leur relecture »). */
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    color: var(--muted);
    letter-spacing: 0;
    text-transform: none;
    background: var(--bg);
}
/* LE RETRAIT NE VAUT QUE LÀ OÙ LE PANNEAU EST LUI-MÊME LE DÉFILEUR, et ce n'est
   pas partout : mesuré, le sélecteur de terrains et celui des minima se laissent
   défiler par la PAGE. Un titre collé à 46 px s'y colle alors à 46 px du haut de
   la FENÊTRE — au milieu de sa liste, par-dessus ses rangées : le titre
   « Spain » recouvrait la rangée de LEPA et interceptait le clic. Ces listes-là
   gardent leur collant à zéro, qui est le haut de CE qui les défile ; les deux
   panneaux NOTAM, dont le panneau EST le défileur, prennent le retrait. */
/* ET CE QUI DÉFILE VERS LE HAUT S'ARRÊTE SOUS LE TITRE, pas dessous. Un titre
   collant recouvre la rangée qui arrive exactement à sa hauteur : amener une
   rangée « en haut » la posait DERRIÈRE le titre, donc elle n'était ni lisible
   ni touchable. Mesuré : le clic sur un terrain amené en vue était intercepté
   par le titre de son pays. `scroll-margin-top` est ce qui dit au défilement de
   s'arrêter une tête plus tôt. */
.airport-option, .config-option, .phenomena-option, .notam-option {
    scroll-margin-top: calc(var(--sticky-head, 51px) + 44px);
}
/* RIEN NE SURPLOMBE LA LISTE DES TERRAINS DANS SON PANNEAU : elle s'ouvre sous
   son champ de recherche, tout en haut de ce qui la défile. Un titre qu'on
   décale alors vers le bas se pose PAR-DESSUS ses propres rangées — mesuré, le
   titre « Spain » recouvrait la rangée de LEPA et interceptait le clic. Le
   surplomb d'une liste est un fait de SA PLACE, pas de son type — la liste des
   minima s'ouvre de la même façon, et le même titre y interceptait le chevron
   de la fiche. */
.airport-dropdown, .config-list { --sticky-head: 0px; }
/* La rangée maître d'un groupe NOTAM est le titre de son groupe : elle colle
   comme eux. Il lui faut un fond opaque — sans lui les codes du groupe
   défileraient au travers. */
.auth-overlay.is-sheet .notam-option-group {
    position: sticky;
    top: var(--sheet-head-h, 0px);
    z-index: var(--z-sticky);
    background: var(--bg);
}
/* The airport row: its symbol, its code, its name, its distance, its
   star. Chosen, it reads heavier and wears a short rule at its left. */
.auth-overlay.is-sheet .airport-option-code,
.auth-overlay.is-sheet .notam-option-code {
    font-family: var(--app-font);
    font-weight: var(--fw-semibold);
    font-size: var(--fs-md);
}
/* CHOIX C.1 — THE CHOSEN ROW WEARS THE CAPSULE (owner's decision, planche
   SÉLECTION): a rounded ground of the ink at 7 % and its name in bold, and
   nothing else — no check, no rule —, the same in every list: the airports and
   the phenomena one adds, the minima, the window and the time zone one picks,
   the source, the radar alert's choices, the record a library has loaded.
   Rows that are taken AWAY keep their switch (MARQ A.3): they are not a choice. */
:is(.airport-option, .phenomena-option, .config-option, .schedule-select-option).selected,
.auth-overlay.is-sheet :is(.airport-option, .phenomena-option, .config-option, .schedule-select-option).selected,
#timezoneOverlay .schedule-select-option.selected,
.sheet-row.is-chosen {
    background: var(--chosen-bg);
    border-radius: var(--radius-sm);
}
.airport-option.selected .airport-option-name,
.phenomena-option.selected .phenomena-option-name,
.config-option.selected .config-option-title,
.schedule-select-option.selected,
.auth-overlay.is-sheet .schedule-select-option.selected,
#timezoneOverlay .schedule-select-option.selected,
.sheet-row.is-chosen > .sheet-k { font-weight: var(--fw-black); color: var(--heading); }
.sheet-row.is-chosen > .sheet-k > .sheet-sub { font-weight: var(--fw-regular); }
/* CHOIX C.3 — the capsule is not laid on a line: the hairlines on either side
   of it step aside, and a row that stands flush with its card or its sheet
   reaches 8 px out, so its words stand inside the ground. */
:is(.airport-option, .phenomena-option, .config-option, .schedule-select-option).selected + *,
.auth-overlay.is-sheet :is(.airport-option, .phenomena-option, .config-option, .schedule-select-option).selected + *,
#timezoneOverlay .schedule-select-option.selected + *,
.sheet-row.is-chosen + .sheet-row { border-top-color: transparent; }
.auth-overlay.is-sheet :is(.airport-option, .phenomena-option, .config-option, .schedule-select-option).selected,
#timezoneOverlay .schedule-select-option.selected,
.sheet-row.is-chosen { border-top-color: transparent; margin-inline: -8px; padding-inline: 8px; width: calc(100% + 16px); }
/* A list that scrolls clips what passes its edge, the capsule's 8 px and its
   corners with it: such a list keeps 8 px of air on either side for it, its
   rows where they stood. */
.auth-overlay.is-sheet :is(.airport-list, .config-list, .phenomena-dropdown),
#timezoneOverlay .schedule-tz-options { margin-inline: -8px; padding-inline: 8px; }
/* One of n, in a sheet and on the Time zone page, which asks the same
   question of four hundred rows. */
.auth-overlay.is-sheet .schedule-select-option,
#timezoneOverlay .schedule-select-option {
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    color: var(--text);
}
/* Yes/no: the row's own toggle carries the app's switch, the ink saying on. */
.auth-overlay.is-sheet .wx-param-row { padding: 2px 0; }
.auth-overlay.is-sheet .wx-filter-toggle {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    /* La rangée d'un interrupteur fait 44 px, comme toute rangée (§ 5.6) :
       l'interrupteur est dessiné à droite, la cible est la rangée entière. */
    min-height: 44px;
    padding: 0 48px 0 0;
    border: 0;
    background: none;
    box-shadow: none;
    color: var(--text);
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    text-align: left;
}
.auth-overlay.is-sheet .wx-filter-toggle .wx-filter-label { color: var(--text); font-weight: var(--fw-regular); }
/* La rangée NOTAM porte son mot dans la grille et non dans le bouton : le bouton
   n'est donc que le rail, et n'a plus à réserver quarante-huit pixels à sa
   droite pour un libellé qui n'y est plus. */
.auth-overlay.is-sheet .notam-option > .wx-filter-toggle {
    flex: none;
    /* 48 px de large pour un interrupteur de 36 : la cible déborde le dessin
       vers le libellé, qui n'est pas une autre cible. */
    width: 48px;
    min-width: 48px;
    padding: 0;
}
/* The row's switch is the app's one switch (§5.1): the button carries the
   taf-switch track and thumb, stood at the right edge of the row. */
.wx-filter-toggle > .taf-switch-track {
    position: absolute;
    right: 0;
    top: 50%;
    margin-top: -10px;
}
/* The master switch (All parameters) takes the sheet's row: full width, 44px,
   the track at the right like every value. Its dress is the app's one switch,
   declared once above — nothing about the track or the thumb is said here. */
.auth-overlay.is-sheet .wx-params-body .taf-switch {
    width: 100%;
    min-height: 44px;
    height: auto;
    padding: 2px 0;
    justify-content: space-between;
    flex-direction: row-reverse;
    color: var(--text);
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
}
.auth-overlay.is-sheet .wx-params-body .taf-switch .taf-switch-label { color: var(--text); font-weight: var(--fw-medium); }
/* The search field of a picker: one quiet field, no contour, the magnifier in
   the muted ink. */
.auth-overlay.is-sheet .airport-field,
.auth-overlay.is-sheet .config-field {
    min-height: 40px;
    border: 0;
    border-radius: var(--radius-md);
    background: rgba(var(--text-rgb), 0.06);
    box-shadow: none;
}
/* The way in, as the sheet's action row at the foot of the list. */
.auth-overlay.is-sheet .picker-new-btn { margin: 0; }
/* The lists stand on the sheet's own ground: no box around them. */
.auth-overlay.is-sheet .airport-dropdown,
.auth-overlay.is-sheet .config-dropdown,
.auth-overlay.is-sheet .phenomena-dropdown {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
}
.auth-overlay.is-sheet .route-wp-input {
    min-height: 40px;
    border: 0;
    border-radius: var(--radius-md);
    background: rgba(var(--text-rgb), 0.06);
    box-shadow: none;
}
/* The filter chips above the airport list keep their run, on no rule. */
.auth-overlay.is-sheet .airport-filters { border-bottom: 0; padding-bottom: 2px; }
.auth-overlay.is-sheet .config-empty-option { padding: 12px 0; }
/* A picker marked .picker-in-panel is opened by its card's "+", so its field
   never shows in the card — only in the panel. Airports keeps its tags there
   (that is what the card is for); WX Minima has none, so the whole thing steps
   out of the card's flow and its cards below close the gap. */
.picker-in-panel .airport-field,
.picker-in-panel .picker-new-btn { display: none; }
.picker-in-panel.config-multiselect { display: none; }
.field-overlay .picker-in-panel .airport-field { display: flex; }
.field-overlay .picker-in-panel .picker-new-btn { display: flex; }
.field-overlay .picker-in-panel.config-multiselect { display: flex; flex-direction: column; }
/* The search field in the panel: the near-white fill on a hairline. Bare in
   the card, it takes the flat --bg of the .settings-card ground it sits on;
   the panel's ground is not that one, so the search box needs its own edge to
   read as a field there. */
.field-overlay .picker-in-panel .airport-field,
.field-overlay .picker-in-panel .config-field {
    border: 1px solid var(--soft-line);
    background: var(--surface-2);
}
/* Airports: the tags of what is already picked read before the list to pick
   from, not after it — the panel is tall enough that they would otherwise sit
   below the fold. The field row is a wrapper around the field and its list, so
   it gives up its box (display: contents) and all three order freely. */
.field-overlay .airport-multiselect {
    padding-bottom: 0;
    display: flex;
    flex-direction: column;
}
.field-overlay .airport-field-head { display: contents; }
.field-overlay .airport-chips { order: 1; }
.field-overlay .airport-dropdown { order: 2; }
/* WX Minima reads in that same order: search, then what is loaded, then the
   list to load from, and last the way out of it ("New user WX minima"). */
.field-overlay .config-chips { order: 1; margin-top: 10px; }
.field-overlay .config-dropdown { order: 2; }
.field-overlay .config-multiselect .picker-new-btn { order: 3; }
.field-overlay .airport-dropdown,
.field-overlay .config-dropdown,
.field-overlay .phenomena-dropdown {
    position: static;
    width: 100%;
    margin-top: 10px;
    box-shadow: none;
}
.field-overlay .param-row.phenomena .phenomena-dropdown { width: 100%; }
/* A phenomena row with its list unfolded under it: the row's own "remove this
   parameter" × keeps to the field's line instead of floating at the middle of
   the list. Every other row is a single line and keeps its own alignment. */
.field-overlay .param-row.phenomena:has(.phenomena-dropdown:not([hidden])) { align-items: flex-start; }
.field-overlay .param-row.phenomena:has(.phenomena-dropdown:not([hidden])) > .danger-btn { margin-top: 4px; }
.field-overlay .airport-list,
.field-overlay .config-list,
.field-overlay .phenomena-dropdown { max-height: min(52vh, 24rem); }
/* The search box is the point of the panel: expanded whatever the pointer, and
   whether or not it holds focus. */
.field-overlay .airport-search,
.field-overlay .config-search {
    flex: 1 1 24px;
    min-width: 24px;
    width: auto;
    padding: 5px 8px;
}
.overlay-header .auth-title { margin: 0; flex: 1 1 auto; }
/* The close rides the header as a round icon button, the same control the card
   heads and the schedule rows use — the cross is its icon, not a text glyph. */
/* The overlay close IS the briefing header's settings disc, same class and all.
   That class is hidden by default (it is the
   mobile-only sidebar toggle in its own home), so the display is restated here.
   The size comes from the same scoped token override the header row uses — a
   panel's title bar is its primary row too, and deserves the same tap target. */
.overlay-header { --round-btn-size: 36px; }
.overlay-close { order: 2; flex: none; display: inline-flex; }
/* THE CLOSE DISC (owner's call, planche 2, 2-A): the PDF disc's dress — a
   hairline ring on no ground, no shadow, the cross in the muted ink. The map's
   floating chrome keeps its contour; a sheet's cross is not chrome. */
.round-icon-btn.overlay-close,
.round-icon-btn.overlay-dismiss,
.round-icon-btn.page-back,
.round-icon-btn.panel-close,
.round-icon-btn.panel-more {
    /* Fermer, revenir, le menu du panneau : des bulles de verre fin au liseré
       d'Apple (planche HABILLAGE B.4). L'anneau en inset, jamais en bordure : la
       zone de contact reste 44 (§ 5.6). */
    border: 0;
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
    color: var(--muted);
}
@media (hover: hover) {
    .round-icon-btn.overlay-close:hover,
    .round-icon-btn.overlay-dismiss:hover { background: var(--hover-bg); }
}
/* L'ICÔNE SEULE D'UN BOUTON EST EN --muted (décision du propriétaire, § 8.13) :
   elle est tout ce qui dit l'action, et --icon (1,8:1 en clair, 2,0:1 en
   sombre) tombait sous le 3:1 d'un graphique d'interface. Les autres glyphes du
   chrome restent de la matière — têtes de champ, séparateurs, et les marques de
   fin de ligne (chevron, « + ») même seules dans leur bouton : le nom de la
   ligne dit l'action. */
.overlay-dismiss svg { fill: none; stroke: var(--muted); stroke-width: var(--icon-stroke); stroke-linecap: round; }
.overlay-close svg { fill: none; stroke: var(--muted); stroke-width: var(--icon-stroke); stroke-linecap: round; }
/* Every overlay title reads at a medium (500) weight in the app font, not the
   heavy Arial Black title stack (which has no lighter weight). */
.overlay-header .auth-title {
    font-family: var(--app-font);
    font-weight: var(--fw-medium);
}

/* Settings overlay: a compact panel (opened from the account menu) holding the
   user's display timezone; reuses the auth-panel shell, the sticky overlay
   header and the schedule tz picker. */

.schedules-quota { margin: 0 0 8px 4px; color: var(--muted); font-size: var(--fs-md); }

/* One card per briefing, stacked with the page's own air between them. */
.schedules-list { display: flex; flex-direction: column; gap: var(--card-gap); }

/* The configuration's own square then its name, in the page's ink (planche 20):
   the legend's mark, so one colour means one configuration wherever it is read.
   The tinted pill it replaced belonged to the Report, where a fill means a
   broken minimum. */
.config-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--fs-md);
    color: var(--ink);
}
.config-badge i { width: 9px; height: 9px; border-radius: 2px; flex: none; }
/* A configuration the briefing still names but that no longer exists. */
.config-badge-gone { color: var(--muted); font-style: italic; }
/* Two settings adopting the briefing at the same hour: said on both cards, in
   the app's status ink (--status-error) — never the weather's red, which is
   reserved for the weather (owner's call) — as a line rather than a box. */
.schedule-conflict {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin-top: 6px;
    color: var(--status-error);
    font-size: var(--fs-xs);
    line-height: 1.35;
}
.schedule-conflict-icon {
    width: 14px;
    height: 14px;
    flex: none;
    margin-top: 1px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Configs shown as wash/ink badges (no identity dot, matching the
   briefing-history configs), aligned with the other body lines under the
   title-bar text. */
.schedule-configs { display: flex; flex-wrap: wrap; gap: 6px; font-size: var(--fs-md); color: var(--muted); }

.schedule-runs { display: flex; flex-direction: column; gap: 6px; padding-top: 4px; border-top: 1px solid var(--row-line); }
.schedule-run {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    font-size: var(--fs-md);
    color: var(--muted);
}
.schedule-run-when { color: var(--heading); }
.schedule-run-status { padding: 1px 8px; border-radius: var(--radius-md); background: var(--surface-2); box-shadow: none; }
.schedule-run-sent { color: var(--status-ok); }
.schedule-run-failed, .schedule-run-blocked, .schedule-run-no_weather { color: var(--status-error); }
.schedule-run-error { flex: 1 1 100%; }
.schedule-run .history-open-btn { margin-left: 0; }

/* The whole form reads as its own card, nested inside the schedules overlay
   panel (which stays --bg) — no fill of its own (the overlay's --bg shows
   through), just a card shadow and rounding, set apart from the
   saved-schedules list above it. */
/* The schedule form is a plain column of white section cards
   (.settings-card.schedule-card-section) — Airports, WX Minima, Schedule
   settings, Briefing, Notifications — plus the title field and actions. */
.schedule-form {
    display: flex;
    flex-direction: column;
    gap: var(--card-gap);
    /* First thing under its own header now, so it takes the same top space as
       the Saved settings list does under the header of its page. */
    margin-top: 10px;
    padding: 0;
    background: transparent;
}
.schedule-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: var(--fs-md);
    color: var(--muted);
    min-width: 0;
}
/* Exactly the 42px height of the airport/config picker fields — a fixed
   height (not min-height) so iOS's taller intrinsic time input and the
   shorter select both land on the same line. 16px font keeps iOS from
   auto-zooming on focus. */
.schedule-field input[type="text"],
.schedule-field select {
    height: 42px;
    box-sizing: border-box;
    padding: 7px 12px;
    border: 0;
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--fs-lg);
    line-height: normal;
    box-shadow: none;
}

/* Two controls sharing one line — the Report card's Issue Time row — each
   taking its share of the row. */
/* THE ZONE LIST IS THE PAGE'S CARD (owner's ask): the search is a row of the
   card above it, and the zones stand on the card's own ground — the list has
   no shell, no ceiling and no scroller of its own, the page scrolls. */
.schedule-tz-options {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
/* Floating region header inside the timezone list — same sticky treatment as the
   airport field's country headers, on the list's ivory ground. */
.tz-group-header {
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
    padding: 6px 8px 3px;
    background: var(--bg);
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
}
.tz-group-header[hidden] { display: none; }

/* Custom View dropdown — a native <select> popup can't take the app's --bg
   list styling, so it's a button + listbox we control fully. The Time / Start /
   End fields use the same shell (see .schedule-time-* below). */
.schedule-select { position: relative; }
.schedule-select-button {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px;
    width: 100%;
    /* Same rounded-square field as the configuration rows and the Briefing
       filter fields (.param-row / .briefing-filter-body .wx-dropdown-btn). */
    min-height: calc(var(--control-h) + 6px);
    padding: 4px 10px;
    border: 1px solid var(--soft-line);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-md);
    cursor: pointer;
}
/* The solid triangle (.wx-drop-caret, shared with the WX filter pills) sits
   2px tighter on both sides than the shell's default: the gap before it and
   the padding after it (its right edge) — the sheet's chevron elsewhere keeps
   the shell's own spacing. */
.schedule-select-button:has(.wx-drop-caret) {
    gap: 2px;
    padding-right: 6px;
}
.schedule-select-option {
    width: 100%;
    padding: 2px 8px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
    text-align: left;
    cursor: pointer;
}
@media (hover: hover) {
    .schedule-select-option:not(:disabled):hover { background: var(--hover-bg); }
}


/* Custom time picker: the same dropdown shell, but the popup holds two
   scrollable columns (hours, minutes) side by side, plus an optional
   "Clear — all day" button (notify Start/End only). */
/* A select's picked value reads like every other field's — heading ink,
   medium — against its muted label; only a placeholder stays muted. */
.schedule-select-value {
    color: var(--heading);
    font-weight: var(--fw-medium);
}
/* Placeholder "--:--" shown when a notify Start/End time is left blank. */
.schedule-select-value.is-placeholder {
    color: var(--muted);
    font-weight: var(--fw-regular);
}

/* The schedule form's toggle and radio text reads in the heading ink, except the
   Show TAF/Watch TAF switches (WX-filter-level controls, not section
   headers or fields in their own right), which stay muted like their
   sidebar counterpart. */
#briefingCards .taf-switch-label,
#scheduleForm .taf-switch-label,
.settings-accordion .wx-dropdown-panel .taf-switch-label { color: var(--heading); }
.field-switch .taf-switch-track { order: 2; margin-left: auto; flex: none; }
/* THE TIME WINDOW, INLINE ON ITS ROW (owner's ask).
   It was a hairline day with five marks and its hours written above the ends
   they mark — three lines for one fact. One line is enough: the label, the rail
   in the middle, the hours at the right where every other value sits.

   ONE THICKNESS, NO MARKS. The pale track is the day and the filled bar is the
   window — the app's own switch, stretched over twenty-four hours, which is
   exactly what a window is: on or off at every hour of the day. The geometry
   (including the two bars a window across midnight comes out as) is decided in
   schedule-drawings.js and locked by tests; this only inks it. */
.schedule-window-row .sheet-k,
.schedule-days-row .sheet-k,
/* Une valeur ne se tronque jamais (docs/ui/grammaire.md, « Ce qui se tronque, et ce qui ne se tronque jamais ») : c'est le
   dessin qui cède la place, pas « Not set ». */
.schedule-window-row .sheet-v,
.schedule-days-row .sheet-v { flex: none; }
/* Le dessin part plus loin que la ligne de gauche (demande du propriétaire) :
   le libellé et la route ne se touchent plus. */
.schedule-rail { display: block; flex: 1 1 auto; min-width: 56px; margin-left: 12px; }
.schedule-rail-track {
    position: relative;
    display: block;
    height: 6px;
    border-radius: 999px;
    background: rgba(var(--text-rgb), 0.14);
}
.schedule-rail-bar {
    position: absolute;
    top: 0;
    height: 6px;
    border-radius: 999px;
    background: var(--text);
}

/* THE DAYS, ON THE SCHEDULE CARD'S ROW (owner's decision). The row carries the
   Days page's own discs at 24px instead of 30 — what is tapped is what is read —
   and only writes what a disc cannot say: nothing chosen.

   A day is a CUMULABLE answer, so it reads as the page's discs do (planche
   INTER I.7, I.10): no fill, a very light ring left out, the text ink's ring
   chosen. A month is thirty-one of them, which is a calendar and not a row, so
   it compresses to one line of dots, the chosen ones numbered. */
.schedule-days-drawing { display: block; flex: 1 1 auto; min-width: 0; margin-left: 12px; container-type: inline-size; }
.schedule-days { display: block; }
.schedule-days.is-week { display: flex; justify-content: flex-end; gap: 3px; }
.schedule-day {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    box-shadow: inset 0 0 0 1px rgba(var(--text-rgb), 0.14);
    color: var(--muted);
    display: grid;
    place-items: center;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
}
.schedule-day.is-on {
    color: var(--heading);
    font-weight: var(--fw-medium);
    box-shadow: inset 0 0 0 1.5px var(--text);
}
.schedule-days.is-month { display: grid; grid-template-columns: repeat(31, minmax(0, 1fr)); }
/* A day of the month: a light dot, a full one in the text ink when chosen, its
   number centred under it — wider than its column, so it overflows both sides. */
.schedule-mday { display: flex; flex-direction: column; align-items: center; }
.schedule-mday-dot {
    width: 4px;
    height: 4px;
    margin: 1.5px 0;
    border-radius: 50%;
    background: rgba(var(--text-rgb), 0.25);
}
.schedule-mday.is-on .schedule-mday-dot { width: 7px; height: 7px; margin: 0; background: var(--text); }
.schedule-mday-n {
    margin-top: 3px;
    color: var(--heading);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
}
/* Two numbers two days apart need about 7.5 px a day; under that, the tight
   one goes and its dot stays (monthDaysHtml marks it). */
@container (max-width: 239px) {
    .schedule-mday-n.is-tight { visibility: hidden; }
}


/* THE PROGRAMME AS A READING — the two rows the LIBRARY card carries (planche
   BRIEFING-RÉSUMÉ, form B; summaryRowsHtml): When and Sends, in words, the
   alerts' channels as their icons. */
.schedule-sum-row .sheet-v { font-variant-numeric: tabular-nums; white-space: nowrap; }
.schedule-sum-dot { color: var(--muted); font-style: normal; }
/* The two channels as their own icons: the Alerts card names both in full, so
   the card that only READS them can be short. */
.schedule-sum-chans { display: inline-flex; align-items: center; gap: 5px; vertical-align: -2px; }
.schedule-sum-chan { width: 15px; height: 15px; color: var(--text); flex: none; }
.schedule-sum-chan svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}


.day-circle-row { display: flex; justify-content: space-between; gap: 4px; flex-wrap: nowrap; }
.day-circle-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    justify-items: center;
    gap: 4px;
}
/* The days of a schedule: the app's own discs, smaller, bare on the card. */
.round-icon-btn.day-circle-btn {
    width: 30px;
    min-width: 30px;
    height: 30px;
    flex: 0 0 auto;
    border: 0;
    background: none;
    box-shadow: none;
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
}
.round-icon-btn.day-circle-btn-small {
    width: 28px;
    min-width: 28px;
    height: 28px;
    font-size: var(--fs-xs);
}

/* Every toggle in the schedules overlay is the app's one switch
   (.taf-switch). A switch standing INSIDE a filter panel keeps its short
   20px row; a switch that IS a row (the form's own, planche 19) takes the
   row's 44px and is left alone here. */
#briefingCards .taf-switch:not(.field-switch):not(.sheet-row),
#scheduleForm .taf-switch:not(.field-switch):not(.sheet-row),
.settings-accordion .wx-dropdown-panel .taf-switch { height: 20px; min-height: 20px; }
/* Inset to match the section title/toggle bar's own 8px horizontal padding,
   so every row inside a section — fields, the nested WX title, its toggles —
   sits indented under the bar above it instead of flush with the form edge. */

/* Notification parameter filters: the switches that scope which breaches (and
   the TAF) raise a notification. Same on/off switch as elsewhere, wrapped in a
   compact row under a muted label. */

.schedule-note { margin: 0; color: var(--muted); font-size: var(--fs-xs); }

/* Schedule config dropdown reuses the settings config picker but drops the
   per-config delete button, so its options are one column narrower. */
#scheduleConfigList .config-option {
    grid-template-columns: minmax(0, 1fr) 26px;
}
/* THE LIST OF SELECTED CONFIGURATIONS, keyed on the class BOTH containers wear
   (owner's rule: the form is exactly the Briefing page). It used to be keyed on
   the form's own container class against the page's id, which is how the two
   ended up with different room and different type for the same rows. */
.config-blocks:empty { display: none; }
.settings-card > .config-multiselect:has(+ .config-blocks:not(:empty)),
.settings-card > .config-field-row:has(+ .config-blocks:not(:empty)) { margin-bottom: 12px; }
/* The parameters' compact type, on both surfaces: the page took it from its
   .settings-content scope, which never reached the form. */
.config-blocks .param-value,
.config-blocks .block-title,
.config-blocks .phenomena-chip,
.config-blocks .phenomena-option,
.config-blocks select { font-size: var(--fs-md); }

/* The confirm dialog: a short panel, not a page — it holds one question and
   its two answers, so it takes only the room they need. */
.confirm-panel { max-width: 420px; padding-bottom: 18px; }
.confirm-text { margin: 0 0 4px; color: var(--text); font-size: var(--fs-md); line-height: 1.45; }

/* Which function(s) a scheduled item carries, as the muted icons the form's own
   section heads use — the document for Briefing, the bell for Notifications.
   They sit right behind the schedule's title, each on its OWN neutral --bg disc
   — the same quiet fill as the notifications button in the footer. The disc is
   the icon itself: 6px of padding inside a square box (global border-box), so
   the 16px glyph keeps its size and the box comes out round. A schedule with
   neither function emits no wrapper at all (scheduleCardHtml). */

#schedulePushBtn { background: var(--soft-line); }

/* Desktop only: drop the border on the METAR/TAF report cards and on every
   configuration card (sidebar + schedule form) — mobile keeps them as-is.
   METAR/TAF cards keep a near-white fill; configuration cards go fully
   transparent. Placed last so it wins the cascade over the same-specificity
   base rules defined earlier in this file. */
/* LE SURVOL, UN SEUL DESSIN DANS TOUTE L'APP (docs/ui/grammaire.md, « Les états ») :
   --hover-bg sur ce qu'on vise, et seulement sous @media (hover: hover) — au
   doigt, un survol reste collé après la touche. Aucune couleur, aucun trait,
   aucune opacité : un fond, et un seul. Ce qui est plein (le bouton primaire)
   n'a pas de survol. */
@media (hover: hover) {
    .round-icon-btn:hover { background: var(--hover-bg); }
    /* WX-filter rows (sidebar and schedule form): the label darkens on hover
       — the row wash itself comes from .wx-param-row:hover. */

    .block-menu-item:hover { background: var(--hover-bg); }
    .icon-btn:hover { background: var(--hover-bg); }
    .danger-btn:hover { background: var(--hover-bg); }
    /* Text buttons turn their label to the heading ink on hover. */
    .small-btn:hover,
    .danger-btn:hover,
    .block-menu-item:hover,
    .view-mode-btn:not(.active):hover { background: var(--hover-bg); }
}

@media (min-width: 841px) and (min-height: 501px) {
    /* Desktop: airports keep the same borderless, unfilled treatment as
       mobile — just wider side padding and a bigger gap between cards. No
       contour at the airport level at all; a stale report says so on its own
       source line (.rg-stale), never by outlining a block. */
    .airport-card:first-child { padding-top: 16px; }
    /* Briefing-history date inputs: text-ink picker chrome rather than browser
       blue (the calendar button itself is hidden — the field opens it). */
    .history-date-input {
        accent-color: var(--text);
    }
    /* Desktop: the briefing column sits on the same ivory (--bg) ground as on
       mobile; only the settings sidebar keeps its near-white (--surface-2)
       panel fill (base .sidebar rule). */
    .app-shell {
        background: var(--bg);
    }
    /* Desktop (and up): with the extra room the header rides LOWER (more top
       padding, every view), so it floats clear of the edge instead of hugging
       it as it does on mobile. */
    .page-header {
        padding-top: calc(18px + env(safe-area-inset-top, 0px));
    }
    /* Every icon button (round header/menu buttons, parameter delete ×, the
       schedule "…" menu) loses its fill and shadow — flat glyphs only — and
       washes to a neutral (--hover-bg) fill on hover. */
    .round-icon-btn,
    .icon-btn {
        background: transparent;
        box-shadow: none;
    }
    @media (hover: hover) {
        .round-icon-btn:hover,
        .icon-btn:hover {
            background: var(--hover-bg);
        }
    }
    /* Claude-style: every round icon button keeps its full circle on desktop
       too (the former desktop-only rounded-square override is gone). */
    /* Account button vertically centered in the logo row on desktop, with no
       hover tooltip. */
    /* Styled hover tooltip for icon buttons: the native title is swapped to
       data-tip in JS, then shown here as a rounded white (--surface) chip
       with a soft border in muted text, below the button with its right edge
       aligned to the button's right edge. */
    .round-icon-btn[data-tip],
    .icon-btn[data-tip] {
        position: relative;
    }
    @media (hover: hover) {
        .round-icon-btn[data-tip]:hover::after,
        .icon-btn[data-tip]:hover::after {
            content: attr(data-tip);
            position: absolute;
            top: calc(100% + 6px);
            right: 0;
            z-index: var(--z-chrome);
            padding: 4px 8px;
            border-radius: var(--radius-sm);
            background: var(--surface);
            border: 1px solid var(--soft-line);
            color: var(--muted);
            font-family: var(--app-font);
            font-size: var(--fs-md);
            font-weight: var(--fw-regular);
            white-space: nowrap;
            pointer-events: none;
        }

        .airport-field:hover,
        .config-field:hover {
            background: var(--hover-bg);
        }
        .color-select-button:hover { background: var(--hover-bg); }
    }
    /* The header (.header-actions) is the container the WX filter pills size
       against: inline just right of the actions cluster when it's wide enough,
       own line when not — keyed off the header's real width, not the viewport
       (which overshoots by the sidebar). See the @container rule at
       #briefingWxFilters. */
    .header-actions { container-type: inline-size; }
}


/* The Show TAF filters' inline/own-line switch is a container query on the
   header (see #briefingWxFilters), not a viewport breakpoint. This block is
   now only the briefing-history PDF button's wide-screen label treatment. */
@media (min-width: 1200px) {
}

/* ============ SETTINGS SECTION CARDS ============
   Each section is a card headed by a lead icon, a title and, where it adds, a
   "+". Every override is scoped under .settings-card or #configBlocks. This
   block sits late in the file on purpose: it wins all equal-specificity ties
   against the -bare rules above. Never give the card overflow:hidden or a z-index — the pick-lists overlay
   out of it and the sticky header/footer (z-index 90) vs dropdown (z-index
   110) order must hold. */
/* UNE CARTE = UN GROUPE FONCTIONNEL (docs/ui/grammaire.md, « Quatre familles d'écran »). LE FOND LA
   DESSINE, PAS UN CONTOUR (décision du propriétaire, planche CARTES, C1) : 3,5 %
   d'encre mêlés au sol — la page garde le sol du Report, et la nuit la carte
   reste sous la feuille (§ 3.3). Arrondi d'avant la refonte, aucune ombre. One
   treatment, one set of classes, on every page that holds a setting. */
.settings-card {
    /* Un voile posé dans le verre, sans contour (planche HABILLAGE B.8). */
    background: var(--glass-group);
    border: 0;
    border-radius: var(--radius-lg);
    padding: 12px 12px 14px;
}
/* Controls inside an OPEN filter panel keep the neutral fill: that panel is
   white, so a near-white field would vanish on it. */
.wx-dropdown-panel .schedule-select-button { background: var(--bg); }

/* ONE CARD HEAD FOR THE WHOLE APP (owner's call): 36px tall and no margin of
   its own — the hairline of the first row does the separating, exactly as on
   the Briefing page. It stood 36px only inside .sidebar, so every other card
   of the app — the account, the pages, the sheets — opened on a 16px head and
   its first row read cramped. */
.settings-card-head {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 36px;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--heading);
    font: inherit;
    text-align: left;
}
/* A CARD WHOSE HEAD IS ITS ROW: the switch that turns the whole card on
   stands beside the title, pushed to the far end (owner's ask). A card named
   SCHEDULE holding a row named Schedule said the same word twice. */
.settings-card-head > .taf-switch { margin-left: auto; padding-right: 0; }
/* Une tête qui porte une action a la hauteur d'une rangée (§ 7.2). */
.settings-card-head:is(button, :has(button, .taf-switch)) { min-height: 44px; }
.settings-card-title {
    margin: 0;
    font-family: var(--app-font);
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--heading);
    line-height: 1.2;
}

/* THE ROWS OF THE SHEET (planche 14, grammar D). A group is a card with a
   hairline ring; inside it every setting is one system row — 44px tall, a
   hairline over it, the name at the left in the app font, the value at the
   right in the muted ink, a chevron where a page opens. The controls the app
   already had (the setup field, the airport picker, the configuration cards,
   the filter fields, the schedule blocks) keep their markup and their wiring
   and take the row's dress here, and only here: the schedule form and the map
   panel keep their own. */
.sidebar .settings-card { padding: 8px 12px 6px; }
/* A head's own mark obeys the rows' rule too: bare, and at the same distance
   from the edge as every chevron under it (the hit zone is added below). */
.sidebar .settings-card-head .settings-card-add {
    border: 0;
    background: none;
    box-shadow: none;
}
.sheet-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 44px;
    margin: 0;
    padding: 0;
    border: 0;
    border-top: 1px solid var(--row-line);
    background: none;
    color: var(--text);
    font: inherit;
    /* La voix des réglages, sur toutes les surfaces : 13 px (décision du
       propriétaire). */
    font-size: var(--fs-sm);
    text-align: left;
}
.sheet-row.sheet-row-tall { align-items: flex-start; padding-block: 10px 8px; }
.sheet-row.sheet-row-tall .sheet-k { padding-top: 6px; }
/* LES RÉGLAGES PARLENT D'UNE SEULE VOIX (décision du propriétaire) : 13 px en
   graisse régulière ; tout ce qui est à gauche — libellé, verbe, « Cancel »,
   « Clear » — dans l'encre du texte, la valeur à droite en gris. Le gras est
   réservé au titre d'une carte et au nom d'un objet de la bibliothèque ; les
   cartes de minima gardent leur format. */
.sheet-k {
    flex: none;
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    color: var(--text);
    white-space: nowrap;
}
/* A label alone on its row (a source's credit) has the row's whole width:
   it wraps there rather than run off the card. */
.sheet-row > .sheet-k:only-child {
    flex: 1 1 auto;
    min-width: 0;
    white-space: normal;
    padding: 8px 0;
}
/* A VERB THAT NAMES ITS OBJECT (Save <name>, Cancel changes on <name>): the
   name is the half that must be read first, and a row label stays within
   14 px / 400–500 (docs/ui/grammaire.md, « Quatre familles d'écran ») — so the verb steps down to
   --fw-regular and the name keeps --fw-medium in the ink: the same contrast
   inside the range, never a bold. The name is the half that gives way when
   the row runs out of room, never the verb. On the muted row it stays muted:
   standing out inside a quieter row, not against it. */
.sheet-k:has(.sheet-k-name) {
    flex: 0 1 auto;
    min-width: 0;
    font-weight: var(--fw-regular);
}
.sheet-k-name {
    font-weight: var(--fw-semibold);
    color: var(--text);
}
.sheet-v {
    margin-left: auto;
    min-width: 0;
    font-size: var(--fs-sm);
    color: var(--muted);
    text-align: right;
}
/* The chevron is a chrome icon like every other (§ 8.13): the one stroke, in
   the matter, in the one box. The "+" is its TWIN: same size, same stroke, same
   ink — one family of marks at the end of a row, saying add or open by their
   drawing alone. */
.sheet-chev,
.sheet-plus-icon {
    flex: none;
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* A row that TAKES SOMETHING AWAY wears a cross, not a chevron: the chevron
   promises a door, and there is none — the legs simply go. Same hairline
   drawing as the chevron so the two read as one family. */
.sheet-cross {
    flex: none;
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
}
.sheet-k + .sheet-chev, .sheet-k + .sheet-plus-icon, .sheet-k + .sheet-cross { margin-left: auto; }
/* A DOWNLOAD READS ITS BAR (planche TÉLÉCHARGEMENT) : the row wraps, its
   figures on the line, the bar under them across the row — the ink on a 12 %
   ink track, the switch's matter; full and with its check once Completed. */
.sheet-row.offline-progress-row { flex-wrap: wrap; row-gap: 8px; padding-block: 12px; }
.offline-progress {
    flex: 1 0 100%;
    height: 4px;
    border: 0;
    border-radius: 2px;
    overflow: hidden;
    appearance: none;
    background: rgba(var(--text-rgb), 0.12);
    color: var(--text);
}
.offline-progress::-webkit-progress-bar { background: rgba(var(--text-rgb), 0.12); }
.offline-progress::-webkit-progress-value { background: var(--text); border-radius: 2px; transition: width 0.25s linear; }
.offline-progress::-moz-progress-bar { background: var(--text); border-radius: 2px; }
/* AN AIRAC ABOUT TO END (planche HORS-LIGNE) : its value in red, the words
   « in 2 days » or « Expired » saying it as well. */
.sheet-row-alert .sheet-v { color: var(--red); font-weight: var(--fw-semibold); }
.offline-done { display: inline-flex; align-items: center; gap: 6px; font-weight: var(--fw-medium); }
.offline-check {
    flex: none;
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--text);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
button.sheet-row, [role="button"].sheet-row { cursor: pointer; }
/* A second door on a row: the chevron is a button of its own (the Source
   sheet — the row picks, the chevron edits), the row's height as its target. */
.sheet-edit {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin: 0 -9px 0 auto;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}
.sheet-v + .sheet-edit { margin-left: 0; }
/* THE ACTIONS OF A ROW, IN A SHEET (planche 20): each verb a line led by its
   own glyph in the muted ink, the destroying one's word muted too — the app
   has no red outside a verdict. */
.action-sheet-rows { display: flex; flex-direction: column; }
/* A SHEET OF OPTIONS IS A BARE LIST (owner's decision, planche « Feuilles
   d'options », D): no card — each verb is a 44 px row, lined up with the
   sheet's title. A HAIRLINE PARTS TWO ROWS (owner's decision): a list in a
   sheet is never a column of loose words. */
.action-sheet-verbs { background: none; border-radius: 0; padding-inline: 0; }
.action-sheet-verbs > .sheet-row { border-top: 0; }
.action-sheet-verbs > .sheet-row + .sheet-row { border-top: 1px solid var(--row-line); }
.sheet-lead-icon {
    flex: none;
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* THE INNERMOST THING ON SCREEN. This sheet is never opened on its own: it
   rises FROM a row, and that row may itself be on a sheet (the chart's
   settings, on the overlay layer). Below that plane its arrows are unreachable
   — measured on the cross-section's Max altitude. So it takes the overlay
   layer too and, appended last to <body>, stands above every sheet the app
   has, which is exactly what "the value is chosen here and nowhere else" means. */
.auth-overlay.alt-sheet { z-index: var(--z-overlay); }
/* The same for a sheet opened FROM a map card (Settings' Flying, Final
   reserve, an inset box): on the sheet layer it opened under the card, out of
   sight, and the row seemed dead. Appended last, it stands above it. */
.auth-overlay:is(.point-sheet, .action-sheet) { z-index: var(--z-overlay); }
/* THE ALTITUDE WHEEL (owner's board, form A): the ONE way a height is chosen,
   wherever the app asks for one. A sheet's head names what is being set; under
   it the rungs roll, and the one framed in the middle IS the value — nothing
   to confirm. The two arrows at the right are the map's own capsule buttons:
   with gloves an arrow never misses, and a rung is never crossed by accident.
   The row's height and the window's are ONE number (--alt-row): the wheel is
   read through that window, and two heights would report a value nobody rolled
   onto. */
.alt-wheel {
    --alt-row: 44px;
    --alt-wheel-h: 264px;
    position: relative;
    height: var(--alt-wheel-h);
    margin: 0 -24px;
}
/* A BAND is two wheels under ONE window (owner's ask): a floor and a ceiling
   are one answer, and one above the other made a pilot scroll the sheet to see
   the pair he was setting. Each keeps its own arrows at its own right edge. */
.alt-wheel.is-split { display: flex; }
.alt-wheel-col { position: relative; flex: 1 1 0; min-width: 0; height: 100%; }
.alt-wheel.is-split .alt-wheel-col + .alt-wheel-col { border-left: 1px solid var(--soft-line); }
/* The two names, on the sheet's own ground above the wheels rather than inside
   them, where the fade would eat them. */
.alt-wheel-head {
    display: flex;
    margin: 0 -24px;
    border-bottom: 1px solid var(--soft-line);
}
.alt-wheel-head span {
    flex: 1 1 0;
    padding: 6px 0;
    text-align: center;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}
.alt-wheel-head span + span { border-left: 1px solid var(--soft-line); }
.alt-wheel-list {
    height: 100%;
    overflow-y: auto;
    scroll-snap-type: y mandatory;
    /* Enough room above the first rung and below the last for either to reach
       the middle: a ladder has a top and a bottom, and both must be pickable. */
    padding: calc((var(--alt-wheel-h) - var(--alt-row)) / 2) 0;
    scrollbar-width: none;
}
.alt-wheel-list::-webkit-scrollbar { display: none; }
.alt-option {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: var(--alt-row);
    padding: 0;
    border: 0;
    background: none;
    scroll-snap-align: center;
    font-family: var(--app-font);
    font-size: var(--fs-lg);
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    cursor: pointer;
}
/* ONE SIZE ON EVERY WHEEL (owner's decision, planche COORD 7): the choice is
   told by its weight, its ink and the window, never by growing — a rung that
   grew made the wheel jump as it turned and pushed four columns off a phone. */
.alt-option[aria-selected="true"] {
    font-weight: var(--fw-semibold);
    color: var(--text);
}
.alt-wheel-window {
    position: absolute;
    left: 16px;
    right: 16px;
    top: calc((var(--alt-wheel-h) - var(--alt-row)) / 2);
    height: var(--alt-row);
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: rgba(var(--text-rgb), 0.06);
    pointer-events: none;
}
/* The rungs out of reach dissolve into the sheet rather than being cut by an
   edge: what is fading is leaving, what is framed is chosen. */
.alt-wheel-fade {
    position: absolute;
    left: 0;
    right: 0;
    height: 72px;
    pointer-events: none;
}
.alt-wheel-fade.is-top {
    top: 0;
    background: linear-gradient(var(--surface), rgba(var(--surface-rgb), 0));
}
.alt-wheel-fade.is-bottom {
    bottom: 0;
    background: linear-gradient(rgba(var(--surface-rgb), 0), var(--surface));
}
.alt-wheel-nudge {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    gap: 6px;
}
/* In a band the arrows hug the sheet's OUTER edges — the left column's on its
   left, the right column's on its right — so the middle, where the two values
   meet, stays clear. Kept inside, the left pair sat on the right column's
   figures (measured). */
.alt-wheel.is-split .alt-wheel-col:first-child .alt-wheel-nudge { right: auto; left: 8px; }
.alt-wheel-nudge .hour-step {
    border: 1px solid var(--soft-line);
    background: var(--surface);
    box-shadow: none;
}

/* THE POSITION SHEET (planche COORD): the position in its format's boxes with
   Paste beside it, the format's segmented, then the latitude's wheels over the
   longitude's — each a row of columns under one window, read as the value is
   written (N · 39° · 33 .74′). Shorter than a level's wheel: two of them
   stand in one sheet. The arrows stay off: four columns leave them no edge. */
/* Two sets of wheels and a field do not fit the 62% of a fitted sheet: it
   grows to the height of the app's large sheets (data-sheet="l"), no further. */
.auth-overlay.is-sheet.coord-sheet[data-sheet="fit"] > .auth-panel { max-height: 86vh; }
.coord-sheet .alt-wheel { --alt-wheel-h: 132px; }
.coord-sheet .alt-wheel-fade { height: 30px; }
.coord-sheet .alt-wheel-nudge { display: none; }
.coord-sheet .alt-wheel.is-split { justify-content: center; }
.coord-sheet .alt-wheel.is-split .alt-wheel-col + .alt-wheel-col { border-left: 0; }
/* THE COLUMNS SIT AS THE VALUE IS WRITTEN (planche COORD 6): each as wide as
   what it writes, the groups — hemisphere, degrees, minutes, seconds — 12 px
   apart, and the two pairs of one number 4 px apart, so they read as one
   number that turns by pairs. */
.coord-sheet .alt-wheel-col { flex: none; width: 48px; }
.coord-sheet .alt-wheel-col + .alt-wheel-col { margin-left: 12px; }
.coord-sheet[data-coord-fmt="dm"] .alt-wheel-col:nth-child(4),
.coord-sheet[data-coord-fmt="dd"] .alt-wheel-col:nth-child(n+3) { margin-left: 4px; }
.coord-sheet .alt-wheel-col:first-child { width: 32px; }
.coord-sheet[data-coord-fmt="dm"] .alt-wheel-col:nth-child(3),
.coord-sheet[data-coord-fmt="dd"] .alt-wheel-col:nth-child(2) { width: 40px; }
.coord-sheet[data-coord-fmt="dm"] .alt-wheel-col:nth-child(3) .alt-option,
.coord-sheet[data-coord-fmt="dd"] .alt-wheel-col:nth-child(2) .alt-option { justify-content: flex-end; }
.coord-sheet[data-coord-fmt="dm"] .alt-wheel-col:nth-child(4) .alt-option,
.coord-sheet[data-coord-fmt="dd"] .alt-wheel-col:nth-child(n+3) .alt-option { justify-content: flex-start; }
.coord-sheet[data-coord-fmt="dd"] .alt-wheel-col:nth-child(3) { width: 32px; }
.coord-card > .sheet-row:first-child { border-top: 0; }
/* THE BOXES (planche COORD 1, 3): the position as the format writes it, read
   at the size of a reading, a hemisphere a key of its own; the field under
   them is the keypad's, unseen. Typing, each box still owed shows its line. */
.coord-entry { position: relative; gap: 8px; }
.coord-mask {
    display: flex;
    flex: 1 1 auto;
    align-items: baseline;
    min-width: 0;
    font-size: var(--fs-read);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--text);
}
.coord-box { min-width: 0.6em; text-align: center; border-bottom: 1.5px solid transparent; }
.coord-mask.is-typing .coord-box.is-empty { color: var(--muted); border-bottom-color: var(--line); }
.coord-mask.is-typing .coord-box.is-next { border-bottom-color: var(--text); }
.coord-mask:not(.is-typing) .coord-box.is-empty { color: var(--muted); }
.coord-sign { color: var(--muted); }
.coord-gap { width: 10px; }
.coord-hemi {
    position: relative;
    z-index: calc(var(--z-page) + 1);
    margin-right: 3px;
    padding: 0 5px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: none;
    font: inherit;
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    color: var(--text);
    cursor: pointer;
}
.coord-keys { position: absolute; inset: 0; width: 100%; opacity: 0; }
.coord-entry > .small-btn { position: relative; z-index: calc(var(--z-page) + 1); flex: none; }
.coord-status { margin: 6px 2px 0; min-height: 1.4em; font-size: var(--fs-sm); color: var(--muted); }
.coord-status.is-refused { color: var(--status-error); }
.coord-format { display: flex; margin-top: 8px; }
.coord-format .view-mode-btn { flex: 1 1 0; }
.coord-axis-name {
    margin: 10px 2px 0;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}

/* THE WHEELS SET IN PLACE (the When page, owner's decision): inside the card,
   not across a sheet's width, and the rungs out of reach fade by a mask
   rather than into a sheet's ground, so they dissolve into the card's own. */
.schedule-window-wheels :is(.alt-wheel, .alt-wheel-head) { margin: 0; }
.schedule-window-wheels .alt-wheel-fade { display: none; }
.schedule-window-wheels .alt-wheel-list {
    mask-image: linear-gradient(transparent, var(--text) 72px, var(--text) calc(100% - 72px), transparent);
}

/* A row whose value is FILLED IN by its page (the waypoint editors' level, a
   filed route's ceiling): the host is not a box of its own — the reading and
   its chevron are the row's own children, exactly as on a row written whole. */
.route-alt-host { display: contents; }
/* The one verb under a wheel, on the sheet's own card: the parameter a minima
   drops, the time a schedule clears. */
.alt-sheet-foot { margin-top: 4px; }
/* What the choice acts on, said under the wheel (the cross-section's Max
   altitude and Corridor). */
.alt-sheet-note { margin: 8px 4px 0; color: var(--muted); font-size: var(--fs-sm); line-height: 1.45; }
/* THE FUEL'S KEYPAD (planche PERF C.1): in the wheel's place, the figure typed
   in twice the reading size, then twelve keys of the flight's contact size. */
.fuel-keypad-value {
    margin: 16px 0 4px;
    color: var(--text);
    font-size: calc(var(--fs-read) * 2);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    text-align: center;
}
.fuel-keypad-value small { color: var(--muted); font-size: var(--fs-md); font-weight: var(--fw-regular); }
.fuel-keypad-last { margin: 0 0 6px; color: var(--muted); font-size: var(--fs-sm); text-align: center; }
.fuel-keypad-keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 8px 0; }
.fuel-keypad-keys button {
    min-height: 56px;
    border: 0;
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--text);
    font-size: var(--fs-xl);
    font-weight: var(--fw-semibold);
}
/* THE LONG NAME UNDER THE HEAD: the sheet's title line is the app's card head
   (small caps, muted), so what the sheet is ABOUT — the aerodrome's name,
   the waypoint's, what a level applies to — is written under it in plain ink. */
.overlay-subtitle {
    display: block;
    margin-top: 2px;
    font-family: var(--app-font);
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    letter-spacing: normal;
    text-transform: none;
    color: var(--text);
}
/* A ROW THAT EXPLAINS ITSELF (planches 19-20): the name on top, the sentence
   that says what the setting IS underneath, in the muted ink. The whole reason
   the form could become rows at all — those sentences were paragraphs beside
   the controls, and a bare row would have dropped them. The key then has to
   stop being a nowrap island and take the row's width. */
.sheet-k:has(.sheet-sub) {
    flex: 1 1 auto;
    min-width: 0;
    white-space: normal;
    padding: 8px 0;
}
/* The value keeps its width whatever the sentence does: the sentence wraps,
   a "--:--" clipped to "-.." reads as a fault. */
.sheet-k:has(.sheet-sub) + .sheet-v { flex: none; }
.sheet-sub {
    display: block;
    margin-top: 2px;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-regular);
    line-height: 1.35;
}
/* A CAPTION SHORT ENOUGH TO FOLLOW ITS LABEL (owner's ask): "Notification ·
   On this device" is one breath, so it stays on the label's own line rather
   than opening a second one. Same ink, same size — only the line changes. */
/* A SWITCH THAT IS A ROW: one element wearing both grammars, so the state and
   the wiring stay the switch's while the shape is the row's. It keeps the
   track and the thumb of every other switch — only the box around it goes. */
.sheet-row.taf-switch {
    height: auto;
    padding: 0;
    border-radius: 0;
    font-size: var(--fs-md);
    color: var(--text);
}
.sheet-row.taf-switch .taf-switch-track { margin-left: auto; }
/* A switch row with a value (Keep offline · 6.0 MB): the value takes the
   free space, the track follows it at the row's own gap. */
.sheet-row.taf-switch .sheet-v + .taf-switch-track { margin-left: 0; }
/* A rail standing as a row's value: the segmented (Raw / Decoded), pushed to
   the right edge the way .sheet-v is. */
.sheet-row > .view-mode-toggle { margin-left: auto; }
/* A row whose only value is a rail goes with it: the rail says when it is not
   to be answered (NOTAM alerts off) by hiding itself. */
.sheet-row:has(> .view-mode-toggle[hidden]) { display: none; }
/* THE THEME IN THREE CARDS (owner's ask, planche THEME A): each card shows a
   small screen of the app in its theme. The screen is drawn in the tokens on
   screen, and the one of the OTHER theme is the same drawing turned over — so
   neither theme's colours are declared twice. Auto is cut on the diagonal. */
.theme-row { border-top: 1px solid var(--row-line); padding: 12px 0 4px; }
.theme-row > .sheet-k { display: block; margin-bottom: 10px; }
.theme-pick { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.theme-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-sm);
}
.theme-thumb {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    border-radius: var(--radius-md);
    overflow: hidden;
    box-shadow: 0 0 0 1px var(--line);
}
.theme-card.active .theme-thumb { box-shadow: 0 0 0 2px var(--heading); }
.theme-card.active .theme-name { font-weight: var(--fw-semibold); color: var(--heading); }
.theme-scr {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 8px;
    background: var(--bg);
}
:root:not([data-theme="dark"]) .theme-scr.is-dark,
:root[data-theme="dark"] .theme-scr.is-light { filter: invert(1) hue-rotate(180deg); }
.theme-scr + .theme-scr { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
.theme-scr-bar { width: 45%; height: 6px; border-radius: 3px; background: var(--text); }
.theme-scr-card {
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
    padding: 6px;
    border-radius: 5px;
    background: var(--card);
    box-shadow: 0 0 0 1px var(--contour);
}
.theme-scr-card > span { height: 4px; border-radius: 2px; background: var(--icon); }
.theme-scr-card > span:nth-child(2) { width: 55%; }
/* THE EMAIL LINES (planche SCHEDULE): on the card, the sentence and its switch,
   pushed right; on the Hours page, an all-day schedule's own hour, a value that
   opens its wheel. */
.schedule-email-row > .sheet-v,
.schedule-email-row > .schedule-select { margin-left: auto; }
.schedule-email-row > [hidden] { display: none; }
.schedule-email-row .schedule-select-button { width: auto; min-height: 44px; padding: 0; border: 0; background: none; font-size: var(--fs-sm); }
.schedule-email-row > .taf-switch { flex: none; margin-left: auto; padding-right: 0; }
/* The library, in one sheet: the areas over the routes, not beside them — the
   filter bodies lay their children out in a row, and two cards of rows read as
   two columns of a table that is not one. */
#scheduleSourceBody { display: flex; flex-direction: column; align-items: stretch; }
/* A time row: the field's own chrome goes, the button IS the row — its
   hairline included. Zeroing the whole border took that too, so Start and End
   stood joined with no rule between them, and none above Start (owner's ask). */
.schedule-select-button.sheet-row {
    justify-content: flex-start;
    min-height: 44px;
    padding: 0;
    border: 0;
    border-top: 1px solid var(--row-line);
    border-radius: 0;
    background: none;
    color: var(--text);
}
/* The wheels, in their sheet: two columns filling its width, each option a
   line of the sheet rather than a line of a popup. */
/* Airports: the tags are the row's value, wrapping under the name. */
.sidebar .sheet-row-tall .airport-multiselect { flex: 1 1 auto; min-width: 0; }
.sidebar .sheet-row-tall .airport-chips { margin: 0; padding: 0; }
/* AND THEY ARE CENTRED ON IT (owner: they were not): the label lost the 6px it
   took to clear a field that no longer stands in this row, the tags lost the
   10px that pushed them below it, and the row centres what it holds — one line
   of tags on the name's own middle, several centred as a block. */
.sidebar .sheet-row-tall:has(.airport-chip) { align-items: center; }
.sidebar .sheet-row-tall:has(.airport-chip) .sheet-k { padding-top: 0; }
.sidebar .sheet-row-tall:has(.airport-chip) .airport-chips:not(:empty) { margin-top: 0; }
/* NO DISC AROUND IT (owner's decision: the card already has its ring and the
   row its hairline — a third contour is one too many). The "+" is the bare
   mark at the row's END, at the chevron's own distance from the edge, so a row
   reads name-left, mark-right whether or not a field sits between them. Its
   tap zone is a 44px square behind it, drawn by nothing. */
.sidebar .sheet-plus,
.sidebar .settings-card-head .settings-card-add {
    position: relative;
    width: auto;
    height: auto;
    margin-left: auto;
    border: 0;
    background: none;
    box-shadow: none;
}
/* TWO AUTO MARGINS SPLIT THE ROOM IN TWO (owner: the name must sit against its
   chevron): the head's value already pushes itself right, so the mark after it
   takes the head's gap and nothing more — the name then ends 8px from the
   chevron, exactly as a row's does, instead of floating 62px short of it. */
.sidebar .settings-card-head .sheet-v + .settings-card-add { margin-left: 0; }
.sidebar .sheet-plus::before,
.sidebar .settings-card-head .settings-card-add::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
/* The Route's body carries NO rule of its own: its first row already draws the
   hairline under the head, and the two together read as a double filet. */
/* WX MINIMA — ONE ROW PER CONFIGURATION, ON THE TWO SURFACES (owner's decision:
   "A partout"). Its name in the identity's ink, its summary under, a chevron at
   the right — no mark before it, no card contour, no cap on the width. The
   colour comes off the card's --config-ink. This drawing was the sidebar's
   alone, which is how the Briefing page and the schedule form came to look
   like two different objects. */
/* L'IDENTITÉ EST DANS LE NOM, ET RIEN QU'EN LUI (propriétaire) : pas de
   pastille, pas de lavis, pas de trait — le nom de la configuration s'écrit
   dans sa couleur, le résumé commence au même pixel que lui. Et le filet
   sépare deux minima : une configuration est une ligne, et une ligne se sépare
   par son filet, comme partout ailleurs dans l'app. */
.config-blocks .config-block {
    max-width: none;
    margin: 0;
    padding: 4px 0 2px;
    border: 0;
    border-top: 1px solid var(--row-line);
    border-radius: 0;
}
.config-blocks .config-block.collapsed { padding-bottom: 0; }
/* The collapsed configuration IS a row of its card (§ 7.2 : toute opération
   interne est une .sheet-row) — its lead wears the class; the block keeps the
   hairline and the air above it, so the lead draws no second one, and the
   grip and the chevron keep the block's full height. */
.config-blocks .config-lead.sheet-row { align-items: stretch; gap: 0; border-top: 0; }
.config-head .block-title-text {
    display: inline-flex;
    align-items: center;
    padding: 0;
    background: transparent;
    color: var(--config-ink, var(--heading));
    font-weight: var(--fw-medium);
}
/* Le nom du bloc est le sélecteur de couleur, un mot de 15 px de haut : sa zone
   de contact déborde jusqu'à 44 px de haut, et vers la droite seulement. À
   gauche se tient la poignée, 22 px : un débord de 12 px y mangeait le centre
   de la poignée, et l'appui qui devait déplacer la carte ouvrait le sélecteur.
   Un nom plus court que 44 px élargit le bouton lui-même vers la droite, où il
   est seul : le carré de 44 px centré sur lui ne touche plus la poignée. */
.config-head .block-title-text { position: relative; min-width: 44px; }
.config-head .block-title-text::after {
    content: "";
    position: absolute;
    inset: -15px -12px -15px 0;
}
/* Le résumé part du bord du bloc, comme le nom au-dessus de lui. */
.config-summary { padding: 0 0 8px; }
/* THE FOOT IS THE ROWS' RHYTHM PLUS THE CARD'S OWN BREATH (owner's choice on a
   three-way board): a configuration ends 10px above the hairline that follows
   it — the summary's 8px and the block's 2px — and the last one adds the 6px
   every card keeps under its content, because what follows it is not a hairline
   but the card's ring and the white between two cards. Measured: 16px. */
.config-blocks .config-head { padding-top: 4px; }
/* Filters: each field is a row — name left, value right, the caret as the
   chevron. ON THE TWO SURFACES
   (owner's rule, measured): scoped to .sidebar, the form's copy of the very
   same card wore a 14px title with no rule above it against the page's 31px
   one, which is the same drift the minima card had. */
.briefing-filter-body { padding: 0; }
.briefing-filter-body .wx-filter-dropdowns { gap: 0; }
/* THE BRIEFINGS PAGE AND ITS FORM (owner's call, planche 16). The list is
   rows: a briefing is its switch, its name (the tap that opens it), its
   marks and its actions, on a hairline, with no card around it. The form's
   groups take the heads of the settings sheet, in the order one fills
   them. */
/* ONE BRIEFING, ONE CARD — the exception § 8.8 names: a briefing carries
   several lines of reading that are compared together (source, fields,
   minima, hours, days, report, alerts), so it keeps its card where the other
   libraries are rows. Its head is the name, and its switch — the active
   state's own drawing — at the head's right end like an alarm's. The card is
   the door to the sheet of verbs; the switch is the one thing that opens
   nothing. Each thing the briefing is, a system row of 44 px (§ 7.4); a value
   that is long wraps under its label rather than shortening. */
.schedule-head { display: flex; align-items: center; gap: 8px; }
.library-card { cursor: pointer; }
/* CHOIX C.5 — THE CARD A LIBRARY HAS LOADED WEARS THE CAPSULE, the whole card
   (owner's decision, form A): the tint laid OVER the card's own ground — put in
   its place, it read like every other card at night — and its name in bold. */
.library-card.is-chosen { background-image: linear-gradient(var(--chosen-bg), var(--chosen-bg)); }
.library-card.is-chosen .settings-card-title { font-weight: var(--fw-black); color: var(--heading); }
.schedule-head .settings-card-title { flex: 1; min-width: 0; }
.schedule-toggle { flex: none; margin-left: auto; }
/* AN AREA AND A ROUTE ARE READ IN THE SAME CARD (owner's decision): their
   library cards take these rows too, so the three summaries cannot drift. */
.library-card .sheet-row { flex-wrap: wrap; row-gap: 2px; padding: 8px 0; }
/* SAUF LA SOURCE : la pilule cède par son nom, et la ligne reste une ligne. */
.library-card .sheet-row:has(> .source-pill) { flex-wrap: nowrap; }
.library-card .sheet-row .sheet-v { white-space: normal; text-align: left; margin-left: auto; max-width: 72%; }
/* The catchment reads before the fields it pulled in, quiet, on the same line:
   it is the SETTING, the pastilles are what it found. */
.sheet-v-note { color: var(--muted); white-space: nowrap; }
.library-card .sheet-v .airport-chips,
.library-card .sheet-v .schedule-configs { justify-content: flex-end; }
.schedule-conflict .sheet-k { display: inline; }
/* THE WAYPOINTS PAGE: the sticky foot the borrowed pane wears in the add
   sheet simply flows here, its rows under the list. */
#waypointsOverlay .waypoints-card .overlay-footer { position: static; padding: 0; margin: 0; }
/* Switched off, the row keeps its name and its switch and drops what it would
   be doing — the same "collapsed" reading the card had. */
/* A PAUSED BRIEFING STILL SAYS WHAT IT IS (planche 20). It used to fold to its
   name alone, so the one row a pilot most wants to read before switching it
   back on — its airports, its minima, the hours it would run — was the one row
   that said nothing. Only the send history stays folded: that is a reading, not
   a description. */
.schedule-card-collapsed .schedule-runs { display: none; }
/* EVERY PAGE'S CARDS ARE THE SAME CARD (planches 16 and 20): the briefing
   form, the route editor, the four lists, the time zone, the two waypoint
   pages and the account alike — the base ring, a tighter inset. */
.auth-overlay.is-page .settings-card { padding: 8px 12px 6px; }
/* UNE SECTION DE LISTE, PAS UNE CARTE (§ 8.8) : la liste des rapports de
   l'historique est un titre et ses rangées sur le sol, sans fond ni retrait.
   Les groupes d'Account, eux, sont tous des cartes (décision du propriétaire). */
.auth-overlay.is-page .settings-card.is-list {
    background: transparent;
    box-shadow: none;
    padding-left: 0;
    padding-right: 0;
}

/* The name is a row of card 01, like every other row of every other card: the
   card's padding is its inset. Only its value is drawn a shade firmer — it is
   what the whole record is called. */
#scheduleForm .schedule-name-row input { font-weight: var(--fw-medium); }
/* THE MAP'S NAVIGATION SECTIONS ARE THE BRIEFING PAGE'S CARDS (owner's
   decision, docs/ui/carte.md, « Weather est un interrupteur, Navigation une feuille »): the same ground, radius and inset as
   .sidebar .settings-card; the foot's credit (`is-bare`) stays on the sheet's
   ground. The chart's settings stay sections on theirs. Either way a group's
   head is the card title (14/600) with no mark, the layer's switch on the
   same line at the right. A filter's sheet of kinds (#mapPickCard) is one of
   these cards too. */
#mapSettingsCard .map-layers-group,
#mapPickCard .map-layers-group,
#ctClassesCard .map-layers-group,
#profileSettingsBody .map-layers-group {
    padding: 8px 0 6px;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
}
#mapSettingsCard .map-layers-group:not(.is-bare),
#ctClassesCard .map-layers-group:not(.is-bare),
#mapPickCard .map-layers-group {
    padding: 8px 12px 6px;
    border-radius: var(--radius-lg);
    /* La carte du Briefing, le même voile dans le verre (planche HABILLAGE B.8). */
    background: var(--glass-group);
}
#mapSettingsCard .map-layers-group.is-bare,
#ctClassesCard .map-layers-group.is-bare { padding-inline: 4px; }
#mapSettingsCard .map-layers-title,
#ctClassesCard .map-layers-title { justify-content: space-between; gap: 12px; }
.map-layers-title > .taf-switch { flex: none; margin-left: auto; }

/* THE SOURCE SHEET (source-sheet.js): the library's two cards, like the map
   sheet's. The record the briefing reads wears the capsule (CHOIX C.1). */
.source-sheet-body .settings-card {
    padding: 8px 12px 6px;
    margin-top: 10px;
}
/* The chart's empty state: one row, the way in. */
/* The empty state's one verb is a row, led by its glyph. */
.route-profile-empty .sheet-row > svg:first-child {
    flex: none;
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.wx-filter-label .sheet-sub { white-space: normal; }
/* THE ACCOUNT AND THE HISTORY ARE PAGES OF ROWS (planche 16). The account's
   links are system rows in one group; the history's reports are rows on
   hairlines, their card gone. */
#authOverlay .auth-logo { margin-top: 8px; }
.account-version { display: block; margin: 16px 0 4px; text-align: center; }
#historyOverlay .history-list { gap: 0; }
/* A CONTROL STANDING IN A ROW IS SIZED BY WHAT IT SAYS (owner's ask). The
   Format rail hugs Raw and Decoded instead of running the row's free width —
   stretched, the two words floated in the middle of their own halves and read
   as a second column rather than as the row's value. It ends where every other
   row's value ends, and its words sit on the row's own line. */
#briefingDisplayCard .view-mode-toggle,
#sbDisplayCard .view-mode-toggle,
.schedule-days-type > .view-mode-toggle {
    display: inline-flex;
    width: auto;
    margin-left: auto;
}

/* WHAT A BRIEFING IS, in one node the form can borrow whole — and a node with
   no box of its own: `display: contents` puts its cards straight into whichever
   column holds it, so they take THAT column's air rather than carrying a second
   gap of their own. One measure between cards, said once, in both hosts
   (owner's ask: the same space between every card of the page). */
#briefingCards,
#schedulePlanCards,
/* …and every other mount a card or a row is drawn into: a mount is a PLACE, not
   a box. With one, what it holds stops being a child of the group around it, and
   every rule and every measure that reads that group's children loses it. */
#briefingFiltersMount,
#sbFiltersMount,
#briefingMinimaMount,
#sbMinimaMount,
#sbPlanMount,
#briefingSourceMount,
#sbSourceMount,
#briefingWindowBlock,
#sbWindowBlock,
#briefingReportMount,
#sbReportMount,
#briefingDisplayMount,
#sbDisplayMount,
#briefingWindowMount,
#sbWindowMount { display: contents; }

/* A CARD ENDS ON WHAT IT CARRIES (owner's decision): no floor of empty room
   under the last thing, whether that thing is a configuration, a row of
   airport pills or the last row of a route — each already carries its own
   space, exactly as a 44px row carries its own. Every card of the panel ends
   on the SAME 6px, measured under its last band, and no card and no
   body inside one adds a floor of its own on top of it. */
/* Nothing loaded yet, so the field holds a placeholder rather than a name. */
/* A search field wears the sign of what it does at its head, on the near-white
   ground and white contour every field of the app stands on. */
.route-search { position: relative; display: block; }
.route-search-icon {
    position: absolute;
    top: 50%;
    left: 11px;
    width: 15px;
    height: 15px;
    transform: translateY(-50%);
    fill: none;
    stroke: var(--muted);
    stroke-width: 2;
    stroke-linecap: round;
    pointer-events: none;
}
.route-search .route-wp-input {
    padding-left: 34px;
    /* Un champ de recherche en verre fin (planche HABILLAGE B.4). */
    background: var(--glass-thin);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    box-shadow: var(--glass-rim);
}
/* Empty, those two bodies keep no room of their own: the container's margin
   goes with its content, so the card's 6px foot is all that stands under the
   head — the same 6px a full card ends on. */
.settings-content #briefingAirportChips:empty,
.settings-content .config-blocks:empty { margin: 0; }
/* The form's Airports and WX Minima cards live by the same rule: empty, they
   are their title and nothing else — the chevron opens the picker either way,
   and the field only appears once it has something to hold. */
#scheduleForm .settings-card:has(#scheduleAirportChips:empty) .airport-multiselect,
#scheduleForm .settings-card:has(#scheduleConfigCards:empty) .config-field-row { display: none; }
#scheduleForm .settings-card:has(#scheduleAirportChips:empty),
#scheduleForm .settings-card:has(#scheduleConfigCards:empty) { padding-bottom: 0; }

/* A card's opener is a bare chevron rather than a filled disc: these cards —
   the sidebar's AND the schedule form's — open a full-screen panel, so the
   control points at what it opens instead of promising to add in place. One
   declaration: the filled-disc recipe it used to override is gone. */
/* Le chevron reste un FILET de 14 px sur 1,5 de trait (décision du propriétaire,
   planche 20) : le dessin ne bouge pas. Ce qui bouge est la zone de contact, qui
   passe de 2,2 mm à 44 × 30 px — large sur les côtés, où la ligne est libre, et
   serrée en hauteur, où la tête de carte voisine est à 11 px. */
.settings-card-add {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
}
.settings-card-add::after {
    content: "";
    position: absolute;
    inset: -8px -15px;
}
@media (hover: hover) {
    .settings-card-add:hover { background: var(--hover-bg); }
}
/* The whole title row is the tap target of its opener (main.js forwards the
   click); the pointer says so. */
.settings-card-head:has(.settings-card-add) { cursor: pointer; }
@media (hover: hover) {
    .settings-card-add:hover { background: var(--hover-bg); }
}

/* Leading search loupe: what every picker field now leads with, in the card and
   in the panel alike. */
.field-search-icon {
    display: block;
    width: 15px;
    height: 15px;
    flex: none;
    fill: none;
    stroke: var(--muted);
    stroke-width: 1.8;
    stroke-linecap: round;
}
/* Breathing room under the "WX minima name" field, before the cards it
   filters (or before the card's own bottom edge when there are none). One rule
   for both wrappers: the sidebar holds the picker bare, the schedule form wraps
   it in .config-field-row. Collapses with #configBlocks' own margin-top, so the
   gap is this value, not the sum. */
.settings-card > .config-multiselect,
.settings-card > .config-field-row {
    margin-bottom: 20px;
}
/* The field flattens to a plain search input row: near-white fill on a
   hairline, no shadow, with a quiet focus ring. */
.settings-card .airport-field,
.settings-card .config-field {
    min-height: 36px;
    padding: 4px 6px;
    border: 1px solid var(--soft-line);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    box-shadow: none;
    cursor: text;
}
.settings-card .airport-field:focus-within,
.settings-card .config-field:focus-within {
    outline: 2px solid var(--soft-line);
}
/* With no label left in the field, the search stays expanded at rest so its
   placeholder invites typing (the bare variants collapse it until focus).
   Metrics match the -bare :focus-visible rules above so nothing jumps on focus. */
.settings-card .airport-search,
.settings-card .config-search {
    flex: 1 1 24px;
    min-width: 24px;
    width: auto;
    padding: 5px 8px;
}
.settings-card .airport-multiselect { padding-bottom: 0; }
.settings-card .airport-chips:not(:empty) { margin-top: 10px; }

/* WX Minima cards nested in the section card keep the base .config-blocks
   .config-block look (no fill, white contour, no shadow) full width
   instead of centered/capped. */
/* NO MARGIN OF THEIR OWN (owner: the empty room under each configuration, and
   between the card's title and the first hairline): these 12px are what spaced
   the configurations back when each was a card with its own contour. They are
   rows now, and a row is separated by its hairline and nothing else — the very
   rule every .sheet-row of the app lives by. Measured: 12px under the head and
   12px under each configuration, gone. */
.config-blocks { margin-top: 0; }
.config-blocks .config-block { max-width: none; margin: 0; }
.config-blocks .config-body { padding: 6px 10px 8px; }
/* A card lifted into the picker overlay to be edited: the panel is the room it
   was given, so it takes all of it — and drops its own card frame there, since
   the panel already is the card. Its rows run to the panel's own padding, no
   lateral inset of their own. */
.field-overlay-body .config-block {
    max-width: none;
    margin: 0;
    padding: 0;
    border: 0;
}
.field-overlay-body .config-body { padding: 0; }
/* The identity rows of a card under edit (colour, title): system rows, the
   name left and the editable control at the right edge — the same reading as
   every value below them. */
.config-identity-row > .color-select { margin-left: auto; }
/* Anchored on the right, where the dot it opens from sits. */
.config-identity-row .color-menu { left: auto; right: 0; }
.config-identity-row .color-select-button { padding: 0; }
.config-identity-row .config-title-input {
    flex: 1 1 auto;
    min-width: 0;
    width: auto;
    padding-right: 0;
    text-align: right;
    color: var(--config-ink, var(--muted));
    /* The panel reads at one size: the lifted card leaves .settings-content
       behind, so .title-input's --fs-lg made this the only line in the overlay
       bigger than its own label. --fs-md is what the rest of the panel inherits
       there — labels, values and "+ Parameter" alike. */
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
}

/* ============ UNIFIED MAP VIEW ============
   Full-page map swapped in for the briefing by the header rail's Map
   segment (briefing-map.js). The map fills every pixel under
   the sticky header; its own chrome (layers card, radar transport bar, note)
   floats over it as Claude-style white cards. Chrome z-indexes sit above
   the engine's own controls (≤1000) but stay under the app overlays (z 200 is
   a separate stacking context on .auth-overlay). */

/* Map-open page state: the main column locks to the viewport, the briefing
   surfaces and the floating dock drop out, and the map section takes every
   remaining pixel. */
body.map-view-open .main {
    display: flex;
    flex-direction: column;
    height: 100dvh;
    overflow: hidden;
    padding-bottom: 0;
}
body.map-view-open #briefing,
body.map-view-open #statusBar { display: none; }
/* THE GRID OF BREACHES belongs to the Report: over the map there is no card to
   jump to — the fields are drawn where they are, with their own ICAO badge and
   their verdict rings — and the map's own legend already names what the ground
   is painted with. Two grids of the same colours for two different facts on
   one screen is the kind of ambiguity that costs in the air. */
body.map-view-open .briefing-summary { display: none; }
/* Map & WX views: the header and the floating footer dock both go fully
   transparent (no fill, no card shadow) and the map fills the whole column
   behind them — the view switcher and the dock's pills float directly over the
   map instead of sitting on a band/card. Each control
   keeps its own track/pill fill, so it stays legible over the map. Height of
   the floating header band, used below to drop the map's own top chrome clear
   of it. */
body.map-view-open {
    --map-header-h: calc(env(safe-area-inset-top, 0px) + 96px);
    /* Floating map controls inset so they line up with the briefing content edge
       instead of hugging the screen. .map-view bleeds a CONSTANT 24px each side
       (margin: 0 -24px) to run the map full-bleed, so a matching 24px inset lands
       every control exactly on .main's content edge — which is 12px from the
       viewport on phones and 24px on wider screens, tracking .main's own padding. */
    --map-gutter: 24px;
    /* The left column's rhythm, stated once: 8px between two discs (owner's
       decision, planche MAP 6), the header's own gap — two finger pads of 4px
       meet in it. */
    --map-stack-gap: 8px;
    /* The column's capsules (planche VOLERGO B.1, « A corrigé »): the discs of
       a group 16 px apart inside it — a flight contact zone's growth, so two
       56 px zones meet without overlapping — the capsule 4 px round them, and
       24 px from one group's discs to the next's. */
    --map-group-gap: 16px;
    --map-group-pad: 4px;
    --map-group-space: 24px;
    /* A disc of the column: the header's own disc, which it is — read by
       the foot's line, which stands outside the header. */
    --map-disc: 40px;
    /* Where the chrome stands from the screen's edge — the column's right,
       the foot line's left — the same 24 px as .main's padding, 12 on a
       phone (below). */
    --map-chrome-x: 24px;
    /* Where the foot's line stands (planche VOL A.3): on the band when
       it is there (--band-h, measured by profile-chart.js), on the
       navigation otherwise. */
    --map-foot-bottom: calc(var(--menu-band) + var(--band-h, 0px) + var(--strip-h, 0px) + 8px);
    /* The contact zone of a flight verb (planche VOL E.1, V-71; owner's
       decision D8): 56 px, grown around a 40 px disc whose drawing does not
       change. The pads are what the ::after rules read. */
    --flight-tap: 48px;
    --map-pad-x: calc((var(--map-disc) - var(--flight-tap)) / 2 - 1px);
    --map-pad-y: calc(var(--map-disc) - 2px - var(--flight-tap));
}
body.map-view-open.flying { --flight-tap: 56px; }
/* In flight the zoom's + reaches up half the air between it and recentre, as
   recentre reaches down the other half: two zones meet, neither steals the
   other's touches (owner's report). Its other edges and − grow as before. */
body.map-view-open.flying .map-zoom-btn:first-child::after {
    inset: calc((var(--map-disc) - var(--flight-tap)) / 2) min(0px, var(--map-pad-x)) 0;
}
/* On a phone the groups stand 16 px apart, where two 56 px flight zones meet
   (owner's decision, planche BOUTONS DE VOL B): the column holds above Terrain
   and Weather on a phone's height. On the ground too, so no group moves at Fly. */
@media (max-width: 599.98px), (max-height: 500px) {
    body.map-view-open .header-discs { --map-group-space: 16px; }
}
/* On a tablet with the height for it, in flight (owner's decision, planche
   BOUTONS DE VOL B): the column's zones grow to 96 px, 15 mm on an iPad mini,
   the drawing unchanged; the discs part so that two zones meet, the groups
   16 px further. The zoom's two halves stay one drawing, so + keeps the
   shared air above it. Terrain and Weather keep 56 px, far from any neighbour. */
@media (min-width: 37.5rem) and (min-height: 900px) {
    body.map-view-open.flying { --flight-tap: 96px; }
    body.map-view-open.flying .header-discs {
        --map-group-gap: calc(var(--flight-tap) - var(--map-disc));
        --map-group-space: calc(var(--flight-tap) - var(--map-disc) + 16px);
    }
    body.map-view-open.flying .map-weather-group {
        --map-group-gap: 16px;
        --map-pad-x: calc((var(--map-disc) - 56px) / 2 - 1px);
    }
}
/* Lift the header out of flow so the map canvas reaches the very top edge; it
   floats over the map, above the map's isolated stacking context and below
   the settings drawer. Higher specificity than the base + mobile/desktop
   .page-header background rules, so its --surface wins in every breakpoint. */
body.map-view-open .main { position: relative; }
body.map-view-open .page-header {
    position: absolute;
    top: 0;
    /* The header floats over the canvas, in the chrome's zone (planche
       PANNEAU B.11): right of the fixed Briefing's place. */
    left: var(--free-left);
    right: 0;
    margin-bottom: 0;
    /* NO GROUND UNDER THE HEADER OVER THE MAP (owner's decision, planche MAP):
       the map runs to the top edge, and the pill and the discs float on it
       with their own surface and contour, which read on any basemap. */
    background: transparent;
    border-bottom: 0;
    /* Empty header areas pass clicks/pans through to the map; only the actual
       control clusters stay interactive. */
    pointer-events: none;
}
body.map-view-open .header-actions,
body.map-view-open .header-top { pointer-events: none; }
body.map-view-open .header-tools,
body.map-view-open .header-discs { pointer-events: auto; }
/* Inset the header's control grid so neither the left cluster (settings) nor the
   right actions (Refresh) hug the screen edge. Padding on the grid keeps both
   sides symmetric (unlike padding on the justify-end .page-header). The inset
   matches .main's own padding — 24px, 12px on phones — so settings lines up with
   the map's left column, whose effective viewport inset is the same (the column
   sits at the constant 24px --map-gutter but inside .map-view's 24px bleed). */
body.map-view-open .header-top { padding-inline: var(--map-chrome-x); }
@media (max-width: 840px), (max-height: 500px) {
    body.map-view-open { --map-chrome-x: 12px; }
}
/* LA TABLETTE EN PORTRAIT est un format à part : la mise en page du téléphone
   est ce que E2 demande — barre basse, colonne unique — mais sa gouttière ne
   l'est pas. Entre 700 et 840 px il y a 744 px de large à occuper (un iPad
   mini) et 820 (un iPad Air) : la gouttière s'ouvre, la colonne respire, et
   rien d'autre ne bouge. Le seuil haut est celui de la bascule vers le
   bureau, pour que les deux ne se recouvrent jamais. */
@media (min-width: 700px) and (max-width: 840px) {
    :root { --main-gutter: 28px; }
    /* The map's left column stands on the header's own 12px — under the
       source pill, like the tools column. */
    body.map-view-open { --map-gutter: 12px; }
}

.map-view {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    /* Bleed .main's side padding, whatever it is at this width, so the map
       runs edge to edge — under the fixed Briefing too (planche PANNEAU
       B.11): the canvas runs under the panel, the chrome does not. */
    margin: 0 calc(-1 * var(--col-pad)) 0 calc(-1 * (var(--free-left) + var(--col-pad)));
    /* Trap the whole map subtree (the engine's own layers, floating chrome
       z 1090-1100) in its own stacking context, so the fixed sheets and
       pages of the app always paint above the map. Without this the panes
       resolve into the root context and bury them. */
    isolation: isolate;
}
.map-canvas {
    position: absolute;
    inset: 0;
    background: var(--bg);
}
/* The crosshair (owner's decision, planche AGE B.1): four strokes round an
   empty centre, 18 px, at the map's centre, under the floating chrome; a mark
   to aim with, never a target — the finger passes through to the map. */
.map-crosshair {
    position: absolute;
    /* The map's centre is the free zone's (planche METEO-RAIL R.9, LOT 6):
       the canvas's middle, moved by half the insets on its two sides
       (briefing-map.js). */
    left: calc(50% + (var(--map-inset-left, 0px) - var(--map-inset-right, 0px)) / 2);
    top: 50%;
    width: 18px;
    height: 18px;
    transform: translate(-50%, -50%);
    /* Its height is a plane of the map (CROSSHAIR_PLANE_Z, map-reading.js),
       written with the others, never a number here (§ 8.13). */
    pointer-events: none;
}
.map-crosshair[hidden] { display: none; }
/* THE CROSS-SECTION AT THE FOOT OF THE MAP (planche BANDEAU). Its height is a
   share of the screen (--strip-share, profile-chart.js), set on the band of
   the hour and the page bar; the map ends at its top edge — it is pushed,
   never covered (A.1) — and its centre, the crosshair's, moves up with it. */
body.map-strip-open { --strip-h: calc(var(--strip-share, 0.25) * 100dvh); }
body.map-strip-open .map-canvas,
body.map-strip-open .map-canvas.maplibregl-map { bottom: calc(var(--menu-band) + var(--band-h, 0px) + var(--strip-h)); }
body.map-strip-open .map-crosshair { top: calc(50% - (var(--menu-band) + var(--band-h, 0px) + var(--strip-h)) / 2); }
/* The strip open and the map short, the zoom put away (owner's decision,
   planche COLONNE C.2): only when the strip could not open at a quarter with it
   (fitStrip); a pinch zooms. */
body.map-view-open.strip-no-zoom #mapZoom { display: none; }
.map-strip {
    position: absolute;
    left: var(--map-inset-left, 0px);
    right: 0;
    bottom: calc(var(--menu-band) + var(--band-h, 0px));
    height: var(--strip-h, 0px);
    background: var(--surface);
    border-top: 1px solid var(--line);
    z-index: calc(var(--z-page) + 2);
    touch-action: none;
}
.map-strip[hidden] { display: none; }
/* The grip: a bar to see, a band of 24 px to take it by. */
.map-strip-grab {
    position: absolute;
    top: -12px; left: 0; right: 0;
    height: 24px;
    display: flex; align-items: center; justify-content: center;
    cursor: ns-resize;
    z-index: calc(var(--z-page) + 1);
}
.map-strip-grab span { width: 44px; height: 5px; border-radius: 3px; background: var(--muted); opacity: 0.6; }
.map-strip-full {
    position: absolute;
    top: 6px; right: 8px;
    width: 32px; height: 32px;
    display: grid; place-items: center;
    border: 0; border-radius: 50%;
    background: var(--surface); color: var(--text);
    z-index: calc(var(--z-page) + 1);
}
.map-strip-full svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.map-strip-chart { position: absolute; inset: 0; overflow: hidden; }
.map-strip-chart .route-profile-svg { width: 100%; height: 100%; }
/* Compact (A.11): two altitudes written at most, the lowest and the top one. */
.map-strip .rp-grid-label:not(:first-child):not(:last-child) { display: none; }
/* The route under the relief is its red alone here: the words stay on the page (A.12). */
.map-strip .rp-under-label { display: none; }
/* The part of the route the map is not showing, veiled (A.6); the map's centre
   as a dashed line (A.7). */
.map-strip-veil { fill: var(--surface); opacity: 0.55; pointer-events: none; }
.map-strip-cross { stroke: var(--text); stroke-width: 1.5; stroke-dasharray: 4 3; pointer-events: none; }
/* In flight, the aircraft at its GPS altitude, in the ownship's own inks (COUPE-VOL A.6, A.7). */
.map-strip-own { fill: var(--ownship-ink); stroke: var(--ownship-paper); stroke-width: 1.5; stroke-linejoin: round; pointer-events: none; }
.map-strip-level { stroke: var(--ownship-ink); stroke-width: 1.5; stroke-dasharray: 2 4; opacity: 0.8; pointer-events: none; }
.map-strip-pop {
    position: absolute;
    top: 6px;
    transform: translateX(-50%);
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    background: var(--surface);
    border: 1px solid var(--line);
    font-size: var(--fs-xs);
    line-height: 1.35;
    white-space: nowrap;
    pointer-events: none;
    z-index: calc(var(--z-page) + 1);
}
.map-strip-pop[hidden] { display: none; }
/* The zone the map leaves free, read once by syncMapInset (briefing-map.js):
   from --free-left to the edge of a panel at the right (LOT 6). */
.free-zone-probe { position: fixed; top: 0; left: var(--free-left); right: 0; height: 0; visibility: hidden; }
body.panels-side.right-open .free-zone-probe { right: var(--panel-w); }
.map-crosshair svg { display: block; width: 100%; height: 100%; overflow: visible; fill: none; stroke-linecap: round; }
.map-crosshair path { stroke: var(--map-crosshair-ink); stroke-width: 1.5; }
.map-crosshair path.map-crosshair-halo { stroke: var(--map-crosshair-halo); stroke-width: 3.5; }
/* MapLibre's own stylesheet is injected AFTER this one (map-engine.js loads it
   when a map opens) and sets `position: relative` on its container at the very
   same specificity — one class against one class. It therefore wins, the inset
   above stops sizing anything, and the map collapses to zero height: a live
   canvas inside a container nobody can see. Restated one notch stronger so the
   app's geometry holds whichever engine is running. Found in the browser while
   porting the shared map, étape 1b-1. */
.map-canvas.maplibregl-map {
    position: absolute;
    inset: 0;
    /* The app's plane numbers ride each MapLibre marker as a z-index — a briefed
       field's pill is 3500 — rather than living inside a stacking context of
       the engine's own. .map-canvas above already isolates, and measured in
       Chromium that IS enough: the pill stays
       under the airport card (1101). On the owner's iPhone it is not — the pills
       paint over the open card — and MapLibre marks every marker
       `will-change: transform`, which WebKit promotes to its own compositing
       layer. An explicit z-index makes the container a stacking context by the
       oldest rule in the book rather than by `isolation`, which WebKit
       implemented late and unevenly. Belt and braces, and it costs nothing:
       the map is meant to sit at the bottom of this view anyway. */
    z-index: var(--z-page);
}
/* Invisible left-edge band: catches the rightward back-to-briefing swipe
   before the map's pan does (bindViewSwipe, main.js). Above the engine's own
   layers, below the floating chrome. */
.map-edge-swipe {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    /* 18, not 20: the section is laid out 12px left of the viewport, so this
       band reaches x6 on screen — exactly where the zoom buttons' finger pad
       starts. At 20 it covered the pad's first two columns, and this element
       sits on the chrome layer, above it. Two pixels, but they were the two a thumb
       reaching for zoom-out lands on first. */
    width: 18px;
    z-index: var(--z-chrome);
    touch-action: none;
}

/* Layers card: the basemap radios + overlay switches, a white Claude card. */
.map-layers-card {
    position: absolute;
    top: 56px;
    right: 12px;
    z-index: var(--z-sheet);
    min-width: 180px;
    padding: 12px;
    border: 0;
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: 0 0 0 1px var(--contour);
    font-size: var(--fs-md);
}
/* Each section is its own card on the app's ivory ground: the data card grew
   long enough that a rule between groups stopped separating them, and a block
   reads as a block.
   The rows inside keep a WHITE list, and that is not decoration — an active row
   is `background: var(--bg)` (.wx-param-row:has(.active)), so on a --bg section
   it would be grey on grey and selection would vanish. That bug was found and
   fixed once already, in the airspace overlay this card replaced; the fix is
   the CONTAINER, never the row, and it moves here with the filters. */
.map-layers-group {
    padding: 8px;
    /* The settings type size (13px), the one every card of the Briefing page
       is set in — the panel was a pixel larger, all through. */
    font-size: var(--fs-md);
}
/* ONE AIR BETWEEN TWO CARDS, IN A SHEET TOO (owner's ask): the sections of
   the map's Navigation sheet, of the chart's settings and of the Source sheet
   are cards of the same grammar, so they take the app's one measure. Written
   on the PAIR, not on the card: the rules above give the first card of each
   sheet its top inset under the bar, and that inset is not a card gap. */
.map-layers-group + .map-layers-group,
#mapSettingsCard .map-layers-group + .map-layers-group,
.source-sheet-body .settings-card + .settings-card { margin-top: var(--card-gap); }
/* A block's own name INSIDE a section — the airspace card's Lower limit below /
   Type / ICAO class, the waypoints card's two switches. It wears the SIDEBAR's
   field label (.briefing-row-label, styled with the briefing filters below), so
   only its spacing lives here. One vocabulary for the whole app. */
.map-layers-subtitle { margin: 8px 4px 4px; }
/* The first thing in a section never needs a gap above it. */
.map-layers-group > .map-layers-subtitle:first-child { margin-top: 0; }
/* A card that opens on a switch with no title above it has no hairline either;
   nor does the first kind of a filter's sheet (#mapPickCard), which has none. */
.map-layers-group > .map-layers-switch:first-child,
#mapPickCard .map-layers-group > .wx-param-rows:first-child > .sheet-row:first-child { border-top: 0; }
/* NO BOX INSIDE THE BOX (owner's call): the rows stand on the card's own
   ground, like every row of the Briefing page. The framed, lighter container
   was there to keep a selected row's --bg wash visible; the rows that carry
   that wash are the tiles and chips, which have their own box, so a plain list
   of rows needs none. */
.map-layers-group .wx-param-rows { padding: 0; }
.map-layers-group .wx-param-row { min-height: 44px; padding: 0; border-radius: var(--radius-sm); border-top: 1px solid var(--row-line); }
.map-layers-group .sheet-row { border-top: 1px solid var(--row-line); }
/* The cross-section's sheet has no card title, so its first row needs no rule. */
#profileSettingsBody .route-profile-controls > .sheet-row:first-child { border-top: 0; }
/* The threshold menu's field, aligned with the lists beside it: its 2px white
   contour would vanish against the section's ground. Scoped to the MAP's own
   sheet by id: the cross-section's sheet borrows .map-layers-group for the
   ground, and a rule on the class alone — same specificity, written 6 600
   lines later — won over the row's own and painted Max altitude and Corridor
   as white fields. */
#mapSettingsCard .map-layers-group .schedule-select-button {
    background: var(--surface);
    border: 1px solid var(--soft-line);
}
/* A section's name is the SIDEBAR's card head (.settings-card-head +
   .settings-card-title), led by the symbol the map draws for that layer. The
   map used to grow its own uppercase, letter-spaced heading — one vocabulary
   too many — so nothing is declared here but the tighter margin a card inside
   a card wants. */
/* Airport card overlay: the full briefing card (renderAirportCard) shown over
   the map when an airport pill is tapped — it honours the page dock's
   view + METAR/TAF/NOTAM filters. The overlay fills the full-bleed map; its top
   padding clears the floating (transparent, out-of-flow) header band via
   --map-header-h and its bottom padding reserves the band the floating dock
   occupies, so the centred panel always sits between the header and the dock.
   A short card floats at content height; a tall one grows to the reserved
   height and scrolls inside. It is a sheet over the map's own chrome; the
   header and dock, in the root stacking context, still paint above the map. */
/* THE AIRPORT'S CARD ON THE MAP IS A SHEET (owner's call, planche 16): the
   same card as the Report's, up from the foot of the map, the map lit and
   live above it. The shell (.is-sheet) places it and draws its handle; only
   what is particular to it is said here. */
/* Named with its shell so the inset outweighs the shell's own padding: 0. */
.auth-overlay.map-card-overlay { z-index: var(--z-overlay); }
/* FROM THE FOOT OF THE SCREEN, AS HIGH AS ITS CONTENT, NO HIGHER THAN HALF
   (owner's call): a point, a beacon, a NOTAM take the lines they need and the
   map stays in view above them. An airport's card may take the whole page. */
#mapAirportCard > .auth-panel { height: auto; max-height: 50dvh; }
#mapAirportCard.is-airport > .auth-panel { max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 8px); }
.map-card-panel {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* Same ground as the briefing page, so the airport card reads identically
       wherever it opens: --bg behind, --surface-2 report blocks on top. */
    background: var(--bg);
    padding-left: 0;
    padding-right: 0;
}
/* The head: the Raw / Decoded rail at the left — it stands here only for an
   airport's card, the one thing it drives on the map — and the close disc every
   sheet wears at the right. */
.map-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin: 0;
    padding: 2px 12px 6px 20px;
}
.map-card-overlay:not(.is-airport) .map-card-rail { display: none; }
/* WHAT CLOSES THE FIELD OR ITS FUEL (owner's decision, planche ALERTES V3):
   under the name, one box per alert — the red rule on its left, the title in
   words beside it, so the colour is never alone —, nothing when nothing is wrong. */
.map-card-alerts {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0 20px 10px;
}
.map-card-alert {
    display: flex;
    flex-direction: column;
    gap: 1px;
    width: 100%;
    padding: 6px 12px;
    border: 0;
    border-left: 3px solid var(--red);
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
    background: var(--surface-2);
    font: inherit;
    font-size: var(--fs-sm);
    color: var(--text);
    text-align: left;
}
button.map-card-alert { cursor: pointer; }
.map-card-alert > span { color: var(--muted); font-size: var(--fs-xs); }
/* AN AIRPORT'S TABS (owner's call, planche « soulignés avec état », five since
   planche FICHE F1): words on one line over a rule, the chosen one in the title
   ink and underlined; Weather carries the field's minima rings, NOTAM its
   count. A word never wraps: the row takes the short names instead. */
.map-card-tabs {
    display: flex;
    gap: 22px;
    margin: 0 20px 8px;
    border-bottom: 1px solid var(--line);
}
.map-card-tab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    margin-bottom: -1px;
    padding: 0;
    border: 0;
    border-bottom: 2px solid transparent;
    background: none;
    font: inherit;
    font-size: var(--fs-md);
    font-weight: var(--fw-medium);
    color: var(--muted);
    white-space: nowrap;
    flex: none;
    cursor: pointer;
}
.map-card-tab[aria-selected="true"] { color: var(--heading); border-bottom-color: var(--heading); }
.map-card-tab-count { color: var(--muted); font-weight: var(--fw-regular); font-variant-numeric: tabular-nums; }
/* Under the tabs, for the two that read it, the open tab's head line (owner's
   decision); Raw / Decoded is the Display page's alone (RACC K.8). */
.map-card-rail { display: flex; align-items: center; gap: 12px; padding: 0 20px 6px; }
.map-card-lead { flex: 1 1 auto; min-width: 0; }
.map-card-lead > [hidden] { display: none; }
.map-card-lead > .notam-run { margin: 0; }
#mapAirportCard:is([data-tab="nav"], [data-tab="charts"], [data-tab="info"]) .map-card-rail { display: none; }
/* The card's own title line, lifted into the head beside the close disc: its
   code and its name, on the one line. */
.map-card-title { flex: 1 1 auto; min-width: 0; }
.map-card-title:empty { display: none; }
.map-card-title > .airport-title { margin: 0; padding: 0; }
/* THE ACTIVE SETTINGS UNDER A SHEET'S TITLE, AND UNDER THE NOTAM TAB'S RUN
   (owner's call, planche RÉGLAGES-LIGNE): the NOTAM scope line's own writing,
   read and not pressed. */
.card-settings { padding: 2px 0 0; }
.map-card-lead > .card-settings { padding-top: 4px; }
/* Its title gone up, a card's facts open the sheet or the tab: no rule over the first line. */
.map-card-body > .airport-card > .airport-facts:first-child,
.map-card-panel > .airport-facts:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* A tab's card starts under the rail, not a card's air below it. */
.map-card-panel > .airport-card { padding-top: 0; }
.map-card-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0 20px calc(12px + env(safe-area-inset-bottom, 0px));
}
/* The field's "show on map" pin is redundant on the map itself — dropped from
   the title line the head carries; the NOTAM rows' own pins stay live. */
.map-card-title [data-airport-map] { display: none; }
/* The layer cards borrow the app's own filter rows (.wx-param-row /
   .wx-filter-toggle, in the WX filters block above) rather than a recipe of
   their own: one format for one gesture, and one place to change it. Only the
   sizing and the type symbol need saying here — a row on a floating card is a
   finger target, so it keeps the WX rows' height rather than their density. */
.map-layers-group .wx-param-row { padding: 0 4px; border-radius: var(--radius-sm); }
.map-layers-group .wx-filter-toggle { min-height: 34px; gap: 8px; }
/* A layer switch reads as a FIELD, not as a control with a caption: its name on
   the left, its state on the right, against the section's full width — the way
   every row of the sidebar is read. The markup writes the label first
   (layerAllHtml, map-core.js) and the pair is spread here. */
/* THE PANEL'S ROWS ARE THE BRIEFING CARD'S ROWS (owner's call): 44px, a
   hairline over each, the name at the left and the track at the right. The map
   kept a 34px row of its own until this lot — one grammar of row for the whole
   app, whatever draws it. */
.map-layers-group .taf-switch {
    min-height: 44px;
    height: auto;
    padding: 0;
    color: var(--text);
    font-size: var(--fs-md);
}
.map-layers-switch {
    display: flex;
    width: 100%;
    justify-content: space-between;
    border-top: 1px solid var(--row-line);
}
.map-layers-switch .taf-switch-label { text-align: left; color: var(--text); font-size: var(--fs-sm); font-weight: var(--fw-regular); }
/* A line's ASIDE (the cloud's top on the Cloud type sheet): a fixed column
   before the switch, its head on the card's title line over it — the same
   width and the same right margin, the track's width plus the gap, so the
   words stand under their head. */
.sheet-aside, .sheet-aside-head {
    flex: none;
    width: 6.5rem;
    margin-left: auto;
    font-size: var(--fs-xs);
    color: var(--muted);
    text-align: left;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.sheet-aside { margin-right: 16px; }
.sheet-aside-head { margin-right: calc(28px + 16px); }
/* A switch with a DOOR beside it (User waypoints → manage). The switch gives up
   its full width so the link can follow its own track on the same line; every
   other switch in the panel keeps the full-width form, where the tracks read as
   one column down the card's right edge. */
.map-layers-switch-line { display: flex; align-items: center; }
.map-layers-switch-line .map-layers-switch { width: auto; flex: none; }
/* The link beside a label is not a row of its own: no block, no full width, and
   UNDERLINED rather than bold (owner's ask) — a link says it is a link by being
   underlined, and the weight belonged to a row heading it no longer is. */
.map-layers-link.is-beside {
    display: inline;
    width: auto;
    padding: 0 6px;
    font-weight: var(--fw-regular);
    text-decoration: underline;
}
/* A switch that narrows the one above it rather than adding a layer of its own
   ("Compulsory only" under "VFR reporting points"): stepped in by the width of
   its own track, so the eye reads the dependency instead of having to know it.
   Its label keeps the row's grammar, the Briefing's (docs/ui/carte.md, « Weather est un interrupteur, Navigation une feuille »): the step says the dependency, not a second ink. */
/* The hairline runs the CARD's full width, always: the row is whole and it is
   the NAME that steps in. Narrowing the row itself broke the rule into a
   staircase, which is not a hairline any more. */
.map-layers-switch.is-sub {
    width: 100%;
    padding-left: 20px;
    font-size: var(--fs-md);
}
.map-layers-group .airport-type-icon { flex: none; color: currentColor; }
/* A FLIGHT LAYER'S SYMBOL KEEPS THE INK THE MAP DRAWS IT IN (owner's decision):
   the symbol is the legend, so the field, the beacon, the obstacle and the
   reporting point read in their sheets exactly as on the chart — never as the
   row's own grey. */
.map-layers-group:not(.is-wx) .airport-type-icon { color: var(--chart-blue); }
/* A row's mark leads its name inside the switch's label — the glyph, the
   polygon's swatch or the ICAO class letter, then the word. */
.map-layers-switch .taf-switch-label { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
/* A group whose choice is one value off a long ladder carries the app's own
   select menu instead of a row per value (the airspace base threshold): the
   same control the cross-section's Max altitude field uses, so the two fields
   of one panel are picked the same way. Full width — a menu alone on its line
   has no reason to hug its longest label. */
.map-layers-menu { padding: 0 4px; }

/* L'ENCRE DIT LA FAMILLE (owner's ask) : every symbol on a weather card — the
   pilot's minima, the six layers, the cameras — is drawn in the secondary ink;
   the layers a flight is drawn with take the heading's. Two inks, two families
   of meaning, so "what the sky is doing" and "what is drawn on the ground" are
   told apart before a word is read — in neutral inks (docs/ui/carte.md, « Weather est un interrupteur, Navigation une feuille » ; docs/heliwx-brand-guidelines.md, « Brand Colors »).
   An unlit mark fades to the same 45% its neighbours' strokes are muted to
   rather than changing ink: the state is the strength, not the colour. */
.map-layers-group.is-wx .airport-type-icon { color: var(--muted); }
.map-layers-group.is-wx .taf-switch:not(.active) .airport-type-icon { opacity: 0.45; }

/* THE GROUND, AT THE HEAD OF THE LAYERS SHEET (owner's decision, planche
   COLONNE): its pictures, the relief switch and its credit, one card above the
   flight's layers. */
.map-basemap-rows:empty { display: none; }
.map-basemap-rows > * + * { margin-top: 8px; }
#mapBasemapRows + #mapSettingsBody { margin-top: var(--card-gap); }
/* The ground's credit under openAIP's, the page's foot read as one block
   (planche COLONNE A.3). */
#mapSettingsCard #mapGroundCredit { padding-top: 0; }
#mapGroundCredit .map-layers-credit { margin-top: 0; }
#mapGroundCredit:empty { display: none; }
/* THE PICKER'S FOOT (owner's decision): under the tiles, the data's credit on
   the left and where the picture comes from — the model and its run — on the
   right, in the list's own quiet voice. */
.map-wx-foot {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-top: 6px;
}
.map-wx-foot .map-layers-credit { flex: none; margin-top: 0; white-space: nowrap; }
.map-wx-source { margin: 0 4px 0 auto; color: var(--muted); font-size: var(--fs-2xs); text-align: right; }
/* A family's word in small capitals, without a mark (owner's decision,
   planche FMETEO W.3). */
/* The family's source after its word (owner's decision, planche COUCHES
   F.5): "Forecast · DWD ICON-EU 7 km · run 08 LT", in the quiet voice. */
.map-wx-family-src { font-weight: var(--fw-regular); letter-spacing: 0; text-transform: none; }
.map-wx-family {
    margin: 8px 6px 6px;
    color: var(--muted);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.map-wx-tile svg {
    flex: none;
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* FOUR SMALL TILES A LINE, one grid per family (owner's decision, planche
   FMETEO W1): Observed holds on one line. */
.map-wx-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.map-wx-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 7px 2px 6px;
    border: 0;
    border-radius: var(--radius-md);
    /* En verre fin, au liseré d'Apple (planche HABILLAGE B.10). */
    background: var(--glass-thin);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-drop), var(--glass-rim);
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-2xs);
    cursor: pointer;
}
/* A layer with nothing to draw on this route keeps its place, greyed; a press
   still lights it, and the legend says there is none. */
.map-wx-tile.is-empty { opacity: 0.45; }
.map-wx-tile[aria-checked="true"] {
    /* Allumée, elle garde son verre, cerclée de 2,5 px d'encre (planche HABILLAGE B.10). */
    box-shadow: var(--glass-drop), inset 0 0 0 2.5px var(--text), var(--glass-rim);
    color: var(--text);
    font-weight: var(--fw-semibold);
}
/* A GROUP INSIDE A FAMILY (owner's decision, planche ISOLEG A2): the
   cross-section's two iso-lines, one at a time, side by side on two of the
   grid's cells, in ONE thin outline whose word rides the trait. The outline
   stands outside the cells (padding + border = the -5 px margin), so its tiles
   keep the width of every other; the family's lines part wider for the word. */
.map-wx-tiles:has(> .map-wx-subgroup) { row-gap: 20px; }
.map-wx-subgroup {
    position: relative;
    grid-column: span 2;
    margin: -5px;
    padding: 4px;
    border: 1px solid var(--contour);
    border-radius: var(--radius-md);
}
.map-wx-subtiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; height: 100%; }
.map-wx-subword {
    position: absolute;
    top: -7px;
    left: 8px;
    margin: 0;
    padding: 0 4px;
    background: var(--surface);
    color: var(--muted);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    line-height: 12px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
/* THE LAYERS' SHEET (owner's decision, planche METEO-RAIL R.2): always a
   sheet, on the band's own edges, coming out from BEHIND the band and the
   navigation, which stay on top and live; its tiles end above the band
   (--band-h). The families of tiles, About and the cross on the first one's
   line (R.8), then the model's credit. */
.map-wx-pop {
    position: fixed;
    left: var(--free-left);
    right: 0;
    top: auto;
    bottom: var(--menu-band);
    z-index: var(--z-chrome);
    container-type: inline-size;
    padding: 18px var(--main-gutter) calc(var(--band-h, 0px) + 12px);
    border: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    /* En verre épais (planche HABILLAGE B.7). */
    background: var(--glass-thick);
    -webkit-backdrop-filter: var(--glass-thick-blur);
    backdrop-filter: var(--glass-thick-blur);
    box-shadow: var(--glass-drop-big), var(--glass-rim);
}
.map-wx-pop::before {
    content: "";
    position: absolute;
    top: 6px;
    left: 50%;
    width: 36px;
    height: 5px;
    margin-left: -18px;
    border-radius: var(--radius-pill);
    background: var(--contour);
}
/* About and the cross on the first family's line, at its right (planche
   METEO-RAIL R.8): no title over the families. */
.map-wx-head {
    position: absolute;
    /* Centred on the first family's word: the sheet's 18 px, the word's 8 px of
       air, half its line, less half the cross's disc. */
    top: 19px;
    right: var(--main-gutter);
    display: flex;
    align-items: center;
    gap: 8px;
}
.map-wx-group { padding: 0 0 4px; }
.map-wx-group > .map-wx-family { margin-top: 8px; }
/* Wide enough, every family on one line, side by side (R.2), the tiles 84 px
   wide (planche BRIEFING-COMPACT L2: the eleven hold beside the open Briefing). */
@container (min-width: 660px) {
    .map-wx-body { display: grid; grid-auto-flow: column; justify-content: start; column-gap: 18px; }
    .map-wx-body .map-wx-tiles { grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: 84px; }
}
/* ...and when that line does not hold them (fitLayerSheet, a measure), one
   family per line: Observed, then Forecast under it (planche BRIEFING-COMPACT L2). */
.map-wx-pop.map-wx-stacked .map-wx-body { display: block; }
.map-wx-pop.map-wx-stacked .map-wx-group + .map-wx-group { margin-top: 12px; }
/* THE GROUNDS AS PREVIEWS (owner's decision, planche MAP 7, B1; COLONNE A.3):
   three equal pictures on one line, twice as wide as tall, the name under
   each, the laid one ringed in the reading ink; an empty one wears the recess
   until its picture lands. */
.map-ground-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; padding: 2px; }
.map-ground-tile {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: var(--fs-xs);
    cursor: pointer;
}
.map-ground-tile img {
    display: block;
    width: 100%;
    aspect-ratio: 2;
    object-fit: cover;
    border-radius: var(--radius-md);
    background: var(--surface-2);
    box-shadow: inset 0 0 0 1px var(--contour);
}
.map-ground-tile[aria-checked="true"] { color: var(--text); font-weight: var(--fw-semibold); }
.map-ground-tile[aria-checked="true"] img { outline: 2px solid var(--text); outline-offset: 2px; }
/* A reading the pilot's limits can judge carries its ramp on the line under
   its own: the segmented, stepped in, as the row's value (§ 8.4, § 8.6). */
.map-layers-ramp-row { justify-content: flex-end; border-top: 0; }

/* WX transport bar: the box the stepper stands in, inside the capsule's bottom
   storey. It grows to fill the row between the two discs and carries no ground
   of its own — the capsule carries it for everything down there. */
.map-radar-bar {
    display: flex;
    align-items: center;
    flex: 1 1 auto;
    min-width: 0;
    min-height: var(--control-h);
    padding: 0;
}
/* THE RADAR'S BAR (owner's decision, planche RADAR2 T): the walk through the
   images as a line, right of the hour and its arrows, out to the cameras'
   disc. The seen images in the quiet ink, the estimated ones in the lighter
   grey beyond — plain, no dashes; a tick where "now" is; the hand a THIN disc
   on the image drawn. The whole 40 px height takes the finger. */
.map-clock-row > .map-radar-bar:has(> .radar-track) { justify-self: stretch; }
.radar-track {
    /* Its width is the measure's (planche TEMPS T.4, T.5: layoutCapsule,
       profile-chart.js), never its own: 0 until measured, hidden under the
       floor. */
    --track-w: 0px;
    flex: none;
    width: var(--track-w);
    height: 40px;
    display: flex;
    align-items: center;
    padding: 0 10px 0 4px;
    touch-action: none;
    cursor: pointer;
    --at: 0;
    --seen: 1;
}
.radar-track-line {
    position: relative;
    flex: 1;
    height: 3px;
    border-radius: 2px;
    background: var(--soft-line);
}
.radar-track-seen {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: calc(var(--seen) * 100%);
    border-radius: 2px;
    background: var(--muted);
}
.radar-track-now {
    position: absolute;
    top: -4px;
    left: calc(var(--seen) * 100%);
    width: 1px;
    height: 11px;
    background: var(--muted);
}
.radar-track-hand {
    position: absolute;
    top: 50%;
    left: calc(var(--at) * 100%);
    width: 12px;
    height: 12px;
    margin: -6px 0 0 -6px;
    box-sizing: border-box;
    border-radius: 50%;
    background: var(--surface);
    border: var(--icon-stroke) solid var(--text);
}
/* ONE DOT PER INSTANT (planche TEMPS T.3, FIL A.1), placed at its hour, in
   the matter's grey and the same either side of the ring: the line is never
   painted (FIL A.3). The present's is black (FIL A.2), drawn after the ring
   so it stays seen inside it. The radar keeps its line, without dots (A.4). */
.radar-track-ticks { position: absolute; inset: 0; }
.track-tick {
    position: absolute;
    top: 50%;
    left: calc(var(--x) * 100%);
    width: 5px;
    height: 5px;
    margin: -2.5px 0 0 -2.5px;
    border-radius: 50%;
    background: var(--icon);
}
/* An hour the route breaks a minimum (planche CASSE B). */
.track-tick.is-breach { background: var(--red); }
.track-now {
    position: absolute;
    top: 50%;
    left: calc(var(--x) * 100%);
    width: 5px;
    height: 5px;
    margin: -2.5px 0 0 -2.5px;
    border-radius: 50%;
    background: var(--text);
    pointer-events: none;
}
.track-now[hidden] { display: none; }
/* THE BOLTS OF THE RADAR (owner's decision, planche FOUDRE L.3): Meteosat's
   flashes of the last fifteen minutes, one filled bolt each in the text ink
   with a halo of the surface, still; one that lands while the pilot watches
   flashes once (FOUDRE L.9). Not a target: nothing to press, nothing to read. */
.map-bolt {
    width: 16px;
    height: 16px;
    transform: translate(-50%, -50%);
    pointer-events: none;
}
/* Its age, set on each bolt (--bolt-alpha, planche FOUDRE L.8): the older, the
   paler. */
.map-bolt svg {
    opacity: var(--bolt-alpha, 1);
    width: 100%;
    height: 100%;
    fill: var(--text);
    stroke: var(--surface);
    stroke-width: 1.6;
    paint-order: stroke;
}
/* The flash of a bolt that has just landed: whole and twice its size, back to
   its own in the motion rule's 200 ms. */
.map-bolt-landed svg { animation: map-bolt-landed 200ms ease-out 1; }
@keyframes map-bolt-landed {
    from { opacity: 1; transform: scale(2); }
    to { opacity: var(--bolt-alpha, 1); transform: scale(1); }
}
/* The unit's segmented, over the unit word it opens from (planche RADAR2
   U.1): the mini wheel's box, the capsule's segmented inside. */
.radar-unit-pop {
    position: fixed;
    z-index: var(--z-sheet);
    padding: 4px;
    border: 1px solid var(--contour);
    border-radius: var(--radius-lg);
    background: var(--surface);
}
/* The gear at the end of the radar's bar (planche OPERAR J): a step-sized
   disc after the bar, and its two switches — the arrows, the bolts — one
   line each, a finger tall. */
.map-radar-bar .radar-marks-btn { flex: none; }
/* The radar in flight (planche MVOL C.1, C.2): its last image in one line —
   the hour in the text's ink, the age quiet, red and heavier once old. */
.map-radar-latest {
    margin: 0;
    padding: 0 6px;
    white-space: nowrap;
    font-size: var(--fs-md);
    font-variant-numeric: tabular-nums;
    color: var(--text);
}
.map-radar-age { color: var(--muted); }
.map-radar-age.is-old { color: var(--red); font-weight: 600; }
.radar-marks-pop { min-width: 220px; }
.radar-marks-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-height: 44px;
    padding: 0 10px;
    color: var(--text);
    /* The map's own switch rows' type (.map-layers-switch): unset, the
       browser's 16 px came through. */
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
}

/* THE MAP'S OWN ABSENCE (no WebGL2, or a GPU that keeps dropping it), written
   in the map's place — the one sentence left once no note hangs over a live
   map (owner's decision, C1). */
.map-unavailable {
    display: grid;
    place-items: center;
    height: 100%;
    margin: 0;
    padding: 0 24px;
    text-align: center;
    color: var(--muted);
    font-size: var(--fs-md);
}
/* Which configurations the WX Minima zones are painting, in the badges the
   briefing header wears for the ones firing on its airports. Bottom-left and
   viewport-fixed, for the measured reason .map-status carries below: #mapView
   is laid out taller than the screen, so a bottom offset measured against it
   lands behind the dock. These keep the NAMES, because a colour on the ground
   names nothing on its own. No ground of their
   own: the badges carry their own tint, and a chip inside a chip is one box
   too many.

   150, and measured rather than chosen: .map-status occupies 700-728 on a
   390x844 phone, so any slot below this one put the badges under the status
   chip the moment a message fired. */
/* The weather ramps over the map's animation bar — the cross-section's own
   legend rows, so the two surfaces key one picture with one component. They
   ride INSIDE the timebar dock rather than floating on their own, which is what
   keeps them clear of the note and the status line however many are shown. */
/* THE LEGEND, ONE STOREY OF THE BAR (owner's redesign). It was two things in
   two places — the ramps here, the configuration badges floating bottom-left
   over the map — which is two legends for one picture. They share one box now,
   joined to the transport under it: same ground, one hairline between the
   storeys, so the bar reads as a single capsule.

   Its own STOREY of the capsule — the capsule is a column, so a storey is a
   row of it and needs nothing said to claim its width. Always whole (planche
   24): the ramp of what is lit and, under Minima, the badges — nothing to
   unfold, no caret. */
/* THE ZOOM'S WAY BACK (planche TEMPS L.6): a disc on the chart's top right,
   as Recenter is on the map, there only while the chart is zoomed. Two
   classes: the contact area's anchor (`.round-icon-btn:not(.wx-header-btn)`,
   relative) weighs two, and a disc it wins is put back in the flow, off the
   screen. */
.route-zoom-reset svg {
    fill: none;
    stroke: var(--muted);
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
    stroke-linejoin: round;
}
.round-icon-btn.route-zoom-reset {
    position: absolute;
    top: 8px;
    right: var(--map-gutter, 12px);
}
.map-wx-legend {
    display: flex;
    flex-direction: column;
    gap: 5px;
    /* Little air under the ramp: the hour's line follows it closely
       (owner's ask). */
    padding: 6px 15px 2px 8px;
    font-size: var(--fs-xs);
}
.map-wx-ramps {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: var(--fs-xs);
}
/* The map's status line — showStatus's message, mirrored here because the
   briefing's #statusBar is hidden while the map is open. Bottom-left, one line
   under the minima badges; the layer's own note answers from under the header
   instead, so the two can never cover each other. Opaque and in --text, not
   --muted: a note is an aside, a status is an answer. */
.map-status {
    /* Viewport-fixed, not absolute: #mapView is laid out 12px wider than the
       screen and offset down by the header, so a bottom offset measured against
       it lands under the dock — the attribution is pinned this way for the same
       reason. Fixed puts the message inside the rectangle a pilot can actually
       read, whatever the section does around it. */
    position: fixed;
    bottom: calc(116px + env(safe-area-inset-bottom, 0px));
    left: 12px;
    right: 12px;
    z-index: var(--z-chrome);
    margin: 0;
    padding: 6px 12px;
    border-radius: var(--radius-md);
    background: var(--surface);
    box-shadow: 0 0 0 1px var(--contour);
    color: var(--muted);
    font-size: var(--fs-xs);
}
/* The engine's attribution: a thin transparent line centered just UNDER the header
   switcher (top-centre), so the floating dock reclaims the bottom edge. Pinned
   viewport-fixed; the top offset clears the switcher and leaves 4px of air under
   it (the switcher ends 40px down on a phone, 54 on desktop — see the media
   query). Same placement as the WX map's MapTiler credit (above), so both views
   match. */
#briefingMap .maplibregl-ctrl-attrib {
    position: fixed;
    /* Centre over the chrome's zone, not the viewport (see --free-left). */
    left: calc(50% + var(--free-left) / 2);
    top: calc(env(safe-area-inset-top, 0px) + 44px);
    bottom: auto;
    transform: translateX(-50%);
    /* One centered line: left:50% would otherwise cap the shrink-to-fit width at
       half the viewport and wrap the encart. */
    white-space: nowrap;
    z-index: var(--z-chrome);
}
/* Desktop: the header rides lower (padding-top 18px), so the credit drops to
   clear the switcher's lower position. */
@media (min-width: 841px) and (min-height: 501px) {
    #briefingMap .maplibregl-ctrl-attrib { top: calc(env(safe-area-inset-top, 0px) + 58px); }
}

/* Airport markers: an ICAO pill standing ABOVE the station fix with a pointer
   down to it (audit A3), wearing the airport's non-green identity dots
   (history-chip semantics). The fix itself is a dot the wrapper draws, so the
   point says where the field is and the pill names it without hiding it.
   It was a translucent fill behind a 2px white contour, blurred. The
   contour was the map chrome's recipe worn by an object ON the map, and at
   this size it was a white ring around four letters rather than a keyline:
   the pill read as a button dropped on the chart.

   The contour is back, and the difference is the whole point of the objection
   it answers: ONE pixel, and a fill that is no longer white. --surface-2 under
   a --surface keyline is a near-white block edged in white — against the chart
   the ring is invisible, against imagery and a NOTAM wash it is exactly the
   keyline the chart symbols wear (.map-glyph-casing), and at no size is it a
   ring around four letters. The code stays in the chart's own blue, like every
   other aerodrome text on the map. */
/* The wrapper is a POINT, not a box: the engine centres it on the fix, and
   sized by its pill it put the pill's own origin half a pill up and left of
   the fix — every transform below was then off by that much (measured at
   390px: the pill's centre 11px above the fix it was meant to stand abeam
   of). At 0 × 0 the wrapper's origin IS the fix, and each transform lands
   where it says. */
.map-airport-anchor { width: 0; height: 0; background: none; border: 0; }
/* An airspace name under a placed chart, seen through it (CALAGE): the DOM
   label stays over the canvas, its content as opaque as the sheet lets it be
   (MapLibre writes the anchor's own opacity). */
.map-airport-anchor.under-chart-sheet > * { opacity: var(--chart-see-through, 0.2); }
/* A SYMBOL STANDS ON ITS OWN POINT (fault the owner reported: the labels read
   up and to the right of the icons). At 0 x 0 the engine's own centring puts
   the wrapper's ORIGIN on the point, and a face in normal flow hangs from that
   origin — below and to the right, half a symbol off: 9 px at Navigation, a
   field drawn where it is not. Each face is therefore centred on the origin
   itself, which is also the one point the labels and the pills are measured
   from (mapLabelSymbolLead, .map-airport-marker.has-symbol). */
.map-airport-anchor > .route-apt-marker,
.map-airport-anchor > .route-navaid-marker,
.map-airport-anchor > .route-rp-marker,
.map-airport-anchor > .route-activity-marker,
.map-airport-anchor > .route-obstacle-marker {
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
}
/* The fix, when the BRIEFED PILL is the field's only mark: 7px of chart blue in
   a white keyline, on the point. It belongs to the pill and to nothing else —
   the wrapper above is shared by every family that stands on this map (radio
   aids, reporting points, obstacles, webcams, aerodrome symbols, the pilot's
   filed points, the airspace tags), and each of those already carries its own
   chart symbol; a bare ::after put a blue dot under every one of them. Hence
   :has(.map-airport-marker) — the pill's own element, the same map the two
   rules below read. Gone the moment a symbol or a leg's disc marks the field —
   that mark stands on the fix, and the pill stands abeam of it. */
.map-airport-anchor:has(.map-airport-marker)::after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 7px;
    height: 7px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: var(--chart-blue);
    box-shadow: 0 0 0 1px var(--surface);
    pointer-events: none;
}
.map-airport-anchor:has(.has-symbol)::after,
.map-airport-anchor:has(.on-waypoint)::after,
.map-airport-anchor:has(.under-ground)::after { display: none; }
.map-airport-marker {
    position: relative;
    display: inline-flex;
    align-items: center;
    /* ABOVE ITS FIX: the lift is the pointer's height plus the dot's half, so
       the pointer's tip touches the dot's keyline. Two briefed fields too
       close for two pills STACK (owner's choice, audit A3): the label pass
       writes --pill-stack on the second, and it stands one step — a pill and
       its air — above the first, whichever placement they share. The marker's
       box IS the pill: the name hangs outside it (.map-airport-aside), so every
       transform below places the pill, and the name follows it. */
    --pill-lift: 9px;
    --pill-step: 21px;
    --pill-stack: 0;
    transform: translate(-50%, calc(-100% - var(--pill-lift) - var(--pill-stack) * var(--pill-step)));
    color: var(--text);
    font-family: var(--app-font);
    /* THE CODE AT 13px (owner's call, planche APT A.6), its pastilles with it;
       the name beside stays chart text (.map-airport-aside). */
    font-size: var(--fs-sm);
    /* THE LINE BOX IS STATED, never inherited (owner's call: the badge was too
       tall): left to the map's own line height it measured 20px, a 22px badge
       where the label pass reserved 17 (PILL_H). 13px, the map labels' own
       line (MAP_LABEL_LINE_H). Top-aligned in the anchor's own line box, so the
       translate below centres it on its fix whatever that box's height. */
    line-height: 13px;
    vertical-align: top;
    font-weight: var(--fw-semibold);
    white-space: nowrap;
    cursor: pointer;
}
/* THE PILL (owner's decision, planche APT A): round, tight, its pastilles then
   its code in the heading ink, on the card's white — and LIFTED by a short
   shadow, with no contour. The one object on this chart that stands ON it
   rather than in it: an owner's departure from « rings, not shadows »
   (docs/heliwx-brand-guidelines.md). The shadow paints outside the box; the
   label pass still reserves a pixel of it either side (PILL_H, PILL_PAD_W). */
.map-airport-pill {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 1px 4px;
    border-radius: 999px;
    background: var(--card);
    box-shadow: 0 0 0 0.5px rgba(var(--shadow-rgb), 0.18), 0 1px 3px rgba(var(--shadow-rgb), 0.28);
    color: var(--heading);
}
.map-airport-pill .ap-rings { gap: 2px; }
/* 11px pastilles beside the 13px code (owner's call). */
.map-airport-pill .ap-ring { width: 11px; height: 11px; }
/* THE PILL STANDS BESIDE ITS SYMBOL. A briefed field is an ordinary aerodrome
   on this chart — its symbol stands on the fix exactly where every other
   field's does, and nothing about the pill may move it — but the pill is not
   text: it is an OPAQUE object, solid fill, code and minima dots. The rest of
   this chart writes its text BELOW its symbol, and that rule was written for
   haloed text, which lets the ground and the mark show through. Stacked under
   an 18px symbol, a solid 18px-tall, 100px-wide block does not complete the
   symbol, it swallows it: the pair reads as one blob and the eye takes the
   blob's centre for the field — which is the pill, a line further down. The
   gap was never the problem (2.5px at Navigation, they already touch); the
   stacking was. Abeam, the symbol stays alone on its fix and the pill reads as
   what it is, a label hung off it — the same reading a leg's numbered disc has
   always had (.on-waypoint below).
   The offset is the SAME arithmetic as before, turned on its side: half the
   mark plus 4px, from the live --map-symbol, so it follows the symbol down the
   tier staircase where a hard pixel value would drift.
   `has-symbol` is written by the airports layer itself (setAirportsWithSymbol,
   route-profile.js). Without one — layer off, kind filtered, zoom under the
   symbol threshold, a leg number already standing there — there is no mark to
   stand beside, and the pill goes back ON the point: it is then the field's
   only mark, and a mark standing beside its own fix is a chart lying about
   where the field is. */
.map-airport-marker.has-symbol {
    transform: translate(calc(var(--map-symbol) / 2 + 4px), calc(-50% - var(--pill-stack) * var(--pill-step)));
}
/* The field is a leg of the route, so it already wears the numbered disc and
   no symbol (syncAirportMarkers skips it). The pill goes ABEAM of that disc —
   the disc is the mark and it stays on the fix, the pill stands beside it.
   16px: half of the disc's fixed 20, the 2px keyline round it, and the 4px
   every mark keeps off its own text. The disc does not walk the symbol
   staircase (it carries a number), so neither does this. Written after .has-symbol on purpose: the two states are
   exclusive by construction, and if that ever stops being true a leg's own
   number outranks a symbol it does not have. */
.map-airport-marker.on-waypoint {
    transform: translate(16px, calc(-50% - var(--pill-stack) * var(--pill-step)));
}
/* The pointer, in the pill's own white, only where the pill stands over its
   fix: abeam of a mark there is nothing to point at. */
.map-airport-marker::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -5px;
    width: 10px;
    height: 5px;
    transform: translateX(-50%);
    background: var(--card);
    clip-path: polygon(0 0, 100% 0, 50% 100%);
}
.map-airport-marker.has-symbol::after,
.map-airport-marker.on-waypoint::after { display: none; }
/* At the ground's zoom the field is its reference point, drawn at its published
   place (planche SOL-OACI D.1): the pill and its dot give way to it. */
.map-airport-marker.under-ground { display: none; }
/* THE NAME STANDS BESIDE THE PILL, as chart text (owner's decision, planche APT
   A.4): the ground's halo, no fill — the pill stays the size of its verdict and
   its code, and the name is the first thing to go when room runs out. Out of
   the marker's flow, so the pill alone is what the transforms place; the label
   pass reserves the name to the pill's right (MAP_LABEL_GAP, then its letters)
   and empties this node when it does not fit. */
.map-airport-aside {
    position: absolute;
    left: calc(100% + 3px);
    top: 50%;
    transform: translateY(-50%);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-medium);
    color: var(--text);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo);
}
.map-airport-aside:empty { display: none; }

/* Finger pad, the .route-apt-marker recipe. This pill was the one marker family
   on the map with no extended target, and the paint is nowhere near a
   fingertip: measured at 390px, 38.1 x 15 for a bare code. The pad carries the
   whole difference — 15 inset both ways is a 45px target, the number this pad
   has always been sized to, restated against the measurement rather than
   against the ~21 the old comment estimated. It has to grow WITH the paint:
   taking two pixels out of the pill above takes two off the target unless this
   moves with it, and a shrunken target is the kind of regression nothing looks
   like. Vertical only: the pill is already wide enough sideways, and pads that
   overlap turn two neighbouring fields into one ambiguous target. */
.map-airport-marker::before {
    content: "";
    position: absolute;
    inset: -15px 0;
    border-radius: 999px;
}

/* The engine's own rail, where a map still wears it (the route editor): its
   4 px is not one of the two radii (docs/ui/grammaire.md, « Les arrondis »). */
.maplibregl-map .maplibregl-ctrl-group { border-radius: 999px; }
.maplibregl-map .maplibregl-ctrl-group button:first-child { border-radius: 999px 999px 0 0; }
.maplibregl-map .maplibregl-ctrl-group button:last-child { border-radius: 0 0 999px 999px; }
/* Attribution: a transparent quiet chip (no fill). */
#briefingMap .maplibregl-ctrl-attrib a { color: var(--muted); }


/* The lists that carry a number — a WX minimum, an altitude, an hour, a TAF
   time — read as wheels wherever a finger is the pointer: they snap one option
   to their middle, and the one at the middle when the finger lets go is the
   value (wireScrollWheel, render.js).

   Keyed on the attribute that function sets, NOT on a media query: the JS
   decides once what counts as a touch screen, and the looks follow that same
   decision — a list can never dress as a wheel it was not wired as, and the two
   can never disagree about what a touch screen is.

   Last in the sheet, so it beats each list's own height and padding. */
[data-scroll-wheel="on"] {
    scroll-snap-type: y mandatory;
    overscroll-behavior: contain;
    /* Five rows tall, and two rows of air at each end so the first and the last
       option can reach the middle like any other. */
    max-height: calc(var(--control-h) * 5);
    padding-block: calc(var(--control-h) * 2);
    /* The band the wheel reads at: one row tall, dead centre. A scroll
       container's background stays put while its content moves, so this is a
       marker, not a stripe running past with the options. */
    background-image: linear-gradient(var(--bg) 0 0);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 100% var(--control-h);
}
/* Named one by one, not "> *": some of these lists wrap their options, so the
   snap points are never all direct children. */
[data-scroll-wheel="on"] .schedule-select-option {
    scroll-snap-align: center;
    min-height: var(--control-h);
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ---------------------------------------------------------------------------
   One rail, everywhere
   ---------------------------------------------------------------------------
   Every pill row in the app — the view switcher, the Raw/Decoded/Summary
   segments, the METAR/TAF/NOTAM elements, the WX layer pills — stands on the
   same rail as a settings field: its height, its white contour, its --surface-2
   ground. A pill is transparent until it is the selected one, and the ONLY
   thing that differs from one rail to the next is that selection's fill. */
/* THE SEGMENTED IS ITS WORDS (owner's decision, planche INTER I.5): no rail
   under them and no chosen pill lifted off one — the words stand on the row,
   the chosen one underlined (below). Every segmented of the app, the days'
   two namings included. */
.view-mode-toggle {
    box-sizing: border-box;
    min-height: calc(var(--control-h) + 6px);
    padding: 0;
    border: 0;
    /* A very light hairline under the whole set of choices, the chosen word's
       line set on it (owner's decision, planche INTER I.9). Its foot falls at
       a row pill's foot (planche CENTRE C.1), measured from the word's middle,
       not the button's, so neither the button nor its target changes. The 2 px
       strip's lower pixel is the hairline, the chosen line covers both. */
    --seg-line: calc(50% + (var(--control-h) - 2px) / 2 - 1px);
    background: linear-gradient(transparent 50%, rgba(var(--text-rgb), 0.14) 50%) 0 var(--seg-line) / 100% 2px no-repeat;
    box-shadow: none;
}
/* The buttons fill the rail's inner height, so every row is one control tall
   whatever it holds. */
/* The rail is one field tall; its buttons fill what the 2px contour and the 2px
   inner rim leave of it, so a rail never grows past the fields beside it. */
.view-mode-toggle .view-mode-btn {
    box-sizing: border-box;
    min-height: calc(var(--control-h) - 2px);
    border-radius: 999px;
}
/* A segmented's word is square-cornered and as tall as its rail, so the
   chosen word's line is straight and lies on the rail's hairline
   (planche INTER I.9). */
.view-mode-toggle .view-mode-btn { border-radius: 0; }
/* Unselected is nothing at all — no fill, no contour, no halo of its own. */
body .view-mode-toggle .view-mode-btn:not(.active) {
    background: none;
    border: 0;
    box-shadow: none;
}
/* And selected is one state, written once: whatever fills it, it wears no
   contour of its own — the rail already carries the white one, a second inside
   it only draws a ring — and it reads at the medium weight. The fill is the
   only thing a rail may choose. The day circles are not on a rail but are the
   same control, so they take the same selected weight. */
body .view-mode-toggle .view-mode-btn.active,
body .day-circle-btn.active {
    border: 0;
    font-weight: var(--fw-medium);
}
/* AND THE MARK SAYS WHICH KIND OF CHOICE IT IS. An EXCLUSIVE choice — one of
   a few, never none — marks its word with a 2 px line on the rail's hairline
   (planche INTER I.5, I.9): the button IS the word, no padding beside it, so
   the line is never wider than the word. A CUMULABLE choice — each pill on or
   off on its own — is ringed instead, below. The days' two namings are an
   exclusive choice like any other. */
body .view-mode-toggle .view-mode-btn.active {
    background: linear-gradient(var(--text), var(--text)) 0 var(--seg-line) / 100% 2px no-repeat;
}
/* An option left out keeps a very light ring (planches INTER I.10, RLIGNE L.1,
   L.2). */
body .round-icon-btn.day-circle-btn:not(.active) {
    box-shadow: inset 0 0 0 1px rgba(var(--text-rgb), 0.14);
}
/* A CUMULABLE ANSWER IS RINGED IN INK (owner's decision, planche INTER I.7):
   the word in the heading ink inside a 1.5 px ring of the text ink, no fill.
   The day circles are the same kind of answer — each on or off on its own — so
   they read it from here. */
body .round-icon-btn.day-circle-btn.active {
    background: none;
    color: var(--heading);
    box-shadow: inset 0 0 0 1.5px var(--text);
}

/* ---------------------------------------------------------------------------
   Route cross-section (route-profile.js) — builder card over the WX map,
   waypoint markers, and the SVG chart overlay. Existing tokens only.
   --------------------------------------------------------------------------- */

/* Builder overlay: the history-page shell, sized for the waypoint rows; no
   sticky footer, so it keeps its own bottom padding. */

/* Pick-on-map pill: floats over the WX map while the builder overlay is
   parked — same recipe as the floating map chrome. */
/* Parked-builder pill: the only way back to the route page, so nothing may
   cover it. It sat at bottom:96px, which on a phone put it INSIDE the weather
   dock — measured at y 810-845 against a dock at 722-844, invisible under the
   timeline. The top-centre is taken by the view switcher (measured overlapping
   it at 1280), so it stays at the bottom, above the dock's tallest form: two
   stacked rows plus the transport bar, 122px measured at 390px wide. */
.route-pick-pill {
    /* Fixed and centred over the content column, like the briefing dock — its
       own absolute container runs past the viewport bottom, which is what made
       every bottom offset land inside the dock. One row above it: the dock
       measures 122px tall in its two-row form at both 390 and 1280. */
    position: fixed;
    left: var(--free-left);
    right: 0;
    bottom: calc(134px + env(safe-area-inset-bottom, 0px));
    width: fit-content;
    max-width: calc(100% - var(--free-left) - 24px);
    margin-inline: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    z-index: var(--z-chrome);
    padding: 8px 16px;
    border: 1px solid var(--contour);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
    box-shadow: none;
    cursor: pointer;
    white-space: nowrap;
}
.route-pick-pill strong { flex: none; color: var(--heading); font-weight: var(--fw-semibold); }
.route-pick-pill svg {
    width: 14px;
    height: 14px;
    flex: none;
    fill: none;
    stroke: var(--muted);
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* The way back is the label; the hint is what gives way when the pill runs out
   of room, never the other way round. */
.route-pick-hint {
    min-width: 0;
    color: var(--muted);
    font-size: var(--fs-xs);
}

/* The legs stand clear of the card's head: a list that starts right under the
   title reads as part of it.

   The list also declares the width of the card's first column, the rail the
   ROUTE'S OWN THREAD runs down — one number for every card rather than each
   keeping its own, since two numbers agreeing by hand are two numbers waiting
   to disagree. */
.route-wp-list {
    --route-rail-w: 28px;
    /* The thread reads as the map's own line, in its ink (--chart-route) — 2px
       here against the 5 it is drawn at over a chart, a list being read at
       arm's length and not zoomed. */
    --route-thread-w: 2px;
    display: flex;
    flex-direction: column;
}
.route-wp-list { margin: 0 0 4px; }
.route-wp-list:empty { margin: 0; }
/* ONE POINT IS ONE SYSTEM ROW beside ONE rail (owner's decision, planche de
   choix 1). The rail holds the point's marker — the map's own .route-wp-marker,
   so the panel and the chart name a point with the same disc — and IS the
   drag handle. Right of it the code with its name under it, and at the row's
   end the level flown over the point. The row answers a tap outside the rail
   with the app's own action sheet. */
.route-wp-entry {
    /* The step from the rail to the text, read by the rule between two cards
       so it starts exactly where the writing does. */
    --route-row-gap: 8px;
    gap: var(--route-row-gap);
    cursor: pointer;
}
/* The rail runs the row's full height and paints the thread behind its
   marker: the route is a line the point sits ON. Only half of it on the two
   ends — a thread sticking out above the first point or below the last would
   draw a leg that is not flown. */
.route-wp-entry > .route-wp-rail {
    position: relative;
    flex: none;
    width: var(--route-rail-w);
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    /* The one gesture that must not scroll the panel out from under itself. */
    cursor: grab;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
}
/* The thread is ONE UNBROKEN STROKE from the first point to the last. The rail
   stretches to the row's CONTENT box, so a thread drawn inside it stops short
   by the row's own padding at both ends — 6px of missing line per card edge,
   which is what made a route read as a dashed one once the legs between the
   cards were taken away. It is pulled back out over that padding, from the one
   place the padding is declared. */
.route-wp-entry > .route-wp-rail::before {
    content: "";
    position: absolute;
    top: calc(var(--route-row-pad, 0px) * -1);
    bottom: calc(var(--route-row-pad, 0px) * -1);
    left: 50%;
    width: var(--route-thread-w);
    margin-left: calc(var(--route-thread-w) / -2);
    background: var(--chart-route);
}
.route-wp-entry:first-child > .route-wp-rail::before { top: 50%; }
.route-wp-entry:last-child > .route-wp-rail::before { bottom: 50%; }
/* The marker over the thread it terminates, the way it sits over the line on
   the map. */
.route-wp-entry > .route-wp-rail > .route-wp-marker { position: relative; }
/* The rule between two points STOPS BEFORE THE RAIL (owner's decision): a
   hairline drawn edge to edge crosses the route and reads as a cut in it. It
   cannot be a border either — even a transparent one is 1px of thread missing,
   and a route drawn in dashes reads as broken — so the border goes and the
   rule is painted as the row's own line, starting where the text column does
   (the rail plus the row's gap). The rule under the list's head keeps its full
   width: the thread does not reach it. */
.route-wp-entry { position: relative; }
.route-wp-entry + .route-wp-entry { border-top: 0; }
.route-wp-entry + .route-wp-entry::after {
    content: "";
    position: absolute;
    top: 0;
    left: calc(var(--route-rail-w) + var(--route-row-gap));
    right: 0;
    height: 1px;
    background: var(--line);
}
.route-wp-entry > .sheet-k { flex: 1 1 auto; min-width: 0; white-space: normal; padding: 0; }
.route-wp-entry.sheet-row-tall { --route-row-pad: 6px; align-items: center; padding: var(--route-row-pad) 0; }
.route-wp-entry.sheet-row-tall .sheet-k { padding-top: 0; }
.route-wp-entry > .sheet-v { flex: none; }
/* Under the finger the row leaves the list: it takes the surface fill and the
   full shadow it otherwise does without, so it reads as lifted. */
.route-wp-entry.is-dragging {
    z-index: var(--z-sticky);
    background: var(--bg);
    opacity: 0.85;
}
/* Where a dragged card would land: a text-ink rule above the card it would push
   down, or under the whole list for the last slot. */
.route-wp-entry.drop-target::before {
    content: "";
    position: absolute;
    top: -5px;
    left: 8px;
    right: 8px;
    height: 3px;
    border-radius: 2px;
    background: var(--text);
}
.route-wp-list.drop-end { position: relative; }
.route-wp-list.drop-end::after {
    content: "";
    position: absolute;
    bottom: -5px;
    left: 8px;
    right: 8px;
    height: 3px;
    border-radius: 2px;
    background: var(--text);
}
/* A card must not answer a press with a text selection — the drag would paint
   its label blue on the way, and on iOS raise the Copy/Look-Up callout over the
   gesture. Fields inside a card keep their selection: they are there to be
   typed in and read back. */
.route-wp-entry,
.config-block {
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}
.route-wp-entry input,
.config-block input,
.config-block textarea {
    user-select: text;
    -webkit-user-select: text;
    -webkit-touch-callout: default;
}
/* The two ends fly nowhere: their height cell is a word, not a menu — and it
   carries the ground's own elevation beside it, in the column every other
   level is read in rather than buried in the description line. */
.route-wp-gnd { color: var(--muted); padding-right: 4px; }
.route-wp-gnd-ft { margin-left: 6px; }
/* EDIT MODE (planche ROUTE-EDIT): « Edit route » turns the legs into a list of
   gestures. An insert row stands before, between and after the points — right
   of the thread, on the names' column, the thread running through it — and
   each point ends on its level (a press of its own, chevron down) and a cross. */
.route-wp-insert {
    --route-row-gap: 8px;
    position: relative;
    gap: var(--route-row-gap);
    min-height: 44px;
}
.route-wp-insert > .route-wp-rail {
    position: relative;
    flex: none;
    width: var(--route-rail-w);
    align-self: stretch;
}
.route-wp-insert > .route-wp-rail::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: var(--route-thread-w);
    margin-left: calc(var(--route-thread-w) / -2);
    background: var(--chart-route);
}
.route-wp-insert:first-child > .route-wp-rail::before { top: 50%; }
.route-wp-insert:last-child > .route-wp-rail::before { bottom: 50%; }
.route-wp-insert > .sheet-k { display: flex; align-items: center; gap: 8px; }
.route-wp-insert-plus {
    flex: none;
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
}
/* One rule between two rows of the list, from the text column, as at rest. */
.route-wp-list.is-editing > .sheet-row + .sheet-row { border-top: 0; }
.route-wp-list.is-editing > .sheet-row + .sheet-row::after {
    content: "";
    position: absolute;
    top: 0;
    left: calc(var(--route-rail-w) + var(--route-row-gap));
    right: 0;
    height: 1px;
    background: var(--line);
}
.route-wp-alt {
    display: inline-flex;
    align-items: center;
    align-self: stretch;
    gap: 2px;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    cursor: pointer;
}
.route-wp-alt-chev {
    flex: none;
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
}
.route-wp-remove {
    flex: none;
    align-self: stretch;
    width: 40px;
    margin-right: -8px;
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
}
.route-wp-remove > svg {
    width: var(--icon-box);
    height: var(--icon-box);
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
}
.route-edit-done { color: var(--text); font-weight: var(--fw-semibold); }
/* A point's sheet (planche ROUTE-EDIT A.6): three cards — its level, its
   verbs, and the one that takes it off the route — one air between them. */
.point-sheet .settings-card + .settings-card { margin-top: var(--card-gap); }
.point-sheet .settings-card > .sheet-row:first-child { border-top: 0; }
/* WHAT'S HERE (planche WHATSHERE): the pressed point marked in the text's
   ink, never the route's it may sit on (ESPACE C.2), the Direct to as the
   sheet's one primary verb, and the airspace stack drawn in the cross-section's
   own recipes, to the scale of heights, each storey opening its volume (ESPACE). */
.whatshere-pin {
    display: block;
    width: 18px;
    height: 18px;
    border: 2.5px solid var(--text);
    border-radius: 50%;
    background: radial-gradient(circle, var(--text) 0 3px, transparent 3.5px);
    box-shadow: 0 0 0 2.5px var(--surface);
}
.whatshere-direct { min-height: 48px; margin: 0 0 var(--card-gap); }
.whatshere-show { width: 100%; min-height: 48px; margin-top: var(--card-gap); }
.whatshere-stack-wrap { display: flex; justify-content: center; padding: 10px 12px; }
.whatshere-stack { display: block; width: 100%; max-width: 300px; height: auto; }
.route-profile-svg.whatshere-stack .whatshere-storey { cursor: pointer; }
.route-profile-svg.whatshere-stack .whatshere-storey rect { pointer-events: all; }
.route-profile-svg .whatshere-tick { stroke: var(--muted); stroke-width: 0.6; stroke-dasharray: 2 3; opacity: 0.6; }
.route-profile-svg .whatshere-axis { fill: var(--muted); font-size: var(--fs-2xs); }
.route-profile-svg .whatshere-storey-chev {
    fill: none;
    stroke: var(--muted);
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.route-profile-svg .whatshere-storey-name { font-size: var(--fs-xs); font-weight: var(--fw-semibold); }
.route-profile-svg .whatshere-cruise { stroke: var(--chart-route); stroke-width: 2.5; }
.route-profile-svg .whatshere-cruise-text {
    fill: var(--chart-route);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
}
/* The small verbs of the filed list (eye, pencil, bin, the rename's tick and
   cross): bare glyphs on the row, no disc of their own. */
.route-wp-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    flex: none;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
}
/* Les petits verbes de la liste sont des glyphes de 24 px ; leur zone de contact
   en fait 44. Mesuré : aucun voisin ne tombe dans ce carré. */
.route-wp-btn { position: relative; }
.route-wp-btn::after {
    content: "";
    position: absolute;
    inset: -10px;
}
@media (hover: hover) {
    .route-wp-btn:hover { background: var(--hover-bg); }
}

.route-wp-btn svg {
    width: 12px;
    height: 12px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* Back at --fs-md, the size the rest of the panel is set in: iOS is kept from
   zooming the page in on focus by the viewport's own maximum-scale (index.html),
   not by inflating every field to 16px — see the note there. */
.route-wp-input {
    width: 100%;
    padding: 7px 10px;
    border: 1px solid var(--contour);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
}
.route-filed-list {
    margin-top: 4px;
    max-height: 190px;
    overflow-y: auto;
    border: 1px solid var(--soft-line);
    border-radius: var(--radius-sm);
}
/* On the add-waypoint page a list IS the pane — airports or the pilot's own
   points, the same list either way: it opens on the whole of what it holds, so
   it gets the height of a list rather than of a hint, and its headings ride
   over its own ground: a veil in the sheet's glass, as the Briefing panel's
   cards are (owner's decision). */
.route-airport-list,
.route-filed-list {
    max-height: min(52vh, 420px);
    margin: 10px 8px 0;
    padding: 4px 0;
    border: 0;
    border-radius: var(--radius-lg);
    background: var(--glass-group);
}
.route-airport-list .airport-group-header,
.route-filed-list .route-group-head { top: -1px; }
/* ON ITS OWN PAGE the pane is not a framed list inside a sheet: a folder is a
   card now, so a frame around them would be a box around boxes, and an inner
   scroller on a page is a second scrollbar under the first. In the route's add
   sheet the frame and the height stay — there the list IS a block of the sheet. */
#waypointsOverlay .route-filed-list {
    max-height: none;
    overflow: visible;
    border: 0;
    background: transparent;
}
/* The route's two numbers on the first line: the distance read plainly on the
   card's own ground — nothing to set, so nothing to frame — and Cruise at the
   right as a field carrying its own label, the near-white fill behind the white
   contour every field of the app wears (and no shadow: it sits on the card, not
   over it). "Airports within" wraps to a line of its own below (see
   .route-brief-corridor): three fields abreast left Cruise unreadable in a
   phone-width drawer. */
/* "Route range" sets what the route briefs — its fields and its NOTAMs — so it
   stands only where that briefing is read: the Report page. Gone on Map and on the
   cross-section (display, not visibility — the rows close back up). */
body.map-view-open .route-brief-corridor,
body.route-profile-open .route-brief-corridor { display: none; }
/* The fields that width pulls in are an AIRPORTS row, the very one the Area
   side writes: same label, same tags, same alignment — a number alone does not
   answer "is Ibiza in it", and a second dress for the same fact would say the
   two sides hold different things. Read, never pressed, so no cross on them. */
/* THE VALUE ENDS WHERE EVERY OTHER VALUE ENDS (owner's ask): the width and the
   fields it found are this row's value, so they finish against the card's right
   edge like the distance, the hours and the last check above and below them.
   They were packed against the label instead, and the row read as a sentence
   trailing off into empty card. Wrapped, each line right-aligns with them. */
.route-brief-fields .airport-chips {
    flex: 1 1 auto;
    min-width: 0;
    justify-content: flex-end;
}
/* The cap, worn as a tag of its own so it cannot be read as one more field:
   what the width found and the plan would not brief. */
.route-brief-fields .airport-chip.is-dropped {
    border-color: transparent;
    background: none;
    color: var(--muted);
    font-weight: var(--fw-regular);
}
/* "Route range" sets what the route briefs and the Airports row answers it, so both stand
   only where that briefing is read: the Report page. Gone on Map and on the
   cross-section (display, not visibility — the rows close back up). Scoped to
   the Briefing page's own block: the libraries draw this same row on their
   cards, and those pages open OVER the map — an unscoped rule took the
   Airports row off every route card the moment the map had been visited. */
body.map-view-open #briefingRouteTotal .route-brief-fields,
body.route-profile-open #briefingRouteTotal .route-brief-fields { display: none; }
/* The pair that acts on the whole route, sharing one line above the legs — with
   air of its own above, so it reads apart from the card's head rather than
   hanging off it. */
/* Identity reads the way an airport does: the chart code on top, the
   descriptive name under it in the muted register. Display only — editing goes
   through the row's sheet. */
.route-wp-code-text {
    flex: none;
    color: var(--heading);
    font-weight: var(--fw-semibold);
}
/* The code's own line, so the sign of what the point is sits beside it rather
   than above the description: an airliner leaving the ground for an aerodrome,
   a pin for a point the pilot placed — quiet, in the muted register the rest of
   the card's icons are drawn in. */
.route-wp-code-line { display: flex; align-items: center; gap: 6px; min-width: 0; }
.route-wp-kind {
    flex: none;
    width: 13px;
    height: 13px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    color: var(--muted);
}
.route-wp-kind-fill { fill: currentColor; stroke: none; }
/* The filed list's own name line, under the code. */
.route-wp-name-text {
    min-width: 0;
    color: var(--muted);
    font-size: var(--fs-xs);
}
/* A code is written in capitals. */
.field-caps { text-transform: uppercase; }
/* A MAP IN A CARD IS A BAND OF THE CARD, NOT A CARD IN THE CARD (§ 7.2 :
   aucune sous-carte) — no contour, no rounding of its own; it runs the rows'
   width under the head's hairline, tall enough to place a point by hand
   without pushing the fields off the screen. Its marker is the route's own
   (.route-wp-marker), so a waypoint looks the same wherever it is moved. */
.card-map {
    height: 190px;
    margin-bottom: 12px;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
    overflow: hidden;
}
/* The colour of the point, unfolded under its field — a colour is chosen by
   pointing at it, not by reading ten words. Eleven swatches (Soft, then the identity palette) sit on one line and
   wrap when the phone is too narrow for them; a twelfth appears when a KML
   arrived painted in a colour outside the palette. The swatches themselves are
   the app's existing .color-option/.color-dot pair — the same pastille the
   briefing configurations are coloured with — so there is nothing new here but
   the row that holds them.

   The swatches gather at the row's right edge, where every value sits. */
.route-color-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 2px;
    padding: 2px 0 8px;
}
/* The Color field shows the point's colour as a pastille on the value side;
   the palette unfolds under it (bindColorPicker). The field is no box of its
   own: its row and the palette stand among the card's rows. */
.wp-color-field { display: contents; }
/* Selected: the swatch keeps the hover fill it shares with the airport options
   and adds a ring, because a wash alone is not an answer to "which one is on"
   when the row holds eleven of them. The ring is the heading ink, the same
   word the app uses for "chosen" — never the swatch's own colour, which would
   read as part of the pastille instead of as a mark on it. */
.route-color-row .color-option.active {
    box-shadow: inset 0 0 0 2px var(--heading);
}
/* Waypoints and Airports pages: one row per point — identity, position, then
   add-to-route / edit / delete. */
.route-saved-panel { max-width: 520px; padding-bottom: 24px; font-size: var(--fs-md); }
/* The Waypoints pane carries a sticky footer (a new waypoint, KML import), so
   like the history panel the page gives its bottom padding up to it. The
   Airports pane has no footer and keeps its own. */
/* A list of rows: the libraries read alike, one hairline between two records
   and no air of a card between them (§ 7.4). */
.route-saved-list { display: flex; flex-direction: column; }
/* Areas and Routes are cards (owner's decision): the air between two is the
   Briefings list's. */
.route-saved-list.library-cards { gap: var(--card-gap); }
/* ONE FOLDER, ONE GROUP OF THE LIST (§ 7.4, § 8.8): its heading is a system
   row, its points the rows under it, no card around them. The heading keeps
   the sticky it already had — a drawer of two hundred points still says which
   folder is being scrolled — on the page's own ground, opaque: a heading that
   let its rows show through while they scroll under it reads as a fault. */
#waypointsOverlay .route-group-head { background: var(--bg); }
.route-saved-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 10px 0 0;
    border-radius: var(--radius-sm);
}
@media (hover: hover) {
    .route-saved-item:hover { background: var(--hover-bg); }
}
.route-saved-item .route-wp-btn { flex: none; }
/* A point from an import still being written: it is on screen so the batch
   reads as arriving, but it is not filed yet and nothing can be done to it —
   quiet, and inert to the finger. */
.route-saved-item.is-pending { color: var(--muted); }
.route-saved-item.is-pending .route-saved-pick { cursor: default; }
/* The row itself adds the point: its identity IS the button, so it takes the
   row's own padding and washes to the selected ground while it is pressed. */
.route-saved-pick {
    flex: 1 1 auto;
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
    padding: 6px 10px;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: inherit;
    font: inherit;
    font-size: var(--fs-md);
    text-align: left;
    cursor: pointer;
}
.route-saved-pick:active { background: var(--bg); }
/* The point's own colour, at the head of its row. The list is where a pilot
   scans a whole import at once — "which of these did I mark red" is a list
   question, not a map one — so the colour has to be readable here and not only
   under the dot on the chart. Smaller than the picker's swatches: a mark beside
   the code, not a control. Centred by hand because the row aligns on the
   baseline its two texts share, which a round dot has none of. */
.route-saved-pick > .color-dot {
    flex: none;
    align-self: center;
    width: 10px;
    height: 10px;
}
/* The name reads as the airport list's does — the plain register beside the
   code, not the card's own muted second line. */
.route-saved-pick > .route-wp-name-text {
    min-width: 0;
    color: var(--text);
    font-size: var(--fs-md);
}
.route-saved-sub { flex: none; color: var(--muted); font-size: var(--fs-xs); }
.route-saved-empty { margin: 0; padding: 8px 10px; color: var(--muted); font-size: var(--fs-xs); }
/* One heading per folder: the caret that folds it, its name — the door to the
   folder's sheet — how many points it holds, and its eye. The country headers
   of the airport list, with a drawer's handle in them — the same sticky
   uppercase divider riding over the list's own white ground, so the two lists
   read as one grammar. */
.route-group-head {
    position: sticky;
    z-index: var(--z-sticky);
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 0 8px;
    border-top: 1px solid var(--row-line);
    background: var(--surface);
}
/* FORM G1 (owner's board): the caret alone folds the drawer — its own contact
   patch behind the 13 px glyph, the size the eye at the other end wears — and
   the NAME is the door to the folder's sheet, the way every library head is.
   No chevron: a card is already a door. "User waypoints" is nobody's folder, so
   there its name is not a button at all (.is-plain) and reads the same. */
.route-group-caret-btn {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    margin: -6px -4px -6px -2px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
}
.route-group-open {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 2px 0;
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
/* Le nom d'un dossier ouvre sa feuille : 18 px dessinés, 44 de contact en
   hauteur. Le nom du dossier libre n'ouvre rien et ne reçoit pas de zone. */
.route-group-open:not(.is-plain) { position: relative; }
.route-group-open:not(.is-plain)::after {
    content: "";
    position: absolute;
    inset: -13px 0;
}
.route-group-caret {
    flex: none;
    width: 13px;
    height: 13px;
    fill: none;
    stroke: var(--muted);
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform 0.15s ease;
}
.route-group-head.is-open .route-group-caret { transform: rotate(90deg); }
.route-group-open.is-plain { cursor: default; }
/* The eye that shows/hides a batch — or a single point — on the map. NO TILE
   BEHIND IT (owner's ask): a filled square on a card's head read as a second
   object sitting on the title's line. Armed, it is simply the drawn eye in the
   ink; struck through and muted, it is not. The state is in the GLYPH, so it
   never relied on a fill — nor on colour alone. */
.route-group-eye[aria-pressed="true"] { background: transparent; color: var(--heading); }
/* A point hidden one by one: its own row says so, not only its eye. Quiet, not
   struck out — the point is still filed, still searchable and still one tap
   from the route; it is the MAP it left. */
.route-saved-item:has(> .route-group-eye[aria-pressed="false"]) .route-saved-pick { opacity: 0.55; }
/* A FOLDER'S NAME IS A RECORD'S TITLE (§ 8.8 : 14–16 px / 500–600): in the
   text ink, no capitals and no tracking, its count beside it in the muted
   12 px. */
.route-group-name {
    flex: 1;
    min-width: 0;
    color: var(--text);
    font-family: var(--app-font);
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
}

/* A filed waypoint on the map: available to add, so a quiet dot rather than a
   numbered marker — the numbers belong to the legs the route already has. The
   white keyline belongs to the chart symbols (§2.22, the casing) and the
   pilot's own dots are not chart objects, so the contour here is the point's
   OWN colour rather than a casing: the same pastille the app draws everywhere
   else (a soft wash inside an ink contour), at map size.

   The colour is data, not style — a point carries its own, imported from the
   layer it was painted in in Google Earth — so it arrives as the three
   properties dotStyle() writes on the span, and this rule only says what to do
   with them. --dot-fill is an rgba wash meant for a light panel and the map
   would show straight through it, so the fill is mixed against the surface
   here instead: opaque, and identical in effect. The pilot's own ink
   (--chart-pilot) is the fallback in each var(), so a marker rendered without a style
   attribute still reads as one of the pilot's points.
   Its contour alone takes it off the ground: no shadow on the chart (§7.3). */
.route-filed-marker {
    position: relative;
    /* The marker is a bare <span> in the engine's marker element: inline, it would
       collapse to its borders (a tall white sliver) — the box must be laid
       out for its width/height to hold. Same reason on the bend dot. */
    display: inline-block;
    /* Sized off the map's one symbol number like every other marker family, so
       it walks down the same staircase. HALF the symbol: a dot naming nothing
       needs less room than a chart glyph, and half of 24 is exactly the 12px it
       always wore where the flying happens. Centred by the engine anchor
       (MapLibreMarker, anchor:"center"), no self-centring translate — one used
       to live here and double-shifted the dot half a symbol off its own point,
       by a different amount at each zoom tier. */
    width: calc(var(--map-symbol) / 2);
    height: calc(var(--map-symbol) / 2);
    /* The contour is drawn INSIDE that width: a border added to it would put
       the dot back up to the size it wore before the staircase. */
    box-sizing: border-box;
    border-radius: 999px;
    /* The point families' shared contour staircase: 2px at Detail, 1px above. */
    border: max(1px, calc(var(--map-symbol) / 12)) solid var(--dot-ink, var(--chart-pilot));
    background: color-mix(in srgb, var(--dot-color, var(--chart-pilot)) 20%, var(--surface));
    cursor: pointer;
}
/* Under the finger the dot fills with its own ink: the wash and the contour
   swap places, which reads as "this one" at any colour. */

/* The planned track (drawRouteLine, route-profile.js). Two strokes still, but
   the under-stroke is no longer a white casing: 7px of --surface under 4px of
   ink was a white band nearly twice the width of the line it was lifting,
   and on a light ground the band read as the route while the ink read as a
   thread down its middle. The sleeve is the route's own ink brought back
   towards the ground.

   The two are the way round a paper chart prints a track: a DARK line on a
   LIGHT halo, rather than a pale line on a dark edge. The line is --chart-route,
   the pilot's own ink and the one token that LIGHTENS over imagery instead of
   sinking into it, so the same rule reads on paper white and on a satellite
   basemap. The halo under it is that ink at 45 % on the ground, at a bit over
   half its opacity — wide
   enough to lift the line off a NOTAM wash, light enough that the line stays
   the mark and the halo stays the lift. Widened twice now, 3.5 on 6 to
   4 on 9 to 5 on 11: at the first weight the track passed for one more airspace
   outline among the ones it crosses, and it is the thing being flown. The halo
   keeps 3px either side of the line at the two top tiers and never falls under
   2.25 — below that it stops being an edge and becomes a fringe.

   Colour and width live here rather than in the layer's paint options, with
   the airspace inks: the width walks down the tier staircase below, and a
   stroke-width passed as an option would have to be rewritten on every
   zoomend. CSS is read back off this class by the facade's style probe. */
.map-route-sleeve {
    stroke: color-mix(in srgb, var(--chart-route) 45%, var(--bg));
    stroke-opacity: 0.55;
    stroke-width: 11;
}
.map-route-line {
    stroke: var(--chart-route);
    stroke-width: 5;
}
/* In flight (planche VOLR D.1; V-34; owner's decision, planche ROUTE): the leg
   being flown keeps the route's full ink and weight, every other leg — behind
   or ahead — pales over it; the same ink, never a new one. */
.map-route-active {
    stroke: var(--chart-route);
    stroke-width: 8;
}
.map-route-pale {
    stroke: color-mix(in srgb, var(--chart-route) 35%, var(--bg));
    stroke-width: 5;
}

/* Where the route crosses each model cell at the corridor's high point: the
   ground the ceiling verdict is measured from, marked at the rock's own
   coordinates. The chart marks the same crossings (.rp-cross-dot). A deviation
   from the EUROCONTROL guide, which defines relief as contours and tints and
   no per-cell figure — written up in docs/vfr-chart-rules.md. */
.map-peak { pointer-events: none; }
.map-cross-dot {
    position: absolute;
    width: 7px;
    height: 7px;
    margin: -4px 0 0 -4px;
    border: 1px solid var(--surface);
    border-radius: 50%;
    background: var(--chart-relief);
}
/* Numbered waypoint markers on the WX map (MapTiler DOM markers).
   The route rides over everything else on the map: markers are siblings in the
   map's own container with no z-index of their own, so the last one CREATED
   would otherwise paint over the rest — and the offers (airport pills, filed
   dots) are drawn after the route. Its points are what the pilot is flying, so
   they take the top and their leg handles sit just under them. */
.route-wp-marker {
    z-index: calc(var(--z-page) + 3);
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* FIXED, at every tier: this marker carries a leg number the pilot reads
       whatever the scale, and the map's rule is that the drawing shrinks while
       the writing stays readable (map-core.js). It is the one family that does
       not walk down the --map-symbol staircase, and the number is the reason.
       18 rather than 22: the disc was sized around a white ring it no longer
       wears, and the ring is what it was carrying — the route is the pilot's
       own mark, not a chart object, so it has no business in the casing the
       chart symbols wear.

       20 rather than 18, with a ring: the points of the route were the quietest
       marks on a chart they are the subject of — smaller than an aerodrome
       symbol and flat against the wash of whatever they stood on. The ring is
       NOT the white casing that was taken off them; it is the route's own ink
       (--chart-route), the family of the line and its halo, so a point reads as
       the end of a leg rather than as a mark dropped on one.

       The disc takes --chart-route, the same ink as the line it terminates, on
       the same argument: the pilot's marks are one family, and that token is
       the one that lightens over imagery instead of sinking into it. The
       keyline is FIXED like the disc it rings — the point families that walk
       the --map-symbol staircase compute their contour from it, and this one
       does not walk it, because it carries a number.

       18 inside that keyline, for 22px overall against the 24 it grew to: the
       point was the heaviest mark on a chart it only has to terminate. It
       comes off the DISC, not off the ring — the ring is what says "end of a
       leg" and it is the thinner of the two — and it stops at 18 because the
       number is the floor: 11px semibold, and two digits still have air on
       both sides there. The keyline stays a whole 2px on top of it, and both
       numbers are held to the label pass's own reservation by
       tests/map-core.test.js: half the disc plus the keyline, or a briefed
       field standing on this leg silently loses its name.

       The touch pad below is widened by exactly what the disc gave up: the
       drawn mark tightens, the target it offers the finger does not. The
       marker box in route-profile.js is sized to the disc (18/9), not left at
       the 22 it was: the engine anchors THAT box on the point, and a disc smaller
       than its box is a disc drawn off its own waypoint — 2px up and left,
       measured. */
    width: 18px;
    height: 18px;
    border-radius: 999px;
    /* SOFTENED (owner's decision, planche de choix 1): the black disc was too
       loud beside its row. The app's own family instead — the soft-line
       ground, a hairline ring and the digit in the route's ink — on the map and
       in the panel alike, since both write this one class. */
    background: var(--soft-line);
    color: var(--chart-route);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    box-shadow: 0 0 0 1px var(--chart-route);
}
/* Every point on the map meets the finger half-way: an invisible ring extends
   each marker's touch target well past its drawn size, so grabbing one never
   demands the precision the drawn dot suggests. ON THE MAP: both discs are worn
   by the route panel as well now (same class, same mark, one grammar), and
   there a pad reaching 40px would lie over the card's own text and swallow the
   taps meant for it. The marker wrapper is what tells the two apart —
   .hwx-map-marker, which map-engine.js puts on every marker whichever engine
   drew it, because each engine names its own wrapper differently. */
/* The waypoint's pad is written as its OUTER size instead: the disc it rings
   was taken in, and a fixed inset would have handed the finger a target that
   shrank with the drawing. 40px whatever the disc measures — the same number
   the filed dot holds while it walks the symbol staircase. */
.hwx-map-marker > .route-wp-marker::before {
    content: "";
    position: absolute;
    inset: calc((100% - 40px) / 2);
    border-radius: 999px;
}
/* The filed dot shrinks with the scale, so a fixed ring would shrink its target
   with it. Computed off the symbol like the aerodrome and camera pads: the
   outer box stays 40px at every step, a chart decision that never becomes a
   missed tap. */
.route-filed-marker::before {
    content: "";
    position: absolute;
    inset: calc((var(--map-symbol) / 2 - 40px) / 2);
    border-radius: 999px;
}
/* Held over an airport or a filed waypoint: letting go would land ON it, and
   the marker says so before the finger commits: the leg number's mark fills
   with the line's own ink, and so does the dot of a leg being bent. */
.route-wp-marker.is-snap { background: var(--chart-route); }
.route-bend-marker.is-snap {
    background: var(--chart-route);
    color: var(--bg);
    border-color: var(--chart-route);
}
/* The new point of a leg being bent, under the finger while the leg is in the
   hand (any part of a leg bends it: no handle is drawn at rest). */
.route-bend-marker {
    z-index: calc(var(--z-page) + 2);
    position: relative;
    display: inline-block;   /* see .route-filed-marker */
    width: 14px;
    height: 14px;
    border-radius: 999px;
    border: max(1px, calc(var(--map-symbol) / 12)) solid var(--chart-route);
    background: var(--surface);
}

/* The airports in view, offered like the filed dots but named: a small
   pill carrying the ICAO, tappable, and what a dragged point snaps to. It
   sizes itself to its text, so it centres on the fix by its own transform
   inside an unsized marker anchor — the briefing pills' recipe. */
/* A field the catalogue holds: the same disc as the webcam pin, carrying
   its VFR chart symbol instead of its code. Eighty four-letter codes over a
   chart said nothing about what any of them was; the symbol answers that at a
   glance, and a tap opens the card that names it. */
/* Every chart symbol on the map is drawn twice (chartMarkerSvg, airport-core.js):
   a white casing, then the ink over it. It replaces the glass disc the symbols
   used to sit in — the disc gave them a background and cost them their size,
   15px of drawing inside a 24px circle, where the whole 24 is now symbol.
   A halo also survives what a disc could not: satellite imagery, where a white
   circle is a hole in the picture and a white outline is just a legible line.

   fill: none, and that is the whole point of the casing. It is a KEYLINE, not a
   background. Filling it painted a white disc inside every closed symbol — the
   aerodrome circle, the VOR hexagon, the military double ring, the obstacle's
   envelope — while the ink above them is fill: none, so the white came from the
   casing alone and from nothing the chart asks for. The 1:500 000 draws those
   symbols hollow, with the ground showing through (2.22), which is what this
   restores. The pieces that ARE solid stay solid: they carry fill="currentColor"
   in the ink and get their halo from this stroke tracing their outline.

   3.4 against an ink of 1.7: the halo spreads 0.85 either side, half the width
   of the line it is lifting. At 4.5 it spread 1.4 — 0.82x the line — and the
   symbols read as white shapes with a blue thread through them, the same defect
   the rose was corrected for.

   The casing rule has to match the CHILDREN, not the group. Some glyphs carry
   presentation attributes in the markup — fill="currentColor" stroke="none" on
   the runway bars, the NDB dots, the compulsory reporting triangle, the webcam's
   knocked-out lens — and a value inherited from the group loses to an attribute
   on the element, while a rule that matches the element itself wins. The ink
   group keeps plain inheritance, which is exactly what leaves those pieces
   solid. */
.map-glyph-casing * {
    fill: none;
    stroke: var(--surface);
    stroke-width: 3.4;
    stroke-linejoin: round;
    stroke-linecap: round;
}
/* A filled dot gets a halo sized on the DOT, not on the line work. 3.4 is the
   right lift for a 1.7 stroke and far too much for a point 1.9 across: it made
   the halo wider than the thing it was detaching. On the NDB that was not a
   cosmetic matter — ten dots on a 7.6 ring sit 4.70 apart, and a halo reaching
   2.65 from each centre needs 5.30 to clear its neighbour, so the ring of dots
   soldered into a solid white annulus and stopped being the symbol the chart
   draws (worse still on the NDB-DME, 3.98 apart inside its square). At 1.8 the
   reach is 1.85 and the dots keep a full unit of ground between their halos. */
.map-glyph-casing .map-glyph-dot { stroke-width: 1.8; }
.route-apt-marker {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Centred on its point by .map-airport-anchor > .route-apt-marker. */
    width: var(--map-symbol);
    height: var(--map-symbol);
    /* Chart ink, not the buttons' ink: on the map, an aerodrome symbol
       reads the way the AIP chart writes it. */
    color: var(--chart-blue);
    cursor: pointer;
}
.route-apt-marker svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.route-apt-marker::before {
    content: "";
    position: absolute;
    inset: calc((var(--map-symbol) - 40px) / 2);
    border-radius: 999px;
}
/* A webcam near the route: the airport pill's own glass disc, a camera glyph
   instead of a code — a glance, not an option, so it sits under the pills.
   Four fifths of a chart symbol (owner's ask): a look out of the window is
   smaller than what is published, and the finger keeps its 40px target. */
.route-cam-marker {
    --cam-symbol: calc(var(--map-symbol) * 0.8);
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transform: translate(-50%, -50%);
    width: var(--cam-symbol);
    height: var(--cam-symbol);
    /* The cameras layer's own colour: the one grey in the range. */
    color: var(--muted);
    cursor: pointer;
}
.route-cam-marker::before {
    content: "";
    position: absolute;
    inset: calc((var(--cam-symbol) - 40px) / 2);
    border-radius: 999px;
}
/* The one solid symbol on the map. Everything else here is chart ink and stays
   hollow; a webcam is not in EUROCONTROL's alphabet, so drawing it filled is
   what separates a look out of the window from something published — and it
   gives the freshness colour below a surface to carry rather than a hairline.
   The white keyline still comes from the casing, which strokes this silhouette
   exactly as it strokes the chart's outlines. */
.route-cam-marker svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
    stroke: none;
}
/* The lens, knocked out of the body so a camcorder still reads as one once the
   body is a block of colour. Scoped to the INK group: in the casing group the
   circle has to stay part of the silhouette being haloed, which is what
   .map-glyph-casing * already makes it. */

/* The webcam card: one camera's last image with its age beside it. The image
   takes the panel's width; the meta line reads muted like every source line;
   the credit is the Windy free tier's licence condition, set small but real. */
.webcam-panel { max-width: 420px; }
.webcam-image {
    display: block;
    width: 100%;
    border-radius: var(--radius-md);
}
.webcam-meta {
    margin: 8px 0 12px;
    font-size: var(--fs-md);
    color: var(--muted);
}
/* The picture's age, as a colour, in three steps (webcamFreshness,
   profile-core.js): under an hour the image stands for the weather, up to three
   it is worth a second look, past that it is offered but not to be flown on.
   The alert palette, which is reserved for operational status — and an image's
   age IS one. Never --red: red means a minimum was breached, not that a photo
   is getting on. `old` keeps --muted, the colour this layer already wore, so
   the code adds meaning rather than repainting the map. Here AND on the pin,
   because the cameras have no layer card to hold a legend: the card is where
   the colour on the map is explained. */
.webcam-meta.is-fresh { color: var(--cam-fresh); }
.webcam-meta.is-ageing { color: var(--cam-ageing); }
.route-cam-marker.is-fresh { color: var(--cam-fresh); }
.route-cam-marker.is-ageing { color: var(--cam-ageing); }
.webcam-player-btn { width: 100%; }
.webcam-credit {
    margin: 10px 0 0;
    text-align: center;
    font-size: var(--fs-xs);
    color: var(--muted);
}
.webcam-credit a { color: var(--muted); }

/* ---------------------------------------------------------------------------
   The weather at one rung — the wind flying, the air and the cloud washed
   ---------------------------------------------------------------------------
   Two alphabets, which is the whole point of drawing them in two different
   hands: a MOTION (the flying particles) and a WASH (the cover, the air's
   temperature). Between them they answer the question the altitude cursor asks
   — what is the air doing at this height — without either fighting for the
   other's pixels. There are no LINES at all: a mesh of iso-lines over a wash
   was two drawings of one fact (owner's call, 30/08), and the numbers are read
   off the legend under the map. Every ink is a token; the geometry is
   static/wx-level-draw.js, the flight static/wx-particles.js, and neither
   carries a colour. */
/* The cloud wash paints its own pixels, so it needs the COLOUR and not a rule
   the engine can apply. Read back off these declarations through the map's own
   style probe (map-engine probeRgb), so the ink still lives here and nowhere
   else — the ground's variant included, which is the whole reason it is probed
   rather than looked up. Two of them now: a block of sky is a MIX of the thin
   grey and the dense one, and one declaration could only ever say one grey. */
/* THE GROUND'S OWN VARIANT, and the only one this app has. `map-ground-dark`
   sits on the ROOT rather than on the map, because the key under the animation
   bar stands outside the map's own container and has to name the colours the
   map is wearing — one source, the rule the whole legend was rebuilt on. It is
   set only while the MAP is the view being looked at (briefing-map.js
   syncBodyClasses): the cross-section is white paper whatever ground the pilot
   picked, and a white cloud on it would be no cloud at all. */
:root.map-ground-dark {
    --map-outline-coast: var(--map-outline-coast-on-dark);
    --map-outline-shadow: var(--map-outline-shadow-on-dark);
    --map-outline-border: var(--map-outline-border-on-dark);
    --wx-cloud-thin-rgb: 217, 217, 217;
    --wx-cloud-solid-rgb: 255, 255, 255;
    --wx-fog-rgb: 236, 190, 64;
    --wx-fog-freezing-rgb: 196, 132, 226;
}
/* THE CROSS-SECTION'S NIGHT INKS (docs/ui/carte.md, « Weather est un interrupteur, Navigation une feuille »): in the
   dark theme the chart stands on a dark ground, so its washes and the key
   under them are recalculated for it. Each ink keeps its conventional hue —
   within half a degree in OKLCH — and only its lightness rises, a third of the
   way to white; the cloud turns lighter than the ground, as it does over a
   dark map, but far under the white the map allows, for the night — a pure
   grey: the vividness dial would saturate the least tint into a colour. The Map keeps its own ramps (§ 8.4):
   `profile-open` is set on the root only while the chart is the page. */
:root[data-theme="dark"].profile-open {
    --wx-cloud-thin-rgb: 58, 58, 58;
    --wx-cloud-solid-rgb: 84, 84, 84;
    --wx-cold-rgb: 97, 149, 253;
    --wx-fog-rgb: 236, 190, 64;
    --wx-fog-freezing-rgb: 196, 132, 226;
    --wx-temp-frigid-rgb: 126, 94, 170;
    --wx-temp-arctic-rgb: 132, 125, 241;
    --wx-temp-cold-rgb: 100, 161, 255;
    --wx-temp-chill-rgb: 99, 207, 252;
    --wx-temp-freezing-rgb: 146, 230, 211;
    --wx-temp-cool-rgb: 163, 229, 126;
    --wx-temp-mild-rgb: 247, 228, 101;
    --wx-temp-warm-rgb: 254, 175, 116;
    --wx-temp-hot-rgb: 253, 116, 98;
    --wx-cat-extreme-rgb: 206, 130, 205;
    --wx-cat-severe-rgb: 254, 104, 93;
    --wx-cat-alert-rgb: 253, 162, 115;
    --wx-cat-caution-rgb: 254, 220, 113;
    --wx-cat-fair-rgb: 121, 201, 121;
}
.map-wx-cover { fill: rgb(var(--wx-cloud-solid-rgb)); }
.map-wx-cover.ink-cloud-thin { fill: rgb(var(--wx-cloud-thin-rgb)); }
.map-wx-cover.ink-cloud-solid { fill: rgb(var(--wx-cloud-solid-rgb)); }
/* Fog's two inks on the map (planche PREV B.2), read through the probe. */
.map-wx-fog.ink-fog { fill: rgb(var(--wx-fog-rgb)); }
.map-wx-fog.ink-fog-freezing { fill: rgb(var(--wx-fog-freezing-rgb)); }
/* The wind, FLYING: strokes on a screen-space canvas over the map
   (wx-particles.js), the coupe's own animation transposed — and 2D, so the
   trajectories curve along the streamlines the way Windy's do. The canvas
   floats over every GL-painted layer and under every marker; its ink is read
   through the engine's probe off the class below, so the dark ground's variant
   applies without the animation being told. */
.map-wx-particles {
    position: absolute;
    inset: 0;
    z-index: calc(var(--z-page) + 1);
    width: 100%;
    height: 100%;
    pointer-events: none;
}
/* The flying trails take their colour from the WIND's own ladder, band by band,
   probed off these declarations like every other map ink (map-engine probeRgb)
   — so the colour a particle flies in and the colour the legend keys are one
   declaration, never two. The bare class is the fallback for a speed no band
   answers for. */
.map-wx-particle-ink { fill: var(--wx-wind); }
.map-wx-particle-ink.ink-wind-kt-calm { fill: rgb(var(--wx-temp-arctic-rgb)); }
.map-wx-particle-ink.ink-wind-kt-light { fill: rgb(var(--wx-temp-cold-rgb)); }
.map-wx-particle-ink.ink-wind-kt-fresh { fill: rgb(var(--wx-temp-chill-rgb)); }
.map-wx-particle-ink.ink-wind-kt-strong { fill: rgb(var(--wx-temp-cool-rgb)); }
.map-wx-particle-ink.ink-wind-kt-hard { fill: rgb(var(--wx-temp-mild-rgb)); }
.map-wx-particle-ink.ink-wind-kt-gale { fill: rgb(var(--wx-temp-warm-rgb)); }
.map-wx-particle-ink.ink-wind-kt-storm { fill: rgb(var(--wx-temp-hot-rgb)); }
/* The temperature WASH takes the cross-section's own nine bands, probed band
   by band like the flying trails (map-engine probeRgb reads
   `fill`). The wash paints its own pixels into a canvas, so it needs the
   colour itself and not a rule the engine could apply — and reading it back
   off these declarations is what keeps one temperature one colour on both
   surfaces. */
.map-wx-temp-ink { fill: rgb(var(--wx-temp-freezing-rgb)); }
.map-wx-temp-ink.ink-temp-frigid { fill: rgb(var(--wx-temp-frigid-rgb)); }
.map-wx-temp-ink.ink-temp-arctic { fill: rgb(var(--wx-temp-arctic-rgb)); }
.map-wx-temp-ink.ink-temp-cold { fill: rgb(var(--wx-temp-cold-rgb)); }
.map-wx-temp-ink.ink-temp-chill { fill: rgb(var(--wx-temp-chill-rgb)); }
.map-wx-temp-ink.ink-temp-freezing { fill: rgb(var(--wx-temp-freezing-rgb)); }
.map-wx-temp-ink.ink-temp-cool { fill: rgb(var(--wx-temp-cool-rgb)); }
.map-wx-temp-ink.ink-temp-mild { fill: rgb(var(--wx-temp-mild-rgb)); }
.map-wx-temp-ink.ink-temp-warm { fill: rgb(var(--wx-temp-warm-rgb)); }
.map-wx-temp-ink.ink-temp-hot { fill: rgb(var(--wx-temp-hot-rgb)); }
/* The value on the 0 °C line. The LINE itself carries no rule here: it wears
   the cross-section's own `.wx-freezing-line` (and its `-casing` twin), read
   back through the engine's style probe, so one fact has one appearance and
   neither surface holds a copy.
   The label cannot be shared the same way — the chart writes SVG text (fill,
   paint-order) and the map writes an HTML span (color, text-shadow) — so it is
   rebuilt here from the same two ingredients and no others: the cold ink the
   line is drawn in, and the halo every label over this map already wears. */
.map-wx-freezing-label {
    display: block;
    transform: translate(-50%, -50%);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    line-height: 1;
    white-space: nowrap;
    color: var(--wx-cold);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo);
}

/* The link out to the airspace panel, inside the card: a way through, not a
   switch, so it wears the app's own quiet text button rather than a row. */
.map-layers-link {
    display: block;
    width: 100%;
    padding: 5px 8px;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    text-align: left;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    cursor: pointer;
}
@media (hover: hover) {
    .map-layers-link:hover { background: var(--hover-bg); }
}
/* A VFR reporting point: the chart's triangle — filled when the report is
   compulsory, hollow on request — on the beacons' glass-disc recipe, one size
   down from the fields. The ink is the controlled-airspace blue: chart
   geometry, the stated exception to the layers' colour code (the guidelines'
   chart-inks section). */
.route-rp-marker,
.route-activity-marker {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Centred on its point by .map-airport-anchor > (this face). */
    width: var(--map-symbol);
    height: var(--map-symbol);
    color: var(--chart-blue);
    cursor: pointer;
}
/* The ENR 5.5 activities wear one ink, the Annex 4's and the guide's magenta
   (owner's decision, planche v3 D2). */
.route-activity-marker { color: var(--chart-magenta); }
.route-rp-marker::before,
.route-activity-marker::before {
    content: "";
    position: absolute;
    inset: calc((var(--map-symbol) - 40px) / 2);
    border-radius: 999px;
}
.route-rp-marker svg,
.route-activity-marker svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* An ENR 5.5 site on its aerodrome (planche ENR 5.5): small, in a row off the
   field symbol's upper right corner, never over it — the mirror of
   ACTIVITY_SLOT in map-layers.js (0.8 symbol across, 0.67 per slot, 0.7 up). */
.map-airport-anchor > .route-activity-marker[data-slot] {
    width: calc(var(--map-symbol) * 0.6);
    height: calc(var(--map-symbol) * 0.6);
    transform: translate(calc(-50% + var(--map-symbol) * var(--slot-x)), calc(-50% - var(--map-symbol) * 0.7));
}
.route-activity-marker[data-slot="0"] { --slot-x: 0.8; }
.route-activity-marker[data-slot="1"] { --slot-x: 1.47; }
.route-activity-marker[data-slot="2"] { --slot-x: 2.14; }
.route-activity-marker[data-slot="3"] { --slot-x: 2.81; }
/* The vector airspace polygons (map-layers.js syncAirspaceShapes): the
   EUROCONTROL 1:500 000 recipes, one class per airspaceStyle() key. The facade
   reads them back through its style probe. NO RECIPE FILLS ITS VOLUME (planche
   ESP): a line, and for the banded styles (AIRSPACE_BANDED, profile-core.js) a
   band along the inside of it — §2.1 for A and B-D, §2.3 for a CTR (the
   band, of the two depictions the guide allows), §2.6 hatched for a
   restriction. Lines only where the guide draws lines (E/F/G, ATZ, TMZ, FIS,
   nature). */
.map-airspace-shape {
    stroke-width: 1.5;
    fill: none;
}
.map-airspace-shape.as-classA { stroke: var(--chart-magenta); }
.map-airspace-shape.as-classBCD { stroke: var(--chart-blue); }
.map-airspace-shape.as-classE { stroke: var(--chart-blue); stroke-width: 2.2; }
.map-airspace-shape.as-classFG { stroke: var(--chart-blue); stroke-width: 1.2; }
.map-airspace-shape.as-other { stroke: var(--chart-blue); stroke-width: 1.2; }
.map-airspace-shape.as-ctr,
.map-airspace-shape.as-ctrA {
    stroke: var(--chart-blue);
    stroke-dasharray: 6 4;
}
.map-airspace-shape.as-atz {
    stroke: var(--chart-blue);
    stroke-width: 2;
    stroke-dasharray: 0 5;
    stroke-linecap: round;
}
.map-airspace-shape.as-tmz {
    stroke: var(--chart-grey);
    stroke-width: 2.5;
    stroke-dasharray: 10 4 1.5 4;
}
.map-airspace-shape.as-fis {
    stroke: var(--chart-green);
    stroke-dasharray: 8 5;
}
.map-airspace-shape.as-nature { stroke: var(--chart-green); }
/* The VFR routes (planche ENAIRE-INFO): a route thin, its arrowheads 5 px in
   its ink (map-layers.js vfrRouteLayerDefs); a radio failure's dashed; a
   corridor's limit thinner still, plain (owner's decision A). */
.map-vfr-route { stroke: var(--chart-navy); stroke-width: 1.4; }
.map-vfr-route.is-radio { stroke-dasharray: 5 3; }
.map-vfr-route-limit { stroke: var(--chart-navy); stroke-width: 0.8; }
.map-airspace-shape.as-restricted { stroke: var(--chart-red); }
/* One volume shown from its card (planche ESPACE B.7): its own ink, heavier. */
.map-airspace-shape.is-shown { stroke-width: 4; }
.hwx-hatch-line { stroke: var(--chart-magenta); stroke-width: 1.6; }
/* The band along the inside of a banded limit (engines/airspace_bands.py): the width
   below is what is PAINTED, from the limit inward (bandStroke, map-engine.js).
   Translucent in the outline's ink; for a restriction the magenta hatch,
   at full strength since its strokes already leave the ground showing. */
.map-airspace-band {
    fill: none;
    stroke-width: 7;
    stroke-opacity: 0.24;
}
.map-airspace-band.as-classA-band,
.map-airspace-band.as-ctrA-band { stroke: var(--chart-magenta); }
.map-airspace-band.as-classBCD-band,
.map-airspace-band.as-ctr-band { stroke: var(--chart-blue); }
.map-airspace-band.as-restricted-band {
    stroke: url(#hwx-hatch);
    stroke-opacity: 1;
}
/* §2.2.1 — where a sector of the same class lies on the other side (a shared
   limit, or the contour of a sector inside a larger one), the thinner band
   the guide recommends. Decided geometrically by the server (engines/airspace_bands.py), never by
   a designator. */
.map-airspace-band.is-thin { stroke-width: 2; }
/* An activity area out of activity (area-activity.js): FADED, NEVER HIDDEN
   (owner's decision, docs/vfr-chart-rules.md) — the same recipe, paler, so it
   no longer reads as a permanent area and is still there when it switches on. */
.map-airspace-shape.as-restricted.is-inactive,
.map-airspace-band.as-restricted-band.is-inactive { stroke-opacity: var(--spent-dim); }
/* Four weight steps down the scale, read off the ONE tier attribute the map
   writes (body[data-map-tier], syncMapTier in route-profile.js — the same
   attribute that sizes the symbols). The rules above are the Detail tier and
   stand alone; the three below take every weight down in turn, floored at 0.5
   — the owner's call, "as thin as possible": legible on a 3x phone, paler on a
   desktop screen, an accepted deviation.

   One boundary used to do this work, and it fell at Navigation while the range
   that suffers is Structure to Detail. Widths ONLY — the dash patterns, the
   inks and the washes are the chart's identity and do not move with the scale,
   which is what keeps a CTR reading as a CTR at every step. Band widths are
   painted widths, the thin band in the proportion of the full one. */
body[data-map-tier="navigation"] .map-airspace-shape { stroke-width: 1.25; }
body[data-map-tier="navigation"] .map-airspace-shape.as-classE { stroke-width: 1.85; }
body[data-map-tier="navigation"] .map-airspace-shape.as-classFG,
body[data-map-tier="navigation"] .map-airspace-shape.as-other { stroke-width: 1; }
body[data-map-tier="navigation"] .map-airspace-shape.as-atz { stroke-width: 1.65; }
body[data-map-tier="navigation"] .map-airspace-shape.as-tmz { stroke-width: 2.1; }
body[data-map-tier="navigation"] .map-airspace-band { stroke-width: 5.5; }
body[data-map-tier="navigation"] .map-airspace-band.is-thin { stroke-width: 1.6; }
/* The planned track comes down the same staircase — it was the one line on the
   map drawn at a fixed width, so at Overview it crossed a continent as thick
   as it crosses a valley. The sleeve keeps 2.5px on the line at every step:
   below that it stops being an edge. */
body[data-map-tier="navigation"] .map-route-sleeve { stroke-width: 10.5; }
body[data-map-tier="navigation"] .map-route-line { stroke-width: 4.5; }
body[data-map-tier="navigation"] .map-route-active { stroke-width: 7; }
body[data-map-tier="navigation"] .map-route-pale { stroke-width: 4.5; }

body[data-map-tier="structure"] .map-airspace-shape { stroke-width: 1; }
body[data-map-tier="structure"] .map-airspace-shape.as-classE { stroke-width: 1.4; }
body[data-map-tier="structure"] .map-airspace-shape.as-classFG,
body[data-map-tier="structure"] .map-airspace-shape.as-other { stroke-width: 0.8; }
body[data-map-tier="structure"] .map-airspace-shape.as-atz { stroke-width: 1.3; }
body[data-map-tier="structure"] .map-airspace-shape.as-tmz { stroke-width: 1.6; }
body[data-map-tier="structure"] .map-airspace-band { stroke-width: 4; }
body[data-map-tier="structure"] .map-airspace-band.is-thin { stroke-width: 1.1; }
body[data-map-tier="structure"] .map-route-sleeve { stroke-width: 9.5; }
body[data-map-tier="structure"] .map-route-line { stroke-width: 4; }
body[data-map-tier="structure"] .map-route-active { stroke-width: 6.5; }
body[data-map-tier="structure"] .map-route-pale { stroke-width: 4; }

body[data-map-tier="overview"] .map-airspace-shape { stroke-width: 0.7; }
body[data-map-tier="overview"] .map-airspace-shape.as-classE { stroke-width: 1; }
body[data-map-tier="overview"] .map-airspace-shape.as-classFG,
body[data-map-tier="overview"] .map-airspace-shape.as-other { stroke-width: 0.5; }
body[data-map-tier="overview"] .map-airspace-shape.as-atz { stroke-width: 0.9; }
body[data-map-tier="overview"] .map-airspace-shape.as-tmz { stroke-width: 1.1; }
body[data-map-tier="overview"] .map-airspace-band { stroke-width: 2.5; }
body[data-map-tier="overview"] .map-airspace-band.is-thin { stroke-width: 0.7; }
body[data-map-tier="overview"] .map-route-sleeve { stroke-width: 8; }
body[data-map-tier="overview"] .map-route-line { stroke-width: 3.5; }
body[data-map-tier="overview"] .map-route-active { stroke-width: 5.5; }
body[data-map-tier="overview"] .map-route-pale { stroke-width: 3.5; }

/* Where the pilot's own minima are broken: one rectangle per grid cell, out of
   limits for ceiling or visibility (route-profile.js drawMinimaZones). No
   colour here, and for the opposite reason to every other rule on this chart:
   it belongs to the pilot's configuration, not to the map, so it is written
   inline by the layer — the one ink in the app that is not a token, exactly as
   on the cross-section.

   No OPACITY here either, and that one is a correction: this rule fixed the
   wash at 18 % while the cross-section washed the same zones at DOT_FILL_ALPHA
   (16 %, render.js), so the two views painted one reading in two strengths. The
   alpha is written inline beside the colour now — one number, in the module
   both views read.

   The map's ONE minima wash: the corridor does not carry the same reading
   along the track, because laid over these cells it composes a third colour
   that is in neither palette nor legend — a blue stretch over a yellow cell
   reads turquoise.

   The cells are PAINTED, not shaped, in a shader on the map's own canvas
   (map-engine.js fieldLayer). There is no
   element here to style and none to take a pointer: a press falls through to
   the map and is answered from the field (minimaCellAt) rather than by a shape.
   The colour and the alpha are the pilot's own configuration, read per fragment
   from the ramp — nothing in this file may fight them. */
/* The airspace tag (route-profile.js syncAirspaceTags, EUROCONTROL
   §2.17/§2.18). ONE rule fills the square: the CLASS letter when the volume is
   classed, the TYPE code when it is not — and when both exist, the type follows
   the square as a plain word ("A TMA", "D CTR"). A class letter alone said what
   the clearance question was and never what the thing is: the thirteen sectors
   over Palma all read "A", and nothing told a TMA from the CTR beneath it.
   MAGENTA throughout for class A — square, type word and limits — because that
   is the ink of its own line and its own band (§2.1.1), and a label belongs to
   the thing it names. The square was red for a while, on the VAC's authority:
   the LEPA/LESJ sheet does print A in red, but in its LEGEND, and a legend box
   is not the body of a chart. The mixed tag that left (red square, blue text)
   agreed with neither the line nor the legend.
   Blue for B-G, and an unclassed square takes its own style's ink — an R area
   and a TMZ are not the same kind of news. Units live in the layer card's
   note, not here. */
.map-airspace-tag {
    position: absolute;
    /* One ROW, standing ON the border it letters, rotated to the run that
       carries it and shifted half a row INTO its own
       volume — the owner's call, §2.19's own words ("along the airspace
       border"), and the openAIP chart's rendering he pointed at. The angle and
       the shift are DATA, so they arrive as custom properties from
       route-profile.js (airspaceTagLabel), the --config-ink precedent; the
       translateY runs in the rotated frame, i.e. perpendicular to the text.
       The stacked column died with the seek-inside-the-shape pass: a row's
       length is now governed by the straight run under it (the run ruler),
       not by what fits in the polygon's belly. */
    transform: translate(-50%, -50%) rotate(var(--tag-rot, 0deg)) translateY(var(--tag-shift, 0px));
    display: flex;
    flex-direction: row;
    align-items: baseline;
    gap: 0.35em;
    font-size: var(--fs-2xs);
    line-height: 1.15;
    white-space: nowrap;
    pointer-events: none;
}
.map-airspace-tag-head {
    display: flex;
    align-items: center;
    gap: 4px;
}
.map-airspace-tag-class {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 14px;
    height: 14px;
    padding: 0 3px;
    border-radius: 2px;
    background: var(--chart-blue);
    color: var(--on-color-light);
    font-weight: var(--fw-black);
}
.map-airspace-tag-class.is-class-a { background: var(--chart-magenta); }
.map-airspace-tag-class.is-restricted { background: var(--chart-red); }
.map-airspace-tag-class.is-equipment { background: var(--chart-grey); }
/* The whole class A tag, not only its square: the type word and the limits
   carry the same magenta, so the text reads as the volume's and not as its
   neighbour's. */
.map-airspace-tag.is-class-a .map-airspace-tag-name,
.map-airspace-tag.is-class-a .map-airspace-tag-limits { color: var(--chart-magenta); }
/* The same rule for the restricted family (R/D/P): the whole tag reads red,
   the ink of its own line, its own band and its own hatch. A blue "GND - 3000"
   beside a red square was the mixed tag all over again — the eye reads the ink
   before the letter, and on this family the ink IS the news. */
.map-airspace-tag.is-restricted .map-airspace-tag-name,
.map-airspace-tag.is-restricted .map-airspace-tag-limits { color: var(--chart-red); }
/* An object's own text, swept by the same pass as the airspace tags
   (route-profile.js syncMapLabels): a radio aid's ident over its frequency
   (§2.26.1/§2.26.2, both "shall"), a reporting point's name. Two short lines
   rather than one long one — "CAA 114.30 MHz" is fifteen characters and would
   find nowhere to stand, while seven and ten fit under the symbol. No fill: the
   halo carries them over the imagery, the airspace limits' own trick.

   Each family stands at a FIXED offset from its own symbol — below for the
   fields, beacons and obstacles, abeam for the pilot's points and the
   reporting points — never sought, only shown or hidden. The lead derives
   from the live symbol size, so the pair tightens together as the tiers step
   down; the JS sweep mirrors the same arithmetic (mapLabelSymbolLead). */
.map-object-label {
    position: absolute;
    --map-label-lead: calc(var(--map-symbol) / 2 + 4px);
    transform: translate(-50%, -50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    font-size: var(--fs-2xs);
    line-height: 1.15;
    white-space: nowrap;
    pointer-events: none;
    color: var(--chart-blue);
    font-weight: var(--fw-medium);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo);
}
.map-object-label.is-below { transform: translate(-50%, var(--map-label-lead)); align-items: center; }
.map-object-label.is-abeam { transform: translate(var(--map-label-lead), -50%); align-items: flex-start; }
/* An airport's label takes the finger and opens its card, as its symbol does
   (owner's decision, planche CTOUCH T.2). */
.map-object-label[data-airport-label] { pointer-events: auto; cursor: pointer; }
/* Above and centred (obstacles): the elevation reads over the top of the mast.
   The lead lifts the box's BOTTOM off the symbol, so the whole label rides one
   lead above the point — mirror of the is-above branch in syncMapLabels. */
.map-object-label.is-above { transform: translate(-50%, calc(-100% - var(--map-label-lead))); align-items: center; }
.map-object-label .map-label-line.is-ident { font-weight: var(--fw-semibold); }
/* The pilot's own waypoints: the label wears the pilot's ink, the filed dot's
   — not the catalogue's chart-blue, not the route's magenta. */
.map-object-label.is-waypoint { color: var(--chart-pilot); }
/* ONE CAPSULE FOR ALL WEATHER OUT OF LIMITS (owner's decision, planche
   « marques des terrains » B, extending N5b): an unbriefed field breaking a
   minimum wears its dots on the crosshair line's frosted glass (owner's
   decision, planche AGE B.14) — a 25 % veil, a 5 px blur, no hairline: the
   second and last exception to the opaque chrome (field-state.js). */
.map-object-label.is-state {
    /* 1px of air all round its pastilles (owner's call): the line is a pastille tall. */
    line-height: 9px;
    flex-direction: row;
    align-items: center;
    gap: 3px;
    padding: 1px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--card) 25%, transparent);
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
    color: var(--text);
    text-shadow: none;
}
.map-object-label.is-state .ap-rings { gap: 2px; }
/* 9px, the size they always had (owner's call): 13px is the briefed pill's alone. */
.map-object-label.is-state .ap-ring { width: 9px; height: 9px; }
/* The type beside the class: a word, no fill, the limits' own halo — it names
   the thing, it is not a second badge competing with the first. */
/* A VFR route's track or limits, along it: the airspace tag's frame, in the
   routes' navy. */
.map-vfr-route-tag {
    position: absolute;
    transform: translate(-50%, -50%) rotate(var(--tag-rot, 0deg)) translateY(var(--tag-shift, 0px));
    font-size: var(--fs-2xs);
    line-height: 1.15;
    white-space: nowrap;
    pointer-events: none;
    color: var(--chart-navy);
    font-weight: var(--fw-semibold);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo);
}
.map-airspace-tag-name {
    color: var(--chart-blue);
    font-weight: var(--fw-semibold);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo);
}
/* One line, lower first, and the halo does what the box used to: the tag no
   longer carries a fill, so forty of them at the Detail tier stop hiding the
   ground they label. The halo goes HERE and not on the container — inherited,
   it would ring the white letter inside the class square too. */
.map-airspace-tag-limits {
    color: var(--chart-blue);
    font-weight: var(--fw-medium);
    text-shadow: 0 0 3px var(--map-label-halo), 0 0 6px var(--map-label-halo);
}
/* The NOTAM shapes/points (briefing-map.js rebuildNotamLayer): same contract —
   the ink stays inline (it follows the pilot's criticality prefs), stroke and
   wash are written here. Solid stroke, unlike the dashed airspace boundary:
   a NOTAM zone is an event, not a chart limit. */
.map-notam-shape {
    stroke-width: 2;
    fill-opacity: 0.15;
}
.map-notam-point {
    stroke-width: 2;
    fill-opacity: 0.7;
}
/* LE NOTAM QU'ON VIENT D'OUVRIR DEPUIS LA LISTE (décision du propriétaire,
   planche NOTAM vue 3) : la carte le centrait déjà, il lui manquait de se
   distinguer une fois qu'on y est. DU TRAIT, PAS UNE COULEUR DE PLUS — la forme
   garde l'encre de son sens, qui est le seul fait qu'une couleur dit dans cette
   app. Le moteur sonde ces règles pour en tirer son trait et son voile, donc un
   surlignage se décrit ici comme tous les autres traits de la carte. */
.map-notam-shape.is-focused {
    stroke-width: 4;
    fill-opacity: 0.3;
}
.map-notam-point.is-focused {
    stroke-width: 4;
    fill-opacity: 1;
}
/* CE QUE LE BRIEFING ÉCARTE, À LA DEMANDE (décision du propriétaire) : un NOTAM
   qu'un interrupteur de la carte NOTAM fait paraître (hors portée, hors fenêtre,
   filtré) se dessine PÂLE — même encre, voile et trait éteints, contour en
   pointillés — pour qu'on ne le prenne jamais pour le briefing. Un point ne peut
   pas se pointiller sous MapLibre : il pâlit seulement. */
.map-notam-shape.is-aside {
    stroke-opacity: 0.55;
    stroke-dasharray: 6 4;
    fill-opacity: 0.05;
}
.map-notam-point.is-aside {
    stroke-opacity: 0.55;
    fill-opacity: 0.25;
}
/* The NOTAM point's finger pad (briefing-map.js): a second, invisible circle on
   the same fix, 44px across against the dot's 14. pointer-events is the whole
   point — SVG's default visiblePainted ignores a fill that paints nothing, so
   without this the pad would be a transparent circle nobody can tap. */
.map-notam-hit {
    fill: transparent;
    fill-opacity: 0;
    pointer-events: all;
    cursor: pointer;
}
/* The tapped-point airspace card leads each volume with the legend's dashed
   swatch — inline within the fact list rather than in a legend row. */
.airport-fact-key .route-legend-swatch {
    display: inline-block;
    vertical-align: baseline;
    margin-right: 6px;
}
/* What the layer could not draw, said where the pilot is already looking. */
.map-layers-note {
    margin: 2px 4px 0;
    font-size: var(--fs-xs);
    color: var(--muted);
}
.map-layers-note.is-end { text-align: right; }
/* The credit a licence asks for, at the foot of the card that switches its data
   on. Quieter and smaller than a note — it explains nothing, it names a source —
   but its link stays a link: every one of these licences asks its name to carry
   one, and a credit nobody can follow is not a credit. */
.map-layers-credit {
    margin-top: 6px;
    font-size: var(--fs-2xs);
    opacity: 0.85;
}
.map-layers-credit a { color: inherit; }
/* The chip and the class letter inside a filter row: legend, before the label. */
.wx-filter-toggle .route-legend-swatch { flex: none; }
.wx-filter-toggle .map-airspace-tag-class { flex: none; font-size: var(--fs-2xs); }
/* A beacon: the aerodrome disc, one size down and without the shadow — it is
   context under the fields and the route, not a place to go. */
.route-navaid-marker {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Centred on its point by .map-airport-anchor > (this face). */
    width: var(--map-symbol);
    height: var(--map-symbol);
    /* Chart ink: a navaid symbol reads the way the AIP chart writes it. */
    color: var(--chart-blue);
    cursor: pointer;
}
/* The relief tint's inks, read by the engine's probe (map-engine.js
   MapLibreReliefTint): fill and fill-opacity, one class a band. */
.map-terrain-above { fill: var(--terrain-above); fill-opacity: var(--terrain-above-alpha); }
.map-terrain-near { fill: var(--terrain-near); fill-opacity: var(--terrain-near-alpha); }
/* TERRAIN'S KEY, UPRIGHT (owner's decision, planche HEMS C.3): red on top,
   as an altimeter reads; each bound written at the foot of its band, in ft
   under the GPS altitude. Its bands wear the tint's own inks
   and opacities, on the map. */
.map-terrain-key {
    display: grid;
    grid-template-columns: 8px auto;
    column-gap: 6px;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--fs-xs);
    line-height: 1.1;
    text-align: start;
    color: var(--text);
    font-variant-numeric: tabular-nums;
    pointer-events: auto;
    cursor: pointer;
}
.map-terrain-key[hidden] { display: none; }
.map-terrain-key-name { grid-column: 1 / 3; justify-self: start; margin-bottom: 4px; font-weight: var(--fw-medium); color: var(--heading); }
.map-terrain-key-bar { grid-row: 2 / 4; display: flex; flex-direction: column; border-radius: 999px; overflow: hidden; }
.map-terrain-key-bar i { flex: 1; display: block; }
.map-terrain-key-bar .map-terrain-above { background: var(--terrain-above); opacity: var(--terrain-above-alpha); }
.map-terrain-key-bar .map-terrain-near { background: var(--terrain-near); opacity: var(--terrain-near-alpha); }
.map-terrain-key-mark { height: 22px; display: flex; align-items: flex-end; translate: 0 0.55em; }
/* An obstacle in a band (planche HEMS A.4): its ink, and a ring on the card's
   ground so it parts from the tint it stands on. */
.route-obstacle-marker[data-terrain] {
    border-radius: 999px;
    box-shadow: 0 0 0 2px var(--card), 0 0 0 4px currentColor;
}
.route-obstacle-marker[data-terrain="above"] { color: var(--terrain-above); }
.route-obstacle-marker[data-terrain="near"] { color: var(--terrain-near-ink); }
/* Obstacles (§2.27): the navaid recipe. On the map a press opens the card
   (planche OBST D.2), so the hand and the touch pad; on the profile the marks
   take no touch (.rp-marks). */
.route-obstacle-marker {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Centred on its point by .map-airport-anchor > (this face). */
    width: var(--map-symbol);
    height: var(--map-symbol);
    color: var(--chart-blue);
}
.map-airport-anchor > .route-obstacle-marker { cursor: pointer; }
.route-navaid-marker::before,
.map-airport-anchor > .route-obstacle-marker::before {
    content: "";
    position: absolute;
    inset: calc((var(--map-symbol) - 40px) / 2);
    border-radius: 999px;
}
/* Obstacles share the rule, not just the box. They had no svg rule of their own
   at all, so their paths fell back to the SVG default — fill black, stroke none
   — and the caret's two strokes are open lines, which enclose no area and so
   drew NOTHING. All that reached the map was the base dot and the casing's
   outline: the mark was being drawn by its own halo. Naming both selectors here
   is what makes "the navaid recipe" true rather than a comment. */
.route-navaid-marker svg,
.route-obstacle-marker svg {
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
/* How big a chart symbol is drawn, by tier (route-profile.js syncMapTier —
   the same body attribute that steps the airspace stroke weights: one
   staircase, one writer). Stacked symbols used to be GATHERED into a counting
   badge; that went out on the owner's call, for the answer a paper chart gives
   the same problem — the symbols get smaller as the scale widens, and stay
   symbols. Four steps, and everything below reads ONE number: no setStyle,
   no redraw, and the marker markup never learns about it.

   FOUR, because Structure and Overview shared 16px: the staircase stopped at
   z7 and the whole wide half of the range — where the crowding it exists to
   answer is at its worst — had no step left in it. Every symbol family sizes
   itself from this one number, the pilot's own filed dots included, at half
   the symbol since a dot names nothing.

   The Overview value paints nothing TODAY and is kept on purpose: the shared
   gate sits on the Structure boundary, so no symbol layer currently draws
   below it. It is the answer waiting for the first layer that does — without
   it such a layer would fall back to the 24px of the bare body rule, which is
   Detail's size at world scale, the exact defect this staircase exists for.

   The TOUCH TARGET does not follow the symbol down. A shrinking symbol is a
   chart decision; a shrinking target is a missed tap. The pads are computed
   off the symbol so their outer box stays 40px at every step — which is why
   they are a calc and not the -8px they used to be. */
/* The bare rule is the Detail step, which a surface may also ask for outright
   (the cross-section's marks, planche COUPE-OBJ D.3). */
body, [data-map-tier="detail"] { --map-symbol: 24px; }
body[data-map-tier="navigation"] { --map-symbol: 18px; }
body[data-map-tier="structure"] { --map-symbol: 14px; }
body[data-map-tier="overview"] { --map-symbol: 11px; }

/* The tapped-point menu, on the map and on the cross-section alike: what the
   point is, then what can be done to it. The app's own white card. */
.route-point-menu {
    position: fixed;
    z-index: var(--z-overlay);
    min-width: 12rem;
    padding: 6px;
    background: var(--surface);
    border: 1px solid var(--soft-line);
    border-radius: var(--radius-md);
    box-shadow: 0 0 0 1px var(--contour);
}
.route-point-menu-title {
    padding: 4px 8px 6px;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    max-width: 16rem;
}
/* What the active weather layer reads at that point, under the point's name:
   the answer the tap would have given had the fix not taken it. */
/* The fact line under the title: where the point is, and how high. It had no
   caller until the filed dot's card started answering with one, and it was
   dressed as a second title — semibold in --heading, the same weight as the
   name above it. A position is not a name: it reads as the list's own sub-line
   does (muted, small), so the eye takes the title first. */
.route-point-menu-note {
    padding: 0 8px 6px;
    color: var(--muted);
    font-size: var(--fs-xs);
}
.route-point-menu-btn {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 7px 8px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
    text-align: left;
    cursor: pointer;
}
/* Each answer led by its sign, in the ink of the answer itself — so the
   removal's cross reads red with the words it belongs to. */
.route-point-menu-btn svg {
    flex: none;
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
@media (hover: hover) {
    .route-point-menu-btn:hover { background: var(--hover-bg); }
}
.route-point-menu-btn.danger { color: var(--muted); }

/* The chart's planned line invites the hand: a grab on it bends the leg. */
.route-profile-svg .rp-route-line { cursor: copy; }

/* The cross-section page: the Route family's second page. An ordinary page
   section in the content column — normal header above, body scroll — sized by
   its own content (the chart draws at display width). */
/* No horizontal padding of its own: the text blocks (meta, controls, legs,
   legend, attribution) sit on the page's own gutter, which .main already
   carries, and the chart escapes that gutter to run the full width. */
/* The chart page is exactly one screen: the column locks to the viewport, the
   head and foot take what they need, and the chart itself takes every pixel
   left — so the source line lands on the bottom edge instead of leaving a gap
   or pushing the page into a scroll.
   The height is taken from the body rather than set to 100dvh on the column:
   the install banner rides above the shell in the flow, and a fixed 100dvh
   under it would overflow the screen by exactly the banner's height. */
body.route-profile-open {
    display: flex;
    flex-direction: column;
    height: 100dvh;
    overflow: hidden;
}
/* The shell stops asking for a viewport of its own (min-height: 100vh) and
   takes what the body hands it; both columns stretch to that, so the sidebar's
   own 100vh does not push the page into a scroll either. */
body.route-profile-open .app-shell {
    flex: 1 1 auto;
    min-height: 0;
    align-items: stretch;
}
body.route-profile-open .sidebar { height: auto; min-height: 0; }
/* The height comes from the shell both ways: stretched by the grid on desktop,
   and taken as 100% of it on a phone, where the shell is an ordinary block. */
body.route-profile-open .main {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    overflow: hidden;
    padding-bottom: 0;
}
.route-profile-view {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* The transport bar floats (it is a dock, see .route-profile-timebar), so the
       column below the chart reserves the band it occupies rather than letting
       the chart run under it. The band is MEASURED (--route-capsule-h, written
       by the chart when it builds the capsule) and not counted by hand: the
       capsule carries a legend now, whose height follows the ramps the pilot
       lit, and a hand-counted 84px hid the distance axis and both endpoint
       codes under it. The variable is the whole band, capsule top to screen
       foot, so the menu's clearance and the safe area are already in it. The
       fallback covers the frame before the first measurement. */
    padding: 0 0 calc(var(--route-capsule-h, 160px) + 8px);
}
.route-profile-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
@media (max-width: 840px), (max-height: 500px) {
    /* Same reservation as above, against the phone's smaller dock inset (the
       docks keep only 40% of the home-indicator area here). */
    .route-profile-view { padding: 0 0 calc(var(--route-capsule-h, 160px) + 8px); }
}
/* Over the drawing the header is a surface of its own, not the ground (§ 8.12). */
/* …and the chart starts right under it, with no band of air between
   (owner's decision). */
body.route-profile-open .page-header { --head-fade: var(--surface); margin-bottom: 0; }
/* While the chart page is up, the briefing surfaces under it step aside the
   same way they do for the full-page maps. */
body.route-profile-open #briefing,
body.route-profile-open .briefing-summary { display: none; }
.route-profile-message { margin: 32px 0; text-align: center; color: var(--muted); }
/* A message with one thing to do about it — the empty page's "open the route
   panel", a refused refresh's "Retry". The word above, the button under it,
   centred together where the chart would be; the message gives up its own
   margins to the stack's gap. */
.route-profile-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    margin: 32px 0;
}
.route-profile-empty .route-profile-message { margin: 0; }
/* The wait for the first chart: the cross-section's own glyph, breathing in the
   middle of the empty page — `margin: auto` in the column flexbox above centres
   it both ways. It borrows the splash's own breath rather than inventing a
   second one, so the app has ONE way of saying "working", and stops with it
   under prefers-reduced-motion. A mark, not a sentence: there is nothing to read
   while a fetch runs. Refusals and errors still come back as words. */
.route-profile-busy {
    margin: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    color: var(--muted);
}
.route-profile-busy p { margin: 0; font-size: var(--fs-md); }
/* A new response fetched under a chart already drawn: the chart stays, and a
   small pill over its top says it is loading. */
.route-profile-loading {
    position: absolute;
    top: 8px;
    left: 50%;
    z-index: var(--z-sticky);
    transform: translateX(-50%);
    margin: 0;
    padding: 4px 12px;
    border-radius: 999px;
    background: var(--surface);
    box-shadow: 0 0 0 1px var(--contour);
    color: var(--muted);
    font-size: var(--fs-sm);
    pointer-events: none;
}
.route-profile-busy svg {
    width: 64px;
    height: 64px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    animation: app-splash-pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
    .route-profile-busy svg { opacity: 0.7; }
}
/* The chart is the page, not a card in it: no radius, no padding, and negative
   margins that undo the page's own gutters so the plot runs from edge to edge —
   a cross-section read at a glance wants every pixel of width. The white ground
   stays (the ink, the halos and the cloud wash are all drawn against it). */
.route-profile-chart {
    /* The wind's canvas is placed inside it, over the plot's own rectangle. */
    position: relative;
    /* Everything else on the page asks for the height it needs; the chart takes
       what is left, so the page ends exactly at the bottom of the screen. The
       basis is 0 rather than the drawing's own height: the leftover decides the
       chart, never the other way round (the SVG is then drawn at that height). */
    flex: 1 1 0;
    min-height: 200px;
    overflow: hidden;
    /* Full bleed: give back exactly the column's pad, whatever it is at this
       width — the token is the one .main pads on, so the bleed cannot drift
       from the padding it undoes. */
    margin-inline: calc(-1 * var(--col-pad));
    /* No ground of its own: the weather, the terrain and the airspace are drawn
       straight onto the page, so the chart belongs to it rather than sitting on
       a white card of its own. */
    /* The zoom/pan gestures own the surface: no native scroll/pinch here, and
       no text selection either — a drag that selected a label would start a
       NATIVE drag on the next grab and cancel the gesture (pointercancel). */
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
}
/* The chart's settings are SYSTEM ROWS now (owner's ask, planche 20), one per
   line under a hairline, exactly like every other sheet in the app: they used
   to be three fields sliding sideways on one line, which is a vocabulary
   nothing else here speaks. */
.route-profile-controls {
    display: flex;
    flex-direction: column;
    font-size: var(--fs-md);
}
/* And the Model value follows the model that is CHOSEN, not the longest one on
   offer. A menu elsewhere holds its width open with a hidden copy of its widest
   label so the fields beside it never jump when the choice changes — worth it
   in a form of stacked rows. Here the fields sit on one line with nothing but
   slack to their right, so there is nothing to jump: all that hidden label did
   was reserve room for a name nobody picked. */

/* A strip holding more than the screen shows fades at the cut edge instead of
   ending in a hard crop that reads as "this is all there is". syncStripCut
   (route-profile.js) marks which edges are cut; each side's fade switches on
   only while content actually continues past it, so the strip scrolled to its
   end finishes clean. */
.route-profile-controls.is-cut {
    --cut-left: 0px;
    --cut-right: 0px;
    mask-image: linear-gradient(to right,
        transparent, #000 var(--cut-left),
        #000 calc(100% - var(--cut-right)), transparent);
    -webkit-mask-image: linear-gradient(to right,
        transparent, #000 var(--cut-left),
        #000 calc(100% - var(--cut-right)), transparent);
}
.route-profile-controls.is-cut:not(.at-start) { --cut-left: 24px; }
.route-profile-controls.is-cut:not(.at-end) { --cut-right: 24px; }

/* A row narrowed by a filter says so in its value's ink (--muted) — a filter set three sessions ago must never be invisible,
   on either view. */
.sheet-row.filtered .sheet-v { color: var(--muted); }
/* One ramp per layer on screen, a row of its own, not a chip among chips —
   in columns of 340 px at least where there is room (owner's choice L1,
   planche ISOLEG E.1): one on a phone in portrait, two to four wider. */
.route-chart-legend {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 6px 28px;
    margin-top: 8px;
    color: var(--muted);
    font-size: var(--fs-xs);
}
.route-legend-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }

.route-legend-swatch {
    width: 14px;
    height: 10px;
    border: 1px solid var(--soft-line);
    border-radius: 2px;
}
/* Airspace: the same chip, dashed and washed in the band's own ink — a legend
   has to look like the thing it explains. */
/* The rain row's own chips, each wearing the mark it explains: the snow chip
   the cold ink the columns take, the freezing-rain chip the red rim, the
   shower chip the dashed outline, the fog chip the dashed --muted line at the
   ground —
   a chip must look like the thing it stands for. */
/* The iso-tachs' two sides. A BAR, not a square: what is on the chart is a
   line, and the chip has to look like the thing it explains. */
.route-legend-swatch.wl-head,
.route-legend-swatch.wl-tail { height: 3px; border: 0; border-radius: 2px; }
.route-legend-swatch.wl-head { background: var(--wind-head); }
.route-legend-swatch.wl-tail { background: var(--wind-tail); }
.route-legend-swatch.pl-snow { background: rgba(var(--wx-cold-rgb), 0.5); }
.route-legend-swatch.pl-frza { border: 1.5px solid var(--red); }
.route-legend-swatch.pl-shower { border: 1.2px dashed var(--muted); }
.rp-precip-legend { gap: 12px; }
/* One chip per airspace style, wherever a style is named — the tapped-point
   card, the panel's type rows, the cross-section legend. Each borrows its own
   boundary's ink and writing, at chip size: the banded styles show their band
   around a middle left the colour of the ground, never a wash (planche ESP). */
.route-legend-swatch.as-restricted,
.route-legend-swatch.as-classA,
.route-legend-swatch.as-ctrA,
.route-legend-swatch.as-classBCD,
.route-legend-swatch.as-ctr,
.route-legend-swatch.as-byClass,
.route-legend-swatch.as-ctrByClass { padding: 2px; }
.route-legend-swatch.as-restricted {
    border: 1px solid var(--chart-red);
    background: linear-gradient(var(--surface), var(--surface)) content-box,
        repeating-linear-gradient(45deg, var(--chart-magenta) 0 1px, transparent 1px 3px) padding-box;
}
.route-legend-swatch.as-classA,
.route-legend-swatch.as-ctrA {
    border: 1px solid var(--chart-magenta);
    background: linear-gradient(var(--surface), var(--surface)) content-box,
        rgba(var(--chart-magenta-rgb), 0.35) padding-box;
}
.route-legend-swatch.as-ctrA { border-style: dashed; }
.route-legend-swatch.as-classBCD,
.route-legend-swatch.as-ctr {
    border: 1px solid var(--chart-blue);
    background: linear-gradient(var(--surface), var(--surface)) content-box,
        rgba(var(--chart-blue-rgb), 0.35) padding-box;
}
.route-legend-swatch.as-ctr { border-style: dashed; }
.route-legend-swatch.as-classE,
.route-legend-swatch.as-classFG,
.route-legend-swatch.as-other { border: 1px solid var(--chart-blue); }
/* Drawn BY ICAO CLASS: the same TMA line comes out magenta in Class A and blue
   in Class D, so no single chip can tell the truth. The square is split — the
   two inks the class chooses between — and the group's note says it in words.
   The class filter sitting right below carries the letters themselves. */
.route-legend-swatch.as-byClass,
.route-legend-swatch.as-ctrByClass {
    border: 1px solid var(--chart-blue);
    background: linear-gradient(var(--surface), var(--surface)) content-box,
        linear-gradient(to right, rgba(var(--chart-magenta-rgb), 0.35) 50%, rgba(var(--chart-blue-rgb), 0.35) 50%) padding-box;
}
.route-legend-swatch.as-ctrByClass { border-style: dashed; }
.route-legend-swatch.as-atz { border: 1px dotted var(--chart-blue); }
.route-legend-swatch.as-tmz { border: 1px dashed var(--chart-grey); }
.route-legend-swatch.as-fis { border: 1px dashed var(--chart-green); }
.route-legend-swatch.as-nature { border: 1px solid var(--chart-green); }
/* A layer's ramp: its name, the graded bar itself, the marks under it, its unit.
   One row per layer on screen. The numbers come from the layer's own scale
   (scaleLegend, profile-core.js) as custom properties — the share of the bar a
   band takes, the opacity at each of its ends, where a mark falls — so the bar
   is the wash on the chart, at legend size. */
.rp-scale {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    width: 100%;
}
.rp-scale-name {
    flex: 0 0 auto;
    min-width: 42px;
    color: var(--heading);
    font-weight: var(--fw-medium);
}
/* A THIN BAR UNDER ITS NUMBERS (owner's decision, planche MAP 4, L5): each
   bound written over the line it marks, in the text ink — no number stands
   on a colour. The track keeps the numbers' height above the bar, so the
   bar is the row's bottom edge — and no more than that height: the numbers sit
   right over the bar (owner's call). */
.rp-scale-track { position: relative; flex: 1 1 140px; min-width: 0; padding-top: 14px; }
.rp-scale-bar {
    display: flex;
    height: 6px;
    overflow: hidden;
    /* Rounded at both ends, as a scale is: the ramp is a shape, not a cell of a
       table. The contour rides inside as a shadow so it follows the radius and
       costs no height — a pale first band on white paper needs an edge. */
    border: 0;
    border-radius: 999px;
    box-shadow: inset 0 0 0 1px var(--soft-line);
    /* The chart's own ground under the wash, so a band's opacity reads the way
       it reads on the chart. */
    background: var(--surface);
}
/* ONE rule, because ONE place decides a colour. Every band arrives with its
   three channels already resolved — through the same reader the map's shader
   and the chart's pixels resolve theirs with, so the operator's VIVIDNESS dial
   is in them — and with its two weights already taken through his OPACITY dial.
   The stylesheet held one rule per ink and resolved the token itself, at full
   strength: a key at 100 % over a wash at 50, and a second painter nobody had
   named. The tokens still live here; nothing reads them for a legend any more. */
.rp-scale-band {
    flex-grow: var(--share);
    background-image: linear-gradient(to right, rgba(var(--band-rgb), var(--a0)), rgba(var(--band-rgb), var(--a1)));
}
/* Over the bar, each number centred on its value's position. */
.rp-scale-marks {
    position: absolute;
    inset: 0 0 auto;
    height: 14px;
    pointer-events: none;
}
.rp-scale-mark {
    position: absolute;
    top: 0;
    left: calc(var(--at) * 100%);
    transform: translateX(-50%);
    white-space: nowrap;
    font-size: var(--fs-2xs);
    /* Not bold (owner's call): a number names a bound, the bar carries the reading. */
    font-weight: var(--fw-regular);
    line-height: 14px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.rp-scale-mark.at-start { transform: none; }
/* UNDER MINIMA, EACH CONFIGURATION NAMED OVER ITS BAND (owner's decision): its
   plain name, no badge, not bold like the limits beside it, on the limits' own
   line, centred in what its band leaves free between them (`--inset-l` /
   `--inset-r`, wx-legend.js). */
.rp-scale-configs {
    position: absolute;
    inset: 0 0 auto;
    height: 14px;
    pointer-events: none;
}
.rp-scale-config {
    position: absolute;
    top: 0;
    left: calc(var(--from) * 100% + var(--inset-l));
    right: calc((1 - var(--to)) * 100% + var(--inset-r));
    overflow: hidden;
    text-align: center;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    line-height: 14px;
    color: var(--text);
}
.rp-scale-mark.at-end { transform: translateX(-100%); }
/* The rain's bounds under its bar, touching it, its class words on the line
   over it (planche RSEUIL F.3): the bar stands in the row's middle, so the
   name and the unit, centred on the row, are centred on the bar (H.2). */
.rp-scale.marks-below { align-items: center; }
.rp-scale.marks-below .rp-scale-track { padding-bottom: 13px; }
.rp-scale.marks-below .rp-scale-marks { top: auto; bottom: 0; height: 13px; }
.rp-scale.marks-below .rp-scale-mark { line-height: 13px; }
/* On the map the layer's icon and name, in their own column, centre on that
   bar too (RSEUIL H.2) — not on its bottom edge as on the other ramps. */
.map-wx-legend:has(> .map-layer-anchor:not([hidden])):has(.rp-scale.marks-below) { align-items: center; }
.map-wx-legend:has(> .map-layer-anchor:not([hidden])):has(.rp-scale.marks-below) .map-layer-pick { translate: none; }
.rp-scale.marks-below > .rp-scale-unit,
#mapWxRamps .rp-scale.marks-below > .rp-scale-unit { align-self: center; translate: none; }
/* A mark that would overlap the one before it steps aside (map-core.js thinCrowdedMarks). */
.rp-scale-mark.is-crowded { visibility: hidden; }
/* ON THE MAP THE LAYER IS NAMED ONCE, on the capsule's line (#mapLayerPick):
   the ramp takes the whole width. The cross-section keeps its names — it has
   no picker to say them. */
#mapWxRamps .rp-scale-name { display: none; }
/* Cloud type's row names are its FAMILIES, not the layer's: they stay. */
#mapWxRamps .ct-key-row .rp-scale-name { display: inline; }
/* On the map the unit ends on the bar like the layer's name (owner's ask):
   its letters, not its box — lowered by a descender, the name's way. */
#mapWxRamps .rp-scale-unit { align-self: flex-end; translate: 0 0.2em; }
/* The unit reads with the numbers, on their line over the bar. */
.rp-scale-unit { flex: 0 0 auto; align-self: flex-start; line-height: 14px; white-space: nowrap; }
/* THE UNIT AS A CONTROL (owner's decision, planche RADAR C.3): the word and a
   chevron, no ground, the app's own wheel behind it. */
.rp-scale-unit-pick {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.rp-scale-unit-pick svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: var(--icon-stroke); }
/* A layer that breaks one of the pilot's own minima wears that configuration's
   dot on its own switch — the same pastille the briefing and the airport cards
   use, at the size a pill can carry. */
.rp-layer-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-left: 5px;
    vertical-align: 1px;
    box-sizing: border-box;
    border-radius: 999px;
    /* The fallback ring wash matches DOT_FILL_ALPHA (render.js), the one
       alpha every colour dot in the app rings itself with. */
    border: 1px solid var(--dot-ink, rgba(var(--text-rgb), 0.16));
    background: var(--dot-color, var(--red));
}
/* The pilot's own ceiling limits, each drawn where it actually sits: the judged
   ground carried up by that configuration's value. This is the line a pilot
   flies against — cloud above it is within limits, cloud below it is not — so
   it is the one thing on this chart drawn firmly in the configuration's ink
   while the zone it explains stays a wash. Stepped, because the ground under it
   is one value per model cell.

   The colour is set per path: it belongs to the pilot's configuration, not to
   the chart, so it is the one stroke here that is not a token. */
.route-profile-svg .rp-ceiling-limit {
    fill: none;
    /* A HAIRLINE (owner's call). The ceiling line beside it wears the very
       same configuration ink where it breaks that limit, so
       weight is what tells the two apart: the thick line is the WEATHER, this
       one is the RULE it is judged against. */
    stroke-width: 0.9;
    stroke-linejoin: round;
    stroke-linecap: round;
}
/* The airspace inks are a convention, not a scale: they keep the two chips. */
.rp-legend-chips { display: flex; flex-wrap: wrap; gap: 6px 14px; width: 100%; }
/* Drawn at display size (measureChartLayout), so it never needs to scroll:
   the whole route is on screen, last waypoint included. */
.route-profile-svg { display: block; width: 100%; height: auto; }
/* The transport bar sits in the page flow here rather than floating over a map,
   so the row only has to hold it. */
/* THE CROSS-SECTION'S HOUR LINE (owner's decisions): the hour at the left,
   Model | Minima at the right. The header's Weather disc opens the picker, and
   stands black with a white cloud while it is open. */
#routeProfileTimebar .hour-stepper { justify-content: flex-start; }
body.route-profile-open .header-actions #mapWeatherBtn[aria-expanded="true"] {
    border-color: var(--text);
    background: var(--text);
}
body.route-profile-open .header-actions #mapWeatherBtn[aria-expanded="true"] svg { stroke: var(--bg); }
/* THE DOOR TO THE LIT LAYER'S SHEET (owner's decision, planche LEGENDE G.15):
   in the sheet's head, before the cross, away from the tiles a press would
   switch. */
.map-wx-about {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 28px;
    padding: 0 6px;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    cursor: pointer;
}
.map-wx-about::after { content: ""; position: absolute; inset: -8px -4px; }
.map-wx-about svg {
    flex: none;
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke);
    stroke-linecap: round;
}
/* THE LAYER'S SHEET (owner's decision, planche LEGENDE T1): three columns
   on the sheet's ground, a hairline between them, no card; the source set
   forth under them, then what the layer shows, quiet. */
.wx-info-times {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    margin: 4px 16px 14px;
}
.wx-info-cell {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding: 2px 12px;
}
.wx-info-cell:first-child { padding-left: 0; }
.wx-info-cell + .wx-info-cell { box-shadow: inset 1px 0 0 var(--soft-line); }
.wx-info-cell b,
.wx-info-source b { color: var(--text); font-weight: var(--fw-semibold); }
.wx-info-cell b { font-size: var(--fs-read); white-space: nowrap; }
.wx-info-k {
    color: var(--muted);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.wx-info-s { color: var(--muted); font-size: var(--fs-2xs); }
.wx-info-source { display: flex; flex-direction: column; gap: 3px; margin: 0 16px 6px; }
.wx-info-source b { font-size: var(--fs-md); }
/* The credit's links wear the line's ink, as every credit does (G.16). */
.wx-info-source a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* THE LIST OF EVERY LAYER (owner's decision, planche COUCHES F.7, F.8): two
   cards, Observed and Forecast, on the sheet's ground; in each, a source's
   hours and credit, and every layer in three short lines — what it is, its
   variable, what HeliWX does. */
.wx-info-card { margin: 0 12px var(--card-gap); }
.wx-info-card-body { padding: 0 0 12px; }
.wx-info-card .wx-info-times { margin-top: 10px; }
.wx-info-entry { margin: 12px 16px 0; }
.wx-info-layer { margin: 0; color: var(--text); font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
.wx-info-note { margin: 2px 0 0; color: var(--muted); font-size: var(--fs-sm); line-height: 1.45; }
.wx-info-line { display: flex; gap: 8px; margin: 3px 0 0; color: var(--muted); font-size: var(--fs-xs); line-height: 1.4; }
.wx-info-line .wx-info-k { flex: 0 0 76px; line-height: 1.6; }
/* A chosen layer with nothing on this route: its name, and the words. */
.rp-scale-none { color: var(--muted); font-size: var(--fs-xs); }
/* THE CROSS-SECTION'S LEGEND, ONE LINE A LAYER (owner's decision, planche
   ISOLEG D.1–D.3): the name in a column of its own, so every bar starts on one
   x; the bar takes what is left, the unit a column too, so none runs off the
   screen; every row the height of a ramp's (its numbers over its bar), a row
   with nothing to show included. The capsule on ONE line keeps its own ramp
   width (layoutCapsule). The rain's kinds are the one row allowed a second line. */
.route-chart-legend > .rp-scale { flex-wrap: nowrap; min-height: 20px; }
.route-chart-legend > .rp-scale-chips { flex-wrap: wrap; }
.route-chart-legend .rp-scale-name { flex: 0 0 52px; min-width: 0; }
.map-timebar:not(.is-one-line) .route-chart-legend .rp-scale-track { flex: 1 1 0; min-width: 0; }
.route-chart-legend .rp-scale-unit { flex: 0 0 32px; }
.route-chart-legend .rp-scale-none { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The SVG layers, styled here so the JS carries geometry only. Clouds are a
   text-ink wash whose per-cell fill-opacity encodes cloud cover; terrain wears
   the two GRAMET lines (corridor max light, under-track dark); the 0°C
   isotherm is the alert red, dashed, always AMSL. */
.route-profile-svg .rp-gridline { stroke: var(--soft-line); stroke-width: 1; }
/* The altitude scale is drawn inside the plot, so it wears the same --surface
   halo as the airspace labels to stay readable over the weather and the ridge
   under it. */
.route-profile-svg .rp-grid-label {
    fill: var(--muted);
    font-size: var(--fs-2xs);
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3;
    stroke-linejoin: round;
}
/* Iso-lines over the temperature and wind washes: each line is painted in ITS
   OWN VALUE'S colour off the layer's palette (CONTOUR_INK, route-profile.js),
   reaching here as --rp-iso on the level's own group — the numbers are the
   chart's, the way of drawing them is the stylesheet's.

   An ISO-TACH also carries --rp-iso-opacity: the exact weight the flying arrows
   are drawn at (windMarkOpacity), so a 20 kt line and a 20 kt arrow are the same
   green, weight included. Its 0 kt line lands on that ramp's floor and keeps the
   neutral stroke — the wash paints no colour where the scale means "neither", and
   the head/tail turn is the LIGHTEST wind there is, not the firmest thing on the
   chart. The opacities below are therefore only the DEFAULT.

   No level is drawn firmer than another any more. A zero USED to be: the rule
   was written for the 0 °C isotherm, back when it was a contour among contours.
   It is not one now — the freezing level carries that degree, as its own firm
   and SOLID line — so the only zero left to catch the rule was the wind's
   0 kt, the one line it was written to exclude.

   The labels reuse the grid-label recipe (halo included) and override only its
   ink: they never take the line's opacity, because an annotation at a soft
   level's own 30 % would be a number nobody could read. */
.route-profile-svg .rp-contour {
    fill: none;
    stroke: var(--rp-iso, var(--muted));
    stroke-opacity: var(--rp-iso-opacity, 0.75);
    stroke-width: 1;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.route-profile-svg .rp-contour-label {
    fill: var(--rp-iso, var(--muted));
    text-anchor: middle;
}
.route-profile-svg .rp-wp-line { stroke: var(--line); stroke-width: 1; stroke-dasharray: 2 3; }
.route-profile-svg .rp-axis-label { fill: var(--muted); font-size: var(--fs-2xs); text-anchor: middle; }
/* THE ROUTE'S OWN OBJECTS, IN THE MAP'S MARKUP (planche COUPE-OBJ): one HTML
   layer over the drawing, at one CSS pixel per chart unit, carrying the map's
   waypoint discs, camera pins, obstacle marks, names and briefed capsules under
   the map's own classes, at the Detail step (data-map-tier). It takes no touch
   — the route's points are grabbed by their geometry — save the cameras, whose
   tap opens their card. Each mark is its own stacking context, so the later
   ones (the obstacles) are drawn over the earlier (the discs). */
.rp-marks { position: absolute; inset: 0; pointer-events: none; }
.rp-mark { position: absolute; left: var(--x); top: var(--y); isolation: isolate; display: flex; }
.rp-mark.is-centred { transform: translate(-50%, -50%); }
.rp-mark.is-measuring { visibility: hidden; }
.rp-marks .route-cam-marker { pointer-events: auto; }
/* A mark that opens the map's card takes the finger (planche COUPE-NAV). */
.rp-marks .rp-mark[data-tap] { pointer-events: auto; cursor: pointer; }
/* An obstacle drawn to scale, as tall as its box (obstacleMarkBox). */
.route-obstacle-marker.is-scaled { height: var(--mark-h); }
/* A briefed field's capsule in the names' row: centred on its point, no pointer. */
.map-airport-marker.in-row { transform: translate(-50%, -50%); cursor: default; }
.map-airport-marker.in-row::after { display: none; }
/* The field layer painting the body of the chart is one image, read wherever the
   field changes and stretched over the plot. It is drawn as a MOSAIC of those
   readings, not interpolated between them (owner's call): a class frontier is
   the thing being read, and an interpolation that carries one into the next
   takes the reading away. The canvas is painted at its final width
   (PAINT_COLUMNS, profile-chart.js) and there is nothing to enlarge, which
   makes this line the LAST guard:
   without it the browser would soften the whole thing on its own stretch to the
   plot. The inks it is painted in are the tokens below, read back from here at
   render time (INK_VARS). */
.route-profile-svg .rp-field image { image-rendering: pixelated; }
/* The wind in the plane of the section: arrows flying with the flight or
   against it, one particle at a time on a canvas of their own. It sits UNDER
   the chart's SVG, so the route, the minima zones and the labels are read
   THROUGH the wind rather than from behind it. The plot's own rectangle reaches
   it as lengths from route-profile.js — a canvas has to be told where the
   drawing is, and every arrow's colour and width is painted there too, off the
   same scale the legend explains. */
.route-profile-chart .rp-wind-flow {
    position: absolute;
    left: var(--flow-left, 0);
    top: var(--flow-top, 0);
    width: var(--flow-width, 0);
    height: var(--flow-height, 0);
    pointer-events: none;
}
/* A chip rides on the END of its scale row (the rain's kinds), so it
   must not steal width from the bar: it takes exactly its content, and the row
   wraps under it only where a screen is too narrow to hold both. */
.rp-scale-chips { flex-wrap: wrap; }
.rp-scale-chips .rp-precip-legend { flex: 0 0 auto; }
/* The radar's band glued over the chart (planche RCOUPE B.2): a hairline
   frame, so a dry route reads as dry and not as missing; the echoes in the
   map's colours, their weight set by the renderer; the map's bolt in it,
   still and paled by its age. */
.route-profile-svg .rp-radar-band-ground { fill: none; stroke: var(--soft-line); stroke-width: 1; }
.route-profile-svg .rp-radar-echo { stroke: none; shape-rendering: crispEdges; }
.route-profile-svg .rp-radar-bolt path {
    fill: var(--text);
    stroke: var(--surface);
    stroke-width: 2.4;
    paint-order: stroke;
}
/* A convective curtain (ICON's RAIN_CON against its RAIN_GSP): outlined where
   a front's steady rain is not, in the ramp colour its own fill wears — the
   per-run rgb rides in on --rp-precip-line, an attribute stroke would lose to
   this rule. Dashed rather than solid so the red of freezing rain, which can
   fall in the same column, stays the only unbroken rim on the layer. */
.route-profile-svg .rp-precip-shower {
    fill: none;
    stroke: var(--rp-precip-line, var(--muted));
    stroke-width: 1.2;
    stroke-dasharray: 5 4;
    stroke-opacity: 0.75;
}
/* Freezing rain: liquid falling through sub-zero surface air. The rim is the
   alert red whatever the fill — this is the ice a rotor collects fastest, and
   it must read over rain ramp and snow ink alike. */
.route-profile-svg .rp-precip-frza {
    fill: none;
    stroke: var(--red);
    stroke-width: 1.6;
}
.route-profile-svg .rp-terrain { fill: var(--chart-relief); }
/* En sombre, le relief reste une masse sombre : son encre, claire la nuit, en
   ferait la seule grande surface claire de l'écran (§ 3.3, § 8.6). */
:root[data-theme="dark"] .route-profile-svg .rp-terrain { fill: color-mix(in srgb, var(--chart-relief) 35%, var(--bg)); }
/* The ground a ceiling is judged against: the highest point of each model cell,
   which is also what the map colours that cell with. It is drawn as the BAND
   between that summit and the ground under the track — the rock that is beside
   the pilot rather than under them — because a ceiling is measured from it and
   a stroke gave that height no thickness.

   The ink the pilot's own marks and the route halo wear, at a wash: this is
   relief and not a warning. The alpha is what lets the relief keep its whole
   shape underneath instead of being buried, which is the objection that
   rules out a dashed line — and the reason the band starts at the terrain
   rather than at the chart floor. The top edge carries the same ink at full
   strength so the summit line itself stays readable where the band is only a
   few pixels tall. Drawn as a PATH: its top is a curve through the cell summits
   rather than a staircase of them. */
.route-profile-svg .rp-judged {
    fill: color-mix(in srgb, var(--chart-relief) 28%, transparent);
    stroke: color-mix(in srgb, var(--chart-relief) 55%, var(--bg));
    stroke-width: 1;
    stroke-linejoin: round;
}
/* The judged ground's own marks — where the route crosses each cell at the
   corridor's high point, which is the ground the ceiling is judged against on
   this view and on the map alike (.map-cross-dot). NOT the cells' whole
   summits beside them: those feed no calculation, and two sets of terrain marks
   around one track invite the reader to judge from the wrong one. */
.route-profile-svg .rp-cross-dot {
    r: 2.5px;
    fill: var(--chart-relief);
    stroke: var(--surface);
    stroke-width: 1;
}
/* THE 0 °C LINE — and it is ONE line, wherever it is drawn.

   Owner's decision: it must look the same on the map as on the cross-section.
   They are two cuts of one surface — the chart cuts it vertically along the
   route, the map cuts it horizontally at the chosen rung — so they are the same
   FACT, and a fact gets one appearance. This class is that appearance, written
   ONCE and unscoped: the chart's polyline wears it, and the map's style probe
   READS IT BACK (map-engine.probeStyle) to paint its GL layer. Neither surface
   holds a copy, so neither can drift. Locked by test.

   In the chart's own COLD ink. NOT red: red on this chart
   means "above ISA", the hot end of the temperature ramp and the tightest
   broken minimum, so a line that says "cold" cannot wear it. --wx-cold is the
   ink the sub-zero air is drawn in, which is exactly what this line is the
   top of.

   SOLID — owner's decision, on both surfaces. It was dashed to be told apart
   from the ceiling at a glance; the ceiling is told apart by its INK instead,
   which changes along the route with the hazard, while this line never leaves
   --wx-cold. What remains dashed on the chart is the second ceiling, the one
   walked on the model's own cover — so a dash there now means one thing only.

   It is drawn TWICE — a --surface casing under it — and heavily, because it is
   ALONE among lines of its kind: no other contour of this fact is drawn beside
   it on either surface, so this one carries the whole weight of the reading.
   The casing is the same --surface halo the labels and the bolt wear, drawn as
   a line because a stroke cannot outline itself (paint-order needs a fill to
   sit over). */
.wx-freezing-line {
    fill: none;
    stroke: var(--wx-cold);
    stroke-width: 2.2;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.wx-freezing-casing { stroke: var(--surface); stroke-width: 4.6; }
/* The ceiling. Told apart from the 0 °C line by its INK, not by its stroke —
   both are solid now. Its colour changes ALONG the route — how low it is, is
   how far it stands above the ground — so the renderer writes the stroke from
   the hazard inks and only the weight lives here, while the freezing line never
   leaves --wx-cold. It is also the thicker of the two, and it is the one a
   verdict hangs off. */
.route-profile-svg .rp-ceiling,
.route-profile-svg .rp-ceiling-casing {
    fill: none;
    stroke-width: 2.6;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.route-profile-svg .rp-ceiling-casing { stroke: var(--surface); stroke-width: 5; }
/* The SECOND ceiling — the one walked on the model's own cover — is dashed and
   uncased, so a glance tells the two apart without reading a legend. No new
   ink and no new token: same ladder, same heights, only the stroke differs. */
/* Drawn against the pilot's own limits and breaking none of them: the line
   stays — the ceiling is still where it is — in the chart's own muted ink, so
   the eye goes straight to the stretches that ARE out of limits. The renderer
   writes no stroke at all for these, which is what leaves the colour here. */
.route-profile-svg .rp-ceiling-clear { stroke: rgba(var(--text-rgb), 0.38); }
/* And its label takes the grid labels' own halo, for the same reason: it is
   written inside the plot, over the weather. */
.route-profile-svg .rp-freezing-label {
    fill: var(--wx-cold);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3;
    stroke-linejoin: round;
}
/* Airspace crossed by the track: an outlined band with its name and limits
   inside the top edge, in the map's own EUROCONTROL recipe (below). Width and
   writing belong to each recipe, so nothing generic is set here — a shared
   dash would quietly draw a Class A boundary as a CTR. The labels wear a
   --surface halo (paint-order) so they stay readable over the cloud curtain. */
.route-profile-svg .rp-airspace-shape {
    /* Its corners turn rather than snap, like every other edge on this chart. */
    stroke-linejoin: round;
}
.route-profile-svg .rp-airspace-limits {
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3;
    stroke-linejoin: round;
}
/* The map's own tag, on the section's bands: the class letter in its inverted
   square — red for A, blue for B–G — or the equipment-zone type on grey, so a
   volume is labelled the same way on either surface. */
.route-profile-svg .rp-airspace-badge { stroke: none; }
.route-profile-svg .rp-badge-a { fill: var(--chart-red); }
.route-profile-svg .rp-badge-bg { fill: var(--chart-blue); }
.route-profile-svg .rp-badge-tmz { fill: var(--chart-grey); }
.route-profile-svg .rp-airspace-badge-text {
    fill: var(--on-color-light);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-black);
    text-anchor: middle;
    stroke: none;
}
/* A volume's drawn sides and bands answer the finger, and so does its tag: a
   tap opens the same card the map's own tap opens. Its empty middle does not —
   that is the plot, where a tap reads the minima. The bands sit under the
   terrain and the weather, which keep their own pointer rules. */
.route-profile-svg .rp-airspace-shape,
.route-profile-svg .rp-airspace-band,
.route-profile-svg .rp-airspace-names { cursor: pointer; }
/* The map's twelve EUROCONTROL recipes, on the section's sides. The STROKE
   carries the identity here — colour and writing — and NOTHING fills a volume
   (planche ESP): the banded styles get the map's band along the inside of each
   side, 7 px, 2 px where a sector of the same class lies across (§2.2.1),
   hatched for a restriction. A side where the route starts or ends inside the
   volume, or a floor on the ground, is not drawn at all (airspaceRings). */
.route-profile-svg .rp-airspace-shape { fill: none; }
.route-profile-svg .rp-airspace-shape.rp-as-classA { stroke: var(--chart-magenta); stroke-width: 1.6; }
.route-profile-svg .rp-airspace-shape.rp-as-classBCD { stroke: var(--chart-blue); stroke-width: 1.6; }
.route-profile-svg .rp-airspace-shape.rp-as-classE { stroke: var(--chart-blue); stroke-width: 1.2; }
.route-profile-svg .rp-airspace-shape.rp-as-classFG { stroke: var(--chart-blue); stroke-width: 0.9; }
.route-profile-svg .rp-airspace-shape.rp-as-other { stroke: var(--chart-blue); stroke-width: 0.9; }
.route-profile-svg .rp-airspace-shape.rp-as-ctrA {
    stroke: var(--chart-magenta);
    stroke-width: 1.4;
    stroke-dasharray: 5 3;
}
.route-profile-svg .rp-airspace-shape.rp-as-ctr {
    stroke: var(--chart-blue);
    stroke-width: 1.4;
    stroke-dasharray: 5 3;
}
.route-profile-svg .rp-airspace-shape.rp-as-atz {
    stroke: var(--chart-blue);
    stroke-width: 1.8;
    stroke-dasharray: 0 4;
    stroke-linecap: round;
}
.route-profile-svg .rp-airspace-shape.rp-as-tmz {
    stroke: var(--chart-grey);
    stroke-width: 1.8;
    stroke-dasharray: 8 3 1.5 3;
}
.route-profile-svg .rp-airspace-shape.rp-as-fis {
    stroke: var(--chart-green);
    stroke-width: 1.2;
    stroke-dasharray: 6 4;
}
.route-profile-svg .rp-airspace-shape.rp-as-nature { stroke: var(--chart-green); stroke-width: 1.2; }
.route-profile-svg .rp-airspace-shape.rp-as-restricted { stroke: var(--chart-red); stroke-width: 1.6; }
/* The band is clipped to its own volume, so the width DECLARED here is twice
   the width painted: 14 → 7 px inside, 4 → 2 px. */
.route-profile-svg .rp-airspace-band {
    fill: none;
    stroke-width: 14;
    stroke-opacity: 0.24;
    stroke-linecap: square;
}
.route-profile-svg .rp-airspace-band.is-thin { stroke-width: 4; }
.route-profile-svg .rp-airspace-band.rp-as-classA-band,
.route-profile-svg .rp-airspace-band.rp-as-ctrA-band { stroke: var(--chart-magenta); }
.route-profile-svg .rp-airspace-band.rp-as-classBCD-band,
.route-profile-svg .rp-airspace-band.rp-as-ctr-band { stroke: var(--chart-blue); }
.route-profile-svg .rp-airspace-band.rp-as-restricted-band {
    stroke: url(#rp-hatch);
    stroke-opacity: 1;
}
.route-profile-svg .rp-hatch-line { stroke: var(--chart-magenta); stroke-width: 1.6; }
/* An area out of activity, or the levels a live one does not allocate, as on
   the map: drawn, faded (docs/vfr-chart-rules.md). */
.route-profile-svg .rp-airspace-shape.is-inactive,
.route-profile-svg .rp-airspace-band.is-inactive { stroke-opacity: var(--spent-dim); }
/* A band's label takes its own boundary's ink, so the writing above a volume
   names the volume under it. Bound to `text`: the label carries the same
   rp-as-<style> class as the band it names, and an unqualified fill here would
   pour that ink into the line-only bands. */
.route-profile-svg text.rp-as-classA,
.route-profile-svg text.rp-as-ctrA { fill: var(--chart-magenta); }
.route-profile-svg text.rp-as-classBCD,
.route-profile-svg text.rp-as-classE,
.route-profile-svg text.rp-as-classFG,
.route-profile-svg text.rp-as-ctr,
.route-profile-svg text.rp-as-atz,
.route-profile-svg text.rp-as-other { fill: var(--chart-blue); }
.route-profile-svg text.rp-as-tmz { fill: var(--chart-grey); }
.route-profile-svg text.rp-as-fis,
.route-profile-svg text.rp-as-nature { fill: var(--chart-green); }
.route-profile-svg text.rp-as-restricted { fill: var(--chart-red); }
.route-profile-svg .rp-airspace-limits { font-size: var(--fs-2xs); font-weight: var(--fw-medium); }
/* The planned route bends at every waypoint: rounded joins so a turn reads as a
   turn rather than as a spike, and rounded ends so the line stops softly. In
   the route's own ink, and wide enough to carry the chevrons that ride inside
   it. THE LINE IS LIGHT AND THE RELIEF DARK, in both themes, so the track
   never melts into the ground it flies over: by day the ink brought back
   towards the page over the relief in the ink itself, at night the reverse
   (see .rp-terrain). */
.route-profile-svg .rp-route-line {
    fill: none;
    stroke: color-mix(in srgb, var(--chart-route) 45%, var(--bg));
    stroke-width: 4.5;
    stroke-linejoin: round;
    stroke-linecap: round;
}
:root[data-theme="dark"] .route-profile-svg .rp-route-line { stroke: var(--chart-route); }
/* The route under the relief (planche RELIEF): the same line in red over it, and
   « under terrain » small along it, in the halo of the altitude labels. */
.route-profile-svg .rp-route-under {
    fill: none;
    stroke: var(--red);
    stroke-width: 4.5;
    stroke-linejoin: round;
    stroke-linecap: round;
    pointer-events: none;
}
.route-profile-svg .rp-under-label {
    fill: var(--red);
    font-size: var(--fs-2xs);
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3;
    stroke-linejoin: round;
    pointer-events: none;
}
/* The direction, drawn IN the line rather than beside it: a --surface chevron
   pointing the way the flight is flown. */
.route-profile-svg .rp-route-arrow {
    fill: none;
    stroke: var(--surface);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* The compass rose (§2.25.9): graduations around a 24 px symbol AND around the
   beacon's own label, at the Detail tier and no wider. Non-interactive and
   unpadded — it is a reference to read off, and a tap on it belongs to the
   beacon it surrounds or to the pan. Fixed size at every scale past its gate:
   a rose is a symbol, not a surface.

   No width here, and that is deliberate: the size is derived from the label the
   circle has to hold (ROSE_SIZE, airport-core.js) and travels on the svg's own
   width/height attributes, so this span shrink-wraps it. It used to be stated
   in both places, which is one place too many for a number that moves. */
.map-navaid-rose {
    position: absolute;
    transform: translate(-50%, -50%);
    color: var(--chart-blue);
    pointer-events: none;
}
/* Block, not the inline default: an inline svg sits on a text baseline and the
   descender space under it would push the rose off its own beacon. */
.map-navaid-rose svg { display: block; overflow: visible; }
/* Two passes like every chart symbol: a white casing under the ink, so the
   graduations read over satellite imagery without a disc behind them. The
   casing rule matches the CHILDREN, never the group — the arrowheads carry
   fill="currentColor" as an attribute, and an attribute beats an inherited
   value while a rule on the element wins. */
.map-navaid-rose .map-rose-casing * {
    fill: none;
    stroke: var(--surface);
    /* Just enough to lift the graduations off the ground: at 3 the halo was
       three times the line and the rose read as white bars with a blue thread
       through them. */
    stroke-width: 2;
    stroke-linecap: round;
}
.map-navaid-rose .map-rose-casing text { stroke-width: 2.5; fill: var(--surface); }
.map-navaid-rose .map-rose-ink {
    fill: none;
    stroke: currentColor;
    stroke-width: 0.9;
    stroke-linecap: round;
}
.map-navaid-rose .map-rose-label {
    font-size: 9px;
    font-weight: var(--fw-medium);
    fill: currentColor;
    stroke: none;
}

/* MapLibre's own control furniture, brought under the rules above.
   Nothing of MapLibre's needs UNDOING here: those rules are scoped by #id and
   already beat its stylesheet, injected after this file though it is. This
   block once opened by resetting the group's border and background "to be
   safe", and being lower in the file it beat OUR OWN glass instead — the rail
   went transparent on the owner's phone. What is left is only what MapLibre
   adds and Leaflet has no equivalent of: the + and - arrive as a background
   image on an inner span, where Leaflet writes them as text, so the span is
   hidden and the two marks are restated. Étape 1b-1. */
/* The attribution's own chrome: the rules above place and size the credit, and
   MapLibre adds a toggle button and a rounded box that Leaflet has no
   equivalent of. */
#briefingMap .maplibregl-ctrl-attrib-button { display: none; }
#briefingMap .maplibregl-ctrl-attrib.maplibregl-compact { min-height: 0; }



/* ===========================================================================
   THE REPORT (owner's call, planche 14 — style E)
   ===========================================================================
   The only colour on the page is a verdict: a reading that breaks a limit is
   written in the ink of the configuration it breaks and the rule on the left
   of its column takes that ink. No pill, no wash. Values are the app's own
   face with tabular figures, labels above them. The METAR is six columns with
   no ruler — an observation has no window; the TAF is a ruler of hours and
   one row per slice, set at its starting hour. Built by render.js off
   report-grid.js and report-rail.js. */

/* A source is a BLOCK: near-white, no contour, a soft shadow, the page's large
   radius. The card around it stays transparent — the blocks are the card. */
/* THE CARD IS A SHEET (owner's call, planche 5): the card's white with a
   hairline contour, its rows running edge to edge and parted by hairlines
   in the ink — a fold in the paper, never a colour of its own. Inside it
   every ground is the card's (the socle's sticky head, the type's grounds
   over the lines). `overflow: clip` rounds the corners without making the
   card a scroll container, which would unstick the socle. */
.rg-blk {
    /* PAS DE CARTE AUTOUR D'UN TERRAIN (§ 5.5, § 8.1) : le bloc METAR/TAF est
       posé sur le sol de la page. Le sol se lit sous le nom de la carte, pour
       que tout ce que la tresse peint « sur la carte » (hachures, pastille des
       chances, barres) prenne ce même sol et n'y découpe pas de blanc. */
    --card: var(--bg);
    --card-rgb: var(--bg-rgb);
    --surface: var(--card);
    --surface-rgb: var(--card-rgb);
    /* LE FILET DE LA CARTE EST LE FILET (§ 2.1, § 3.3) : --line, 1,35 fois le
       sol la nuit au plus — plus un dosage à part de l'encre. */
    --rg-line: var(--line);
    background: var(--card);
    border: 0;
    /* PAS D'ARRONDI (exception au § 5.1) : le bloc n'a ni fond propre ni
       contour, et il rogne son contenu (overflow: clip) — un rayon mordrait
       l'icône d'état posée à son coin. */
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    margin-top: 8px;
    overflow: clip;
    container: rg-blk / inline-size;
}
/* THE SOURCE LINE (owner's call, planche 5): the name as a label, letter-spaced
   and muted; the issue time in the ink, semibold; the age muted, regular, in
   its own case; the raw switch at the right end. The planche's lighter greys
   (42 % of the ink) fall to 2.6:1 on white; the muted token keeps the same
   hierarchy at 4.9:1, the floor every text of the app holds. */
.rg-src {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    /* THE SAME AIR UNDER BOTH SOURCE LINES (owner's call): a title belongs to
       what follows it, so the METAR's line holds its cells exactly as the TAF's
       holds its braid. Measured at the INK, never at the boxes — which is why
       this is one pixel under the TAF's: the row under a METAR line is a label
       whose leading adds a pixel of its own, the row under a TAF line an hour.
       Four pixels more than it was, then six more (owner's call): a name and
       what it names were close enough to read as one block of type. */
    padding: 12px 14px 13px;
    font-size: var(--fs-2xs);
    letter-spacing: 0.02em;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
/* THE NAME READS FIRST (owner's call): the name of the bulletin in the ink, a
   size above the rest of its line, and the issue time after it in the light
   ink — it is the name that says which of the two reports this is, and a card
   of four fields is scanned by name. The other way round, the eye landed on an
   hour and had to hunt for what it was the hour of. */
.rg-src b { color: var(--quiet); font-weight: var(--fw-medium); font-size: var(--fs-xs); letter-spacing: 0.1em; text-transform: uppercase; }
/* L'ÉCLAIR DU SPECI (décision du propriétaire) : le signe que porte déjà
   l'alerte poussée sur le téléphone, dessiné devant le nom du rapport. Il est
   dans l'ENCRE DU TEXTE et non dans une couleur à lui : sur ce rapport une
   couleur veut dire « minimum franchi », et un SPECI parfaitement dans les
   clous n'en franchit aucun. Il fait EXACTEMENT LA HAUTEUR DES CAPITALES du mot
   et s'assoit sur leur ligne de base — mesuré sur la page : le dessin occupe
   20/24 de sa boîte, donc une boîte de 0,9em pour 9 px de capitale à 12 px de
   texte, descendue des 2/24 qui la séparent du haut. En em et non en pixels :
   il monte et descend avec l'échelle du texte. La marge qui le suit est plus
   serrée que le gap de la ligne — il appartient au mot, pas à la liste des
   éléments. */
.rg-speci-bolt { width: 0.9em; height: 0.9em; margin-right: 4px; vertical-align: -0.075em; fill: currentColor; stroke: none; }
.rg-src .rg-time { color: var(--quiet); font-weight: var(--fw-regular); }
/* The age is a sentence, not a label: it keeps its case ("15 h ago"). */
/* THE AGE STANDS BESIDE THE NAME (owner's call): "METAR 47 min ago". */
.rg-src .rg-age { color: var(--quiet); font-weight: var(--fw-regular); letter-spacing: 0; text-transform: none; }
/* Freshness is per SOURCE and nowhere else: one page-wide "updated N min" hides
   the single report that is actually old behind four fresh ones. Past its time
   the age turns red, heavier, without a word (owner's call): the same red the
   PDF uses, the one red on the page that is not a minima verdict. */
.rg-src .rg-stale { color: var(--red); font-weight: var(--fw-semibold); }
/* THE SWITCH THAT UNFOLDS THE CARD'S MESSAGES, at the end of the title's row
   (owner's call, planche A2). A BARE WORD, no hollow and no contour: that row
   is a line of type, and the pin it stands beside is bare by decision — a
   surface there would be the only one on the row. It takes the row's own type
   and the ink every action at rest carries, and turns to the heading ink
   while the messages stand open, which is the whole of its state. */
/* « map » PORTE SON PROPRE NOM, pas celui de « raw » (leçon d'un verrou cassé) :
   je lui avais donné la classe de « raw » pour hériter de son allure, et tout
   sélecteur qui visait « le raw de ce pli » s'est mis à attraper « map ». Deux
   comportements derrière une classe, c'est la façon dont un verrou finit par
   mesurer autre chose que ce qu'il nomme. L'allure se partage par la RÈGLE. */
.rg-raw-btn {
    margin-left: auto;
    font: inherit;
    /* UN SEUL MOT, UNE SEULE ÉCRITURE (décision du propriétaire) : il héritait du
       corps de la rangée qui le porte, donc il se lisait à 14 px sur la ligne du
       terrain et à 11 sur une ligne de source — le même mot, deux tailles. Il
       déclare la sienne : un libellé d'action, 12 px (§ 4.2 — le 11 px est
       réservé aux unités et aux métadonnées). */
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--muted);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
}
@media (hover: hover) {
    .rg-raw-btn:hover { background: var(--hover-bg); }
}
.rg-raw-btn[aria-expanded="true"] { color: var(--heading); }
/* Le mot prend l'espace libre de la rangée ; le pin le suit à l'écart de la
   rangée elle-même, au lieu de partager cet espace en deux (deux marges
   automatiques) ou d'en inventer un troisième (les 10 px d'avant, qui
   s'ajoutaient aux 14 px de boîte vide du pin : 31 px entre le mot et le glyphe). */
.rg-raw-btn + .section-map-btn { margin-left: 0; }


/* ONE COLUMN: its name above its readings and a HAIRLINE ON ITS LEFT (owner's
   call): the column's identity, a line in the ink at 8 % — and no other line:
   no grid, the rows parted by air alone; the grid stands 14px in from the
   card's edges so the first column has its rule like the others. The rule takes a verdict's ink ONLY BESIDE
   THE VALUE THAT BREAKS, thicker there (rg-hit::before): the segment at the
   reading's own height, drawn by the reading in its ink, the rest of the
   rule staying plain. No cell has a ground of its own, and nothing moves a
   value: a marked reading starts on the same pixel as an unmarked one. */
.rg-c {
    position: relative;
    min-width: 0;
    text-align: left;
    color: var(--text);
}
/* The rule spans the cell's text and nothing more (owner's call): no vertical
   padding, and a cell is as tall as its own lines, never stretched to its
   row's. The segment beside a breach is 3px wide, CENTRED on the 1px rule,
   and as tall as the breaching value's own line. */
.rg-mc > .rg-c {
    padding: 0 12px 0 10px;
    border-left: 1px solid var(--rg-line);
}
.rg-mc > .rg-c .rg-i:is(.rg-hit, .rg-hit-ride)::before {
    content: "";
    position: absolute;
    left: -12px;
    top: 2px;
    bottom: 2px;
    width: 3px;
    background: var(--ride-ink, currentColor);
}
/* RANG 5 — LE NOM DE LA LECTURE, écrit au-dessus d'elle (docs/ui/grammaire.md, « La typographie ») : 12 px, 400, --muted, en bas de casse et sans chasse décorative. Il
   nomme, il ne pèse pas : la lecture dessous porte tout le poids de la case. */
.rg-lb {
    display: block;
    font-style: normal;
    font-size: var(--fs-xs);
    letter-spacing: normal;
    text-transform: none;
    font-weight: var(--fw-regular);
    color: var(--quiet);
    line-height: 1.2;
    margin-bottom: 4px;
}
/* THE UNIT READS SMALLER AND LIGHTER THAN ITS VALUE in a cell (owner's call). */
/* In a cell, a trend's row and a slice of the braid alike (owner's call). */
.rg-mc .rg-u, .rg-trend .rg-u, .rg-tr .rg-u, .rg-t .rg-u { font-size: var(--fs-2xs); font-weight: var(--fw-regular); color: var(--muted); margin-left: 1px; }
/* The wind's swing reads quieter than the wind it completes (owner's call). */
.rg-i-soft { font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--muted); }
/* A LAYER A PILOT FLIES UNDER READS LIGHTER (owner's call): where a message
   mixes broken or overcast with few or scattered, the FEW and the SCT take
   --muted — a grey DARKER than the labels' --quiet (owner's call: a layer is
   still data) — at the reading's own size and weight. They are cloud, not a ceiling. Alone they are
   all the cell has to say and they keep the reading's ink. A layer that breaks
   a limit keeps its verdict's ink, which is written on the element itself. */
.rg-i-pale { color: var(--muted); }
/* A cloud's type riding on a light layer is weather in its own right: it keeps
   the reading's ink, or its verdict's. */
.rg-i-pale .rg-ride:not(.rg-hit) { color: var(--text); }
.rg-tr .rg-i-soft { font-size: var(--fs-2xs); }
/* A READING A PILOT READS IN FLIGHT: 16px, semibold, on every screen
   (owner's call, planche 5). */
.rg-c > b {
    display: block;
    font-size: var(--fs-read);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    color: inherit;
    line-height: 1.3;
    overflow-wrap: anywhere;
}
/* ONE ELEMENT OF A CELL. A message states as many layers, sector minima and
   runway ranges as it likes, and none of them may be lost: they are written one
   after another INSIDE their own cell. `inline-block` is what keeps a name of
   several words whole — the box moves to the next line entire when it no
   longer fits, and folds on its own spaces only when wider than the cell. */
.rg-i {
    position: relative;
    display: inline-block;
    max-width: 100%;
    margin-right: 8px;
    /* A reading is one token: "1022hPa" never breaks into "1022hP / a". */
    white-space: nowrap;
}
/* What COMPLETES a reading rather than being it — the gust under a wind, the
   layers above the ceiling — on its own line, IN THE SAME WRITING AND SIZE as
   the reading (owner's call). The ink is the reading's too, except for a cloud
   layer a pilot flies under under a ceiling: see .rg-i-pale. */
.rg-i-more {
    display: block;
    margin-right: 0;
    /* A sentence of several words folds on its spaces, never inside a word. */
    white-space: normal;
}
/* RANG 6a — UNE LECTURE QUI CASSE (docs/ui/grammaire.md, « La typographie ») : l'encre de son
   verdict, et rien d'autre — ni gras (un quatrième codage, qui déplace le texte
   et ne distingue plus rien quand tout casse), ni fond. Son unité reste une
   unité (rang 6b, --muted). Dans une case, le segment de règle à côté. */
.rg-hit .rg-u { color: var(--muted); }
/* A word never breaks inside itself. */
.rg-i { overflow-wrap: normal; }
/* THE TEMPERATURE CELL READS LEFT TO RIGHT (owner's call): T, then the dew
   point on the left edge of the second column, then the humidity, the three
   columns packed from the left and never spread to the cell's edges, each
   name LEFT-ALIGNED ON ITS READING — the cell is one grid of three columns,
   names on the first row, readings on the second, the name and the reading
   of one column starting on the same pixel. The humidity stands where the
   METAR's row is wide enough for it (the container query below), and the
   dew point ALWAYS fits: the cell is
   as wide as its readings, which never fold (min-content, nowrap), and the
   grid's columns give way around it. */
.rg-c-tdp {
    display: grid;
    grid-template-columns: repeat(3, max-content);
    justify-content: start;
    column-gap: 16px;
    row-gap: 4px;
}
.rg-c-tdp > b, .rg-c-tdp > .rg-lb { display: contents; }
.rg-c-tdp .rg-i, .rg-c-tdp .rg-i-more { display: inline-block; margin: 0; white-space: nowrap; }
/* A READING OF UP TO THREE WORDS NEVER FOLDS (a layer, a gust, the wind's
   swing — owner's call for the swing, which has priority over the humidity),
   and a cell of the METAR is never narrower than its readings: min-content,
   so an auto column gives way to a sentence's words and never to a token. */
.rg-mc .rg-i-word { white-space: nowrap; }
.rg-mc > .rg-c { min-width: min-content; }
.rg-c-tdp > .rg-lb { margin: 0; }

/* A PHENOMENON HAS TWO WRITINGS AND THE SCREEN CHOOSES (owner's call): the
   plain-language name wherever there is room for it — a tablet, a computer —
   and the message's own code on a phone, where "Thunderstorm Heavy rain Hail"
   costs three lines of a card that has to be read at a glance. Both writings
   are in the page and only one is shown, so a tap swaps them with nothing to
   fetch and nothing to redraw. */
.rg-ph-c, .rg-ph-w { font-style: normal; }
.rg-ph-c { display: none; }
/* A GUST HAS TWO WRITINGS AND THE SCREEN CHOOSES (owner's call), the same rule
   a phenomenon follows: the plain word wherever there is room for it, and the
   MESSAGE'S OWN LETTER IN FRONT OF THE VALUE on a phone — "G35kt", which is
   what the TAF says and what a pilot reads there. On the braid's readings
   alone: a slice's line carries the wind, its gust and its swing end to end,
   where a METAR's cell folds onto a second line and has the room. */
.rg-g-w, .rg-g-c { font-style: normal; }
.rg-g-c { display: none; }
@media (max-width: 599.98px) {
    .rg-ph-c { display: inline; }
    .rg-ph-w { display: none; }
    .rg-v .rg-g-w { display: none; }
    .rg-v .rg-g-c { display: inline; }
    /* THE CODE READS ALOUD UNDER A FINGER, its words laid over it (main.js
       handlePhenomenaCode, point-menu.js showReading): nothing on the card
       moves. NO RULE UNDER THE CODE (owner's call): a reading of the METAR is
       never underlined. */
    .rg-ph { cursor: pointer; }
}
/* THE WORDS OVER THE CODE (owner's call): the reading's own writing — 16px
   semibold, the group's ink written on the element — on the card's ground, no
   frame, two pixels of air each side. */
.rg-said {
    position: fixed;
    z-index: var(--z-overlay);
    padding: 0 2px;
    background: var(--surface);
    font-size: var(--fs-read);
    font-weight: var(--fw-semibold);
    line-height: 1.3;
    white-space: nowrap;
}
/* THE HUMIDITY GOES FIRST when the METAR's row has no room for six columns
   at their width (owner's call): the row measures itself — the grid, whose
   width is its parent's and not its content's, so the containment costs
   nothing — and under the threshold the cell keeps T and DP at its two ends. */
@container rg-row (max-width: 620px) {
    .rg-c-tdp { grid-template-columns: repeat(2, max-content); }
    .rg-c-tdp .rg-i:nth-child(3), .rg-c-tdp .rg-lb-h { display: none; }
}

/* THE METAR: six columns, always in the same order, EACH AS WIDE AS WHAT IT
   CARRIES (owner's call): auto tracks, which grow to their content before
   the air left over is shared between them — an empty WX column no longer
   costs the dew point its unit. Three and three on a phone (the same 600 px
   as a phenomenon's code), six on a line above it. */
/* THREE COLUMNS, SIX WHERE THE CARD IS 800px WIDE (owner's call,
   planche 5) — the card measures itself (@container rg-blk), never the
   screen: a desktop's briefing column beside its panel is a phone's width.
   Parted by the hairlines the cells draw; auto tracks, so a column is never
   narrower than its readings and the free width is shared equally — a
   `1fr` floor of min-content let a long phenomenon push the last column
   out of the card. */
.rg-mc {
    display: grid;
    grid-template-columns: repeat(3, auto);
    gap: 12px 0;
    padding: 0 14px 8px;
    align-items: start;
    container: rg-row / inline-size;
}
@container rg-blk (min-width: 800px) {
    .rg-mc { grid-template-columns: repeat(6, auto); }
}
/* What belongs to no weather column: the runway state, on its own line. */
.rg-foot {
    margin: 0;
    padding: 8px 14px;
    border-top: 1px solid var(--rg-line);
}
.rg-foot > b { display: block; font-size: var(--fs-md); font-weight: var(--fw-medium); color: var(--text); }
/* THE METAR'S TREND IS A ROW OF THE CARD (owner's call, planche 5), under
   the observation, parted by a hairline: its word and its horizon stacked in
   a fixed block at the left — TEMPO in the ink, BECMG lighter — and its
   values flowing beside, as they are, without names or rules: a forecast of
   the next two hours is never the ceiling that was observed. */
.rg-trend {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0;
    padding: 11px 14px;
    border-top: 1px solid var(--rg-line);
}
.rg-trend .rg-kd { flex: none; display: flex; flex-direction: column; gap: 2px; width: 70px; letter-spacing: 0.14em; }
.rg-trend .rg-kd > b { font-weight: var(--fw-black); color: var(--text); }
.rg-trend .rg-kd-bc > b { color: var(--muted); }
.rg-trend .rg-kd > span { font-weight: var(--fw-medium); letter-spacing: 0.08em; color: var(--muted); }
.rg-trend .rg-cols { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 10px; }
.rg-trend .rg-cols .rg-c { display: contents; }
.rg-trend .rg-cols .rg-lb { display: none; }
.rg-trend .rg-cols .rg-c > b { display: contents; font-size: var(--fs-read); font-weight: var(--fw-semibold); }
.rg-trend .rg-cols .rg-i { display: inline-block; margin: 0; white-space: nowrap; }
/* The word a slice carries: TEMPO, BECMG, PROB40 TEMPO. */
.rg-kd {
    font-style: normal;
    font-size: var(--fs-2xs);
    letter-spacing: 0.1em;
    color: var(--muted);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    white-space: nowrap;
}
/* The columns of a slice or a trend: as wide as what they carry, side by side. */
.rg-cols { display: flex; gap: 10px; min-width: 0; }
.rg-cols .rg-c { flex: 0 0 auto; max-width: 100%; }

/* ===== THE TAF BLOCK ===== */
/* --cap IS A BAR'S CORNER, said once: half the bar's height, so a capsule's
   end is a half-circle whatever the bar. A PROB's bar is taller, so its own
   corner is wider. WHAT A BAR CARRIES IS PLACED ON ITS EDGE (owner's call):
   an hour above it and the values under it start on the bar's own left, not
   one radius in — the rounding is not read as an inset. */
.rg-tf { position: relative; isolation: isolate; padding: 0 14px; --cap: 3.5px; }
/* Sous la tresse, pas de fond de carte : l'air sous un terrain est celui du
   filet entre deux terrains, 18 px (docs/ui/grammaire.md, « Les séparations »). */
/* THE HEAD IS THE SOCLE (owner's call): the card's axis of time under the
   METAR's cells and its trends, ONE ROW deep — a HAIRLINE the window long
   carrying everything that has an hour: the night as a short thicker
   segment, civil dusk to civil dawn; its two hours, each against an end of
   that segment; a tick where the date turns and the date itself just right
   of that tick. Every word on the line stands on the card's own ground,
   which breaks the hairline around it instead of striking the text through.
   No word "now" and no ring here: the present instant is the ring on the
   band of state, and nothing else. No mark and no age for the observation
   (owner's call: its source line says both). It sticks under the page's
   slab, which is sticky itself: two heights, one stack, the slab's measured
   on the page (layoutTafBands sets --page-header-h). */
/* LA NUIT EST UNE BANDE DERRIÈRE LA RÈGLE DES HEURES (décision du
   propriétaire) : 5 px, une par nuit au terrain, centrée sur les CHIFFRES
   qu'elle qualifie — la rangée est à -14 px sur 11 px de haut, son centre est
   donc à -8,5 et la bande s'y pose à -11. Elle qualifie des HEURES : elle ne
   touche ni la barre ni les lectures, et rien ne passe plus sous la barre ni
   derrière les dents d'un BECMG. C'est le défaut mesuré du voile qu'elle
   remplace : son bord haut partait du sommet de la barre, donc il se voyait
   sous une barre passée et grisait le blanc d'un changement.
   ELLE S'ARRÊTE AU CENTRE DE CHAQUE GLYPHE, EN FONDU : une icône n'est jamais
   coupée par un bord. Capsule, et sous les chiffres — une heure se lit à
   pleine encre par-dessus. */
.rg-night {
    position: absolute;
    top: -11px;
    height: 5px;
    z-index: var(--z-page);
    border-radius: 2.5px;
    background: linear-gradient(90deg, transparent 0, var(--bar-past) 10px,
                var(--bar-past) calc(100% - 10px), transparent 100%);
}
/* LA BANDE ET SES DEUX GLYPHES SONT UNE SEULE MATIÈRE (décision du
   propriétaire) : l'encre exacte de la bande, à la taille qu'ils avaient sur la
   ligne de source. CENTRÉS SUR LE CENTRE DE LA BANDE, et le centrage porte sur
   le DESSIN et non sur sa boîte : mesuré au navigateur, une icône en ligne
   s'assoit sur sa ligne de base et tombe 4 px dessous. */
.rg-night > .rg-nb-i {
    position: absolute;
    top: 50%;
    width: 11px;
    height: 11px;
    margin-top: -5.5px;
    color: var(--bar-past);
}
.rg-night > .rg-nb-i[hidden] { display: none; }
.rg-night > .rg-nb-dusk { left: 0; margin-left: -5.5px; }
.rg-night > .rg-nb-dawn { right: 0; margin-right: -5.5px; }
/* LES DEUX HEURES DE LA NUIT, AU BOUT DE LA LIGNE DE SOURCE (décision du
   propriétaire) : elles disent un FAIT DU TERRAIN et non une place sur l'axe —
   la bande dit déjà où la nuit tombe — donc elles se lisent une fois par carte,
   dans l'encre et la graisse de cette ligne. CHAQUE ICÔNE DEVANT SON HEURE, le
   même ordre deux fois : l'œil tombe sur le glyphe et lit l'heure après, au
   lieu d'apprendre de quel côté se tient l'icône. */
.rg-tw-set { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
.rg-tw { display: inline-flex; align-items: center; gap: 3px; white-space: nowrap; }
/* Une heure n'est pas le nom du bulletin : elle rend tout ce que « .rg-src b »
   donne à ce nom — la taille au-dessus de la ligne, l'interlettrage, les
   capitales — et ne garde que la typographie de la ligne, pour que son icône
   à 1em la vaille exactement. */
.rg-tw b { font: inherit; letter-spacing: inherit; text-transform: none; color: var(--quiet); }
/* Le dessin remplit sa boîte : en ligne, il s'assoirait sur la ligne de base
   et tomberait sous le centre de la bande (mesuré : 4 px). */
.rg-nb-i { display: block; width: 1em; height: 1em; color: var(--icon); }
.rg-nb-i svg { display: block; width: 100%; height: 100%; fill: currentColor; }
/* THE TAF'S LINE ON ITS OWN ROW (owner's call, planche 5): under the socle,
   over the extremes and the hours of the state — nothing shares its row and
   no line crosses it, the lines of the body start under it.
   ITS AIR IS ASYMMETRIC (owner's call): four times more above than below — 24
   px from the socle it leaves, 6 px to the braid it names. A title belongs to
   what comes after it, and the gap above is what separates two reports. */
/* ENTRE LE METAR ET LE TAF, DE L'AIR ET AUCUN FILET (décision du propriétaire) :
   la tête « TAF » dit déjà qu'une autre lecture commence. */
.rg-tf > .rg-src { padding: 20px 0 14px; }
/* THE MESSAGE STARTS WHERE EVERY OTHER READING DOES: the braid already holds
   the card's inset, so its raw message takes none of its own — otherwise the
   TAF's text stands fourteen pixels right of the METAR's, and two messages of
   one card read as two columns. */
.rg-tf > .rg-raw { padding-left: 0; padding-right: 0; --rg-raw-pad: 0px; }
/* PLUS AUCUNE VERTICALE NE DESCEND DANS LA TRESSE (décision du propriétaire) :
   ni celle de « now », ni celle du changement de jour. Le jour reste dit par son
   00 plus lourd sur la règle, et l'instant par la robe de la barre — c'est là
   qu'elle change, et un trait le disait une seconde fois. */
.rg-body { position: relative; }
/* LE CALQUE DU HORS-GARDE, DANS LE MESSAGE BRUT : une opacité et rien d'autre.
   Aucun fond ici — un fond sur le calque emporterait le voile avec le texte,
   puisqu'une opacité s'applique à l'élément entier. La tresse, elle, a ses deux
   doses (.rg-fade-past / .rg-fade-off) : elle distingue ce qui est derrière nous
   de ce qui est hors garde, quand le message brut ne dit que la garde. */
.rg-out { opacity: var(--guard-dim); }
/* LA GARDE EST DEUX MONTANTS FINS À SES DEUX INSTANTS (décision du
   propriétaire) : plus de voile sur le sol et plus de triangle — la garde se dit
   par la ROBE de ce qu'on lit, et ces deux marques disent seulement où elle
   ouvre et où elle ferme. Dans l'encre MÊME de la barre gardée, pour que la
   marque et ce qu'elle borne se répondent. Elles tiennent dans l'air que la
   règle laisse déjà : trois pixels de part et d'autre d'une barre de sept,
   donc zéro pixel de carte. */
.rg-gd {
    position: absolute;
    top: -3px;
    width: 1.5px;
    height: 13px;
    margin-left: -0.75px;
    z-index: calc(var(--z-page) + 3);
    background: var(--text);
}
/* THE EXTREMES, JUST OVER THE HOURS (owner's call, planche 27): a triangle up
   for the maximum, down for the minimum, the value beside it on the same
   line, IN THE VALUES' OWN FACE — the face of every reading on the rail — and
   the triangle centred on its hour (the pass sets it on the label that writes
   the instant). No colour on the icon — the direction is what tells them
   apart, and an ink that is not a verdict says nothing here. */
/* The extremes' row: the mark stands at its foot, close over the hours (owner's call). */
.rg-tp { position: relative; height: 18px; }
/* NO ICON ON AN EXTREME (owner's call): 30 is larger than 18, and a triangle
   said nothing the value did not. The value alone stands at its instant. */
.rg-t {
    position: absolute;
    bottom: 0;
    display: inline-block;
    /* THE NUMBER IS CENTRED ON ITS HOUR, THE UNIT DOES NOT COUNT (owner's
       call): the unit is given a box of zero width that its ink overflows, so
       it stays in the line — same baseline, same air against the digits — and
       weighs nothing in the centring. A °C counted in would push every extreme
       off its own instant by half its width. */
    transform: translateX(-50%);
    white-space: nowrap;
    font-family: var(--app-font);
    /* DANS LA FACE DES VALEURS DE LA TRESSE (décision du propriétaire), donc au
       rang 6c : 13 px en 500, comme elles. */
    font-size: var(--fs-sm);
    line-height: 16px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.rg-t .rg-u { display: inline-block; width: 0; overflow: visible; }
.rg-t b { font-weight: var(--fw-medium); }
/* At either end the value is pulled back onto the card. There the unit is worth
   its width again: what must not leave the card is the whole reading. */
.rg-t-first { transform: none; }
.rg-t-last { transform: translateX(-100%); }
.rg-t-last .rg-u { width: auto; }
.rg-src .rg-age { white-space: nowrap; }
/* LA TRESSE (owner's call). The state is ONE BAND of boxes, a TEMPO or a PROB a
   chip in a lane under it; a slice is as wide as its text, its bar on top as
   long as its duration, its hours above the bar's two ends and its word just
   under it. Widths, lanes and heights are the pass's answer (report-rail.js
   layoutTafBands); the band leaves the hours their row above it. */
/* AIR BETWEEN THE ROWS AND THE LANES (owner's call): 18px rows, 16px over
   the band, 26px over each lane of chips (owner's call: more air over what is
   not the established state) — THE SAME OVER EVERY LANE, whichever
   variable the lane starts on: a band has only the rows its slices write
   (--row-*, report-rail.js bandRows). */
.rg-bn { position: relative; z-index: calc(var(--z-page) + 1); margin-top: 16px; min-height: 20px; }
.rg-bn-cd { margin-top: 26px; }
/* NO SLICE HAS A GROUND OF ITS OWN (owner's call): a state box, a chip, all
   are type on the block's paper; the bar alone says the box. */
.rg-tr {
    position: absolute;
    top: 0;
    padding: 12px 4px 0 1px;
    white-space: nowrap;
}
/* Pulled back inside the card, a chip ends with its bar and reads from the
   right — its hours, its odds AND its readings. A row of readings is a flex
   box, which `text-align` does not reach: the row itself has to pack to its
   end, or the values stay left while everything above them ends at the edge. */
.rg-tr-pull { padding: 12px 1px 0 4px; text-align: right; }
.rg-tr-pull .rg-v { justify-content: flex-end; }
/* THE BAR CARRIES NO COLOUR (owner's call): it says when, the values say what
   and whether it breaks. EVERY BAR IS 7 px TALL in the full matter's grey
   (owner's call), a capsule at a bar's two ends only; a TEMPO's, and nothing
   else's, is HATCHED — the full matter struck on the soft one, so the hatch
   says "temporary" while staying a bar and never carries the darkest ink on
   the card. NOTHING ABOUT A BAR OR ITS VALUES CHANGES FOR A PROB (owner's
   call): the bar of a 30 % and the bar of a 40 % are the bar every slice
   draws, and their readings are read like every other reading. The odds are
   written once, on the pill — and a reading a pilot has to weigh is never
   dimmed for being less certain. */
/* HORS GARDE ET DEVANT NOUS, LA BARRE PREND LA MATIÈRE DE LA CARTE (décision
   du propriétaire) : --bar était trop proche de la robe du passé, les deux gris
   se touchaient. La dose bouge, scopée à la tresse ; le jeton, lui, ne bouge
   pas — le rail des NOTAM garde le sien. */
.rg-bn-st, .rg-bn-cd { --bar: var(--icon); }
.rg-bar { position: absolute; top: 0; height: 7px; background: var(--bar); }
/* UNE ROBE EST LA BARRE PEINTE UNE SECONDE FOIS, JAMAIS UN APLAT (décision du
   propriétaire) : la copie porte les MÊMES classes, la MÊME boîte et les MÊMES
   enfants que la barre qu'elle double, et ne change qu'une chose — la dose de
   --bar. Toute trame se reproduit donc à l'identique et EN PHASE : la hachure
   d'un TEMPO, le peigne d'un BECMG. Un aplat posé dessus les mange tous les
   deux, et il le fait sans bruit. La MÊME BOÎTE est ce qui garde la phase : une
   trame repeinte dans une boîte plus étroite casserait au raccord, un dégradé
   répété se posant depuis sa propre boîte et non depuis la carte.
   Chaque robe est coupée À SES DEUX BORNES, dans sa propre boîte (--c0, --c1,
   des parts de CETTE barre que report-rail.js calcule) : la fenêtre d'un BECMG
   et la barre qu'il tient sont deux boîtes. Aucune ne se peint de fond : un
   fond ici EST l'aplat que cette règle existe pour interdire.
   DANS LA GARDE ET DEVANT NOUS, LA BARRE EST NOIRE — l'encre des lectures.
   DERRIÈRE NOUS ELLE EST PLUS CLAIRE, et le passé l'emporte : il est peint en
   dernier, donc une heure passée dans la garde est claire et non noire. */
.rg-bar-on { --bar: var(--text); clip-path: inset(0 calc(100% - var(--c1, 0%)) 0 var(--c0, 0%)); }
.rg-bar-past { --bar: color-mix(in srgb, var(--icon) 18%, var(--card)); clip-path: inset(0 calc(100% - var(--c1, 0%)) 0 var(--c0, 0%)); }
.rg-tr-st > .rg-bar { left: 0; right: 0; }
/* A chip's bar sits just over the first variable it writes (--drop, the
   painter), its hours with it: no empty row between the bar and the values. */
.rg-tr-cd > .rg-bar { left: var(--g0, 0px); width: var(--g1, 100%); border-radius: var(--cap); top: var(--drop, 0px); }
.rg-tr-cd > .rg-hb { top: calc(var(--drop, 0px) - 14px); }
/* The band of state is ONE bar: rounded at its two ends only — the first
   bar's left, the last bar's right — its boxes meeting square in between,
   and never at a BECMG's junction, where the window meets the held bar
   (owner's call). */
.rg-tr-first > .rg-bar:not(.rg-bar-held) { border-top-left-radius: var(--cap); border-bottom-left-radius: var(--cap); }
.rg-tr-last > .rg-bar:not(.rg-bar-step) { border-top-right-radius: var(--cap); border-bottom-right-radius: var(--cap); }
/* A TEMPO IS THE MATTER CUT BY WHITE (owner's call): the same bar as a state,
   with thin cuts of the card's own ground through it — so what tells a
   forecast from a state is the TEXTURE, never the force. The cut is the narrow
   one: 1.5px of white every 5px, so the bar keeps its weight and only loses
   its continuity. */
.rg-bar-tempo { background: repeating-linear-gradient(115deg, var(--bar) 0 3.5px, var(--card) 3.5px 5px); }
/* THE ODDS OF A PROB RIDE ITS BAR (owner's call): a pill on the CARD'S OWN
   GROUND, centred on the bar, which breaks the bar around it instead of
   covering it — the socle's idiom, where a word stands on the ground and the
   hairline stops at it. Its bar is an ordinary 7px bar like every other: it
   was 13px only to swallow a pill, and a swallowed bar no longer says how long
   the PROB lasts. Where the bar is too short to be read around the pill, the
   pass sets it BEFORE the bar — "PROB40 TEMPO", the raw message's own order —
   or after it when there is no room ahead (report-rail.js placeOdds). */
.rg-pc {
    position: absolute;
    top: calc(var(--drop, 0px) - 2px);
    left: calc(var(--g0, 0px) + var(--g1, 100%) / 2);
    transform: translateX(-50%);
    z-index: calc(var(--z-page) + 2);
    font-style: normal;
    font-size: var(--fs-xs);
    line-height: 12px;
    /* ONE PIXEL OF GROUND EACH SIDE, NO MORE (owner's call): the pill's job is
       to break the bar around the number, not to hold a cushion — and on a
       short bar every pixel it takes is a pixel of duration it hides. */
    padding: 0 1px;
    border-radius: 3px;
    font-weight: var(--fw-black);
    color: var(--text);
    background: var(--surface);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* Off the bar there is no bar around it to make it a pill: it gives up a
   weight, so it reads as a qualifier and never as one more reading. */
.rg-pc-before, .rg-pc-after { font-weight: var(--fw-semibold); }
.rg-pc-before { left: var(--g0, 0px); transform: translateX(calc(-100% - 3px)); }
.rg-pc-after { left: calc(var(--g0, 0px) + var(--g1, 100%)); transform: translateX(3px); }
/* A BECMG IS A COMB THAT CLOSES (owner's call): over its own window (--own)
   the bar is the card's own matter cut by the card's ground, and it is the
   PITCH that says the change — 2 px of matter, an opening that starts at 3 px
   and shuts to nothing where the state is reached, so the window ends full and
   its junction with the held bar cannot be seen. What tells a forecast from a
   state stays the TEXTURE and never the force: a TEMPO's cuts lean and keep
   their pitch, a BECMG's stand upright and close. This is what retired the
   fade from the card's ground: over the first half of a 25px window it was
   white on white, so the bar had a hole and the window's own start hour had
   nothing under it. The values are still written where the window ends
   (--hold), where the value is reached; a box with no duration of its own (the
   initial slice a BECMG opens with) is type on the card. The pass measures the
   window and places the teeth (report-rail.js combStep); their thickness is
   written here, once. */
.rg-bar-step { left: 0; width: var(--own); border-radius: 0; background: none; overflow: hidden; }
.rg-bar-step > i { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--bar); }
/* Written at the band's own specificity: ".rg-tr-st > .rg-bar" sets every
   bar's left to 0, and under it the held bar would cover the step from its
   first pixel. */
.rg-tr-st > .rg-bar-step { right: auto; }
.rg-tr-st > .rg-bar-held { left: var(--own); right: 0; border-radius: 0; }
.rg-tr-st > .rg-cols { margin-left: var(--hold, 0px); }
.rg-tr-open { z-index: calc(var(--z-page) + 1); }
/* EVERY BAR WRITES ITS HOURS above its two ends: the start at the left, the
   end at the right; an extreme's hour is centred on its instant. EVERY HOUR OF THE BLOCK
   WEARS THE INK AND WEIGHT OF THE SOURCE LINE'S AGE (owner's call): muted,
   regular, all alike, a BECMG's own end no lighter than the others. The pass moves one
   that would touch its neighbour to the right of it (report-rail.js
   spaceHours). */
.rg-hb {
    position: absolute;
    top: -14px;
    left: var(--g0, 0px);
    z-index: calc(var(--z-page) + 2);
    font-family: var(--app-font);
    font-weight: var(--fw-regular);
    font-size: var(--fs-2xs);
    line-height: 11px;
    letter-spacing: 0.02em;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* CHAQUE HEURE PORTE LA ROBE DU SEGMENT QU'ELLE NOMME (décision du
   propriétaire) : la règle est le miroir de la barre, DÉCALÉE D'UN CRAN VERS
   L'ENCRE — une heure est une lecture et non une matière. Mesuré sur la carte
   blanche, la robe de la barre appliquée telle quelle tombe à 1,10:1 ; sur
   cette échelle-ci, le hors-garde tient 5,05:1 et la garde 16,1:1. */
.rg-hb-past { color: var(--icon); }
.rg-hb-off { color: var(--muted); }
.rg-hb-on { color: var(--text); }
/* CE QUI EST FINI OU HORS GARDE S'ÉCLAIRCIT, jamais la barre ni la règle : ce
   sont les LECTURES qui pâlissent, et par une opacité — un gris remplacerait la
   teinte, et une valeur qui dépasse cesserait de le dire. */
.rg-fade-past { opacity: var(--spent-dim); }
.rg-fade-off { opacity: var(--guard-dim); }
.rg-hb-last { left: calc(var(--g0, 0px) + var(--g1, 100%)); transform: translateX(-100%); }
.rg-hb-own { left: var(--own); }
.rg-hb-temp { transform: translateX(-50%); }
/* WHERE THE DAY TURNS, THE BAND WRITES ITS 00 (owner's call): always, even
   where no slice begins there, and heavier than the hours around it — it is
   the one hour a pilot counts from. Heavier, never darker: the emphasis stays
   inside the hours' own ink, which is what keeps a ruler a ruler. The socle
   writes no date and no tick any more; this is where the day is said. */
/* LE 00 DU JOUR EST PLUS LOURD QUE LES HEURES AUTOUR (décision du
   propriétaire) : plus lourd et jamais plus sombre, l'accent restant dans
   l'encre des heures — c'est ce qui garde une règle une règle. */
.rg-hb-day { font-weight: var(--fw-black); }
/* The 00 the band adds where no slice writes that instant: centred on it, the
   way an extreme's hour is. A slice's own hour keeps its end's alignment. */
.rg-hb-mid { transform: translateX(-50%); }
/* EVERY VARIABLE KEEPS ITS ROW (owner's call): the wind, then the visibility,
   the ceiling, the phenomena — rows of 18px (13px type, a reading a pilot
   reads in flight — owner's call), a variable that does not change
   leaving its row empty, so one variable reads along its own line from one
   end of the TAF to the other. The rows are born of what is written (auto
   rows, each variable naming its own): a slice ends on the last row it
   writes and carries no blank under its last word — a chip of wind alone is
   one row tall, its lane with it (owner's call). What completes a reading
   (the gust) follows on its row; A LAYER MORE GOES UNDER THE ONE BEFORE IT
   (owner's call, planche FRISE TAF): side by side, two layers made a box
   wider than a phone could give it, and it wrote over the next box. Only the
   ceiling row grows for it. report-rail.js SLICE_ROW mirrors the row for a
   chip's drop onto its first variable. */
.rg-tr .rg-cols { display: grid; grid-auto-rows: minmax(18px, auto); gap: 0; }
.rg-tr .rg-v-ceiling { flex-direction: column; gap: 0; }
.rg-tr .rg-v { grid-column: 1; }
.rg-tr .rg-v-wind { grid-row: var(--row-wind, 1); }
.rg-tr .rg-v-vis { grid-row: var(--row-vis, 2); }
.rg-tr .rg-v-ceiling { grid-row: var(--row-ceiling, 3); }
.rg-tr .rg-v-wx { grid-row: var(--row-wx, 4); }
.rg-tr { z-index: calc(var(--z-page) + 1); }
.rg-tr .rg-v {
    display: flex;
    gap: 6px;
    font-family: var(--app-font);
    /* RANG 6c — UNE LECTURE PRÉVUE (décision du propriétaire, planche ÉPURE T1,
       docs/ui/grammaire.md, « La typographie ») : 13 px en 500, sous les 16 px du METAR —
       la prévision se lit sous l'observation, et une valeur qui casse garde
       l'encre de son verdict. */
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 18px;
    letter-spacing: 0.01em;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
/* In a slice every element is one line, never folded: a box the axis could
   not stretch enough overflows rather than breaking "SCT 4500ft" in two. */
.rg-tr .rg-i { display: inline; margin: 0; white-space: nowrap; }
/* A TAF with no window to draw on (rg-tf-flat), and a trend: on a phone a
   slice's readings are ONE LINE OF TEXT rather than columns — the values
   alone, in their order, separated by a dot, wrapping if they must. */
@media (max-width: 599.98px) {
    .rg-tf-flat .rg-cols { display: block; min-width: 0; }
    .rg-tf-flat .rg-cols .rg-c { display: inline; border: 0; padding: 0; }
    .rg-tf-flat .rg-cols .rg-lb { display: none; }
    .rg-tf-flat .rg-cols .rg-c > b { display: inline; font-size: var(--fs-md); }
    .rg-tf-flat .rg-cols .rg-c + .rg-c::before { content: ""; margin: 0 3.5px; }
    .rg-tf-flat .rg-cols .rg-i + .rg-i { margin-left: 5px; }
    .rg-tf-flat .rg-cols .rg-i-more { display: inline; }
}

.rg-more {
    font-size: var(--fs-xs);
    color: var(--muted);
    padding: 5px 0 0;
    letter-spacing: 0.04em;
}

/* The raw message: one change group per line (report-rail writes the
   line breaks), the words that break a limit in their verdict's ink. */
/* THE MESSAGE IS A READING, IN THE INK OF EVERY OTHER (owner's call): a raw
   METAR or TAF is not an aside about a reading, it IS the reading — in Raw it
   is the only thing on the card — so it wears --text like the decoded values
   and not the light ink that names them. Its lines all start at the same left:
   the message carries its own left edge, and the block's inset is the only
   margin it gets (colorizeRawReport, render.js). */
.rg-raw {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-family: var(--app-font);
    font-size: var(--fs-md);
    line-height: 1.6;
    color: var(--text);
    padding: 4px 14px 10px;
    /* Le débord du voile : exactement le retrait horizontal du message, pour
       que la zone hors garde coure d'un bord de la carte à l'autre. Dans la
       tresse ce retrait est nul (le corps le porte déjà), donc le débord aussi. */
    --rg-raw-pad: 14px;
}
.rg-raw .rg-w { font-weight: var(--fw-semibold); }
/* LE MÊME VOILE ET LE MÊME CALQUE SUR LE MESSAGE : un raw dit le même fait que
   la tresse, donc il prend les mêmes deux valeurs et n'en invente aucune.
   Le voile est ici un FRÈRE posé SOUS le segment, jamais son propre fond : une
   opacité s'applique à l'élément entier, fond compris, donc un fond porté par
   le segment pâlirait avec son texte et le voile disparaîtrait. Il déborde du
   retrait de la carte pour que la zone coure d'un bord à l'autre. */
.rg-raw-seg { position: relative; display: block; }
.rg-raw-veil {
    position: absolute;
    left: calc(-1 * var(--rg-raw-pad, 14px));
    right: calc(-1 * var(--rg-raw-pad, 14px));
    top: 0;
    bottom: 0;
    background: var(--guard-veil);
    z-index: var(--z-page);
}
.rg-raw-seg > span { position: relative; z-index: calc(var(--z-page) + 1); }

/* ===== A NOTAM'S RAIL: its validity on the next 24 h ===== */
/* A NOTAM'S VALIDITY IS A FILET (owner's call): one line as long as the
   validity is inside the briefing's window — no track behind it, nothing else
   on the row — with its two hours BESIDE its ends, ON ITS OWN LINE: the start's
   to the left of the left end, the end's to the right of the right end. Above
   the filet they made the rail two storeys tall for two numbers; beside it,
   18 ──── 05 reads as one thing, and the row is as tall as one line of text.
   The rail has no ruler: the filet's two ends say when, once. */
/* Its height and its place in the row belong to the row (.notam-entry), which
   is the only thing that carries a rail: two heights for one rail meant one
   of them was a value nobody read. */
.rg-rail { position: relative; --rail-hour: 32px; --rail-end: 35px; --rail-word: var(--row-word, 44px); --rail-stub: 14px;
           --bar-h: 5px; --cap: calc(var(--bar-h) / 2); --crenel: 17.5px; --crenel-step: 3.5px;
           --bar: color-mix(in srgb, var(--icon) var(--bar-dose, 42%), var(--card)); }
/* LA BARRE D'UN NOTAM EST PLUS CLAIRE QUE CELLE DE LA TRESSE (décision du
   propriétaire), et c'est la DOSE qui bouge, pas le jeton : --bar reste déclaré
   une seule fois dans la palette, et le rail le remélange pour lui seul. Un
   quatrième gris nommé serait une couleur de plus à documenter et à accorder sur
   les deux thèmes ; une dose, c'est la même matière ramenée plus loin parce que
   cette barre-là se tient SEULE sur une ligne nue, sous un titre en gras, où le
   même gris pèse plus qu'il ne dit.

   LA DOSE N'EST PAS PARTAGEABLE, LE PLANCHER L'EST : 25 niveaux de gris sous la
   carte, en dessous desquels une matière cesse d'être lue sans qu'on la cherche.
   L'écart de la matière à la carte vaut 60 niveaux au jour, 45 la nuit —
   une même proportion passerait donc SOUS le plancher sur les thèmes sombres,
   ce qui est exactement ce qui est arrivé : 19 niveaux, mesurés après coup ; à
   57 %, 25,6 sur le sol neutre du sombre unique (§ 3.1).
   C'est le seul endroit de la carte où un ton se règle sur un absolu et non sur
   une proportion : le plancher de l'œil ne se contracte pas avec l'amplitude du
   thème. Les trois doses sont mesurées peintes, pas calculées, et un verrou de la
   recette navigateur les remesure dans les deux thèmes. */
:root[data-theme="dark"] .rg-rail { --bar-dose: 57%; }
/* THE WINDOW IS INSET BY ONE HOUR'S WIDTH AT EACH END: an hour is written
   OUTSIDE the filet, so a validity starting at the very edge of the window
   would have nowhere to write it. The inset is the same on every row, so the
   scale stays common — which is the whole point of drawing them on one window.
   THE WINDOW IS THE TITLE'S OWN COLUMN, both ends (owner's call): it begins
   under the first letter of the title and ends where the title's line ends, so
   a filet that runs the whole window runs the whole row. On the LEFT that costs
   nothing — the inset is the pin's column, free on the filet's line — and the
   start's hour is written in it. On the RIGHT there is nothing to borrow, so
   the end's hour is held inside by the rule below instead of the window being
   pulled in: 32px kept empty on every row for the rare filet that reaches the
   edge is 32px lost on all the others.
   --rail-hour is that label's width, MEASURED in the page: "23:20" at --fs-2xs
   plus the 4px that keeps it off the filet; an hour on the hour is written "23"
   and takes half of it. A measured value, so a lock measures it too: the browser
   recipe puts the widest label at the closest point to each edge it can reach
   and checks nothing hangs out of the rail, which is the whole row. Change the
   type scale, or narrow the pin column below the label, and it reddens. */
/* LA FENÊTRE DU RAIL RENTRE D'UNE LARGEUR D'HEURE, À GAUCHE SEULEMENT, et c'est
   SON besoin à elle : l'heure de début se pose à GAUCHE du filet et pendrait hors
   du rail sans cette réserve (18 px mesurés). À droite il n'en faut aucune —
   l'heure de fin est retenue par son propre `min` (.rg-h-end), et une réserve y
   garderait 32 px vides sur toutes les rangées pour le seul filet qui va
   jusqu'au bord.
   Ce retrait s'écrivait avec la largeur de la gouttière du pin — la place était
   prise sur une colonne vide sur cette ligne-là. La punaise est partie (planche
   NOTAM vue 5 § 1), donc la réserve se dit maintenant par ce qu'elle sert :
   --rail-hour. Conséquence à connaître : le filet ne commence plus sous la
   première lettre du titre mais une largeur d'heure plus loin, exactement comme
   avant le lot 4 — c'est le titre qui s'est déplacé, pas le filet. */
.rg-rail-win { position: absolute; inset: 0 0 0 var(--rail-hour, 0px); }
/* LA VALIDITÉ EST UNE BARRE, CELLE DE LA TRESSE (décision du propriétaire) :
   7 px, le ton des barres, capsule à ses deux bouts. La décision d'avant tenait
   un FILET d'un pixel, au motif qu'à deux il se lirait comme une BARRE, donc
   comme une QUANTITÉ, quand une validité est une ÉTENDUE. Ce que la tresse a
   prouvé depuis : ses propres barres sont des étendues elles aussi — un état
   qui tient de telle heure à telle heure — donc l'objection tombe et l'idiome
   se transporte tel quel : une barre prend --bar et non la matière des glyphes,
   parce qu'elle traverse la carte là où une icône fait quelques pixels.

   PLUS CLAIRE QUE CELLE DE LA TRESSE (décision du propriétaire) : --bar est
   redéfini sur .rg-rail, qui n'habille QUE la rangée NOTAM. Le ton de la tresse
   est celui d'une grille serrée où chaque barre est bornée par ses voisines ;
   ici une seule barre se tient sur une ligne nue, sous un titre en gras, et le
   même gris y pèse plus lourd qu'il ne dit. Le fait ne change pas de nom pour
   autant : c'est --bar des deux côtés, mélangé d'une main plus légère là où le
   fond est vide. */
.rg-slice {
    position: absolute;
    top: 50%;
    height: var(--bar-h);
    margin-top: calc(var(--bar-h) / -2);
    min-width: var(--bar-h);
    /* La capsule est la MOITIÉ de la hauteur : les bouts sont des demi-disques,
       pas des coins adoucis. Une seule valeur, --bar-h, change les deux. */
    border-radius: var(--cap);
    /* Et le créneau la suit : sans ça, son dégradé peint des coins carrés
       PAR-DESSUS l'arrondi, qui ne clippe pas ses enfants tout seul. */
    overflow: hidden;
    background: var(--bar);
}
/* UN BOUT COUPÉ SE VOIT (décision du propriétaire) : une barre qui s'arrête sur
   le bord de la fenêtre ne disait rien, si bien qu'un NOTAM courant depuis
   avant-hier ressemblait à un NOTAM qui commence pile maintenant. Le bout coupé
   porte un CRÉNEAU — des carrés pleine hauteur, un sur deux, du fond de la carte
   à travers la matière. C'est l'idiome du TEMPO, dont c'est le pas de matière :
   3,5 px. Le créneau ne suit PAS la hauteur de la barre — il a sa propre valeur,
   --crenel-step : ce qu'il faut pour se voir sur un téléphone ne rétrécit pas
   parce que la barre a maigri. Jamais plus de la moitié de la pièce, sinon une
   pièce courte au bord de la fenêtre serait tout entière un créneau.

   ET LE BOUT COUPÉ GARDE SA CAPSULE (décision du propriétaire, qui renverse
   « un bout coupé la perd, sinon il se lit comme un vrai bout ») : le créneau
   dit déjà que c'est coupé, donc le coin carré le redisait — et il le redisait
   mal, en donnant deux fins différentes à la même barre selon qu'elle tenait
   dans la fenêtre ou non. Tous les bouts sont ronds ; ce qui distingue un bout
   coupé, ce sont ses carrés.

   LE CRÉNEAU COMMENCE PAR DE LA MATIÈRE, pas par un vide : mesuré, un vide au
   ras du bout effaçait les 3,5 px où vit justement l'arrondi, et le premier
   trait visible était un CARRÉ — la capsule ne se voyait donc jamais sur une
   barre coupée, qui est le cas courant. La barre finit par sa capsule, puis les
   carrés s'égrènent vers le bord : c'est aussi le sens de lecture d'un « ça
   continue ». */
.rg-cut-l::before, .rg-cut-r::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: var(--crenel);
    max-width: 50%;
}
.rg-cut-l::before {
    left: 0;
    background: repeating-linear-gradient(90deg, var(--bar) 0 var(--crenel-step),
                                      var(--card) var(--crenel-step) calc(var(--crenel-step) * 2));
}
.rg-cut-r::after {
    right: 0;
    background: repeating-linear-gradient(270deg, var(--bar) 0 var(--crenel-step),
                                      var(--card) var(--crenel-step) calc(var(--crenel-step) * 2));
}
.rg-h {
    position: absolute;
    top: 50%;
    left: var(--at);
    /* The start's hour ends where the filet begins; the end's begins where the
       filet ends. Both sit on the line, not above it.
       ET ELLE NE SORT PAS DU RAIL PAR LA GAUCHE. Elle se posait dans la gouttière
       de la punaise, qui n'existe plus (planche NOTAM vue 5 § 1) : un filet qui
       commence au bord de sa rangée laissait l'heure déborder de 18 px mesurés.
       la fenêtre du rail lui réserve sa largeur de chaque côté (.rg-rail-win). */
    transform: translate(-100%, -50%);
    padding-right: 4px;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-regular);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    z-index: calc(var(--z-page) + 2);
}
/* AU BOUT DROIT IL N'Y A PLUS DE RANGÉE OÙ ÉCRIRE : l'heure de fin s'arrête au
   bord du rail plutôt que d'en sortir, et chevauche les derniers pixels du
   filet. C'est le seul endroit où elle n'est pas dehors, et c'est le prix pour
   que la fenêtre finisse EXACTEMENT où finit la ligne du titre — une rangée qui
   garde 32 px vides à droite pour un cas rare les perdait sur toutes les
   autres. Un min() et non un calcul à la construction : la bascule tient à la
   largeur PEINTE du rail, que seul le navigateur connaît. */
/* UN MOT FERME LA LIGNE — « PERM », ou « + 3h35 » quand le bout est coupé.
   Il réserve plus large qu'une heure : quatre lettres contre « 05 ». La barre
   s'arrête sur la MÊME valeur (report-rail.js pose right: var(--rail-word) sur
   la dernière pièce), donc la gouttière est écrite une fois et lue des deux
   côtés — la déplacer ne peut pas laisser la barre passer sous le mot. */
.rg-h-word { --rail-end: var(--rail-word); }
/* CE QUE LA RANGÉE DIT QUAND IL N'Y A PAS DE FILET À TRACER (décision du
   propriétaire) : le NOTAM commence au-delà de la fenêtre du briefing, donc la
   ligne écrit à partir de quand, sur la ligne même où le filet se serait tenu et
   dans son encre. Sans elle, une rangée hors fenêtre serait la seule à ne rien
   dire du temps. */
.rg-when {
    position: absolute;
    top: 50%;
    left: 0;
    transform: translateY(-50%);
    font-size: var(--fs-2xs);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* ET DANS COMBIEN DE TEMPS (décision du propriétaire) : il n'y a pas de barre
   ici pour dessiner le délai par sa distance au bord, et compter quatre jours
   sur une date se fait de tête. La DATE d'abord, sa lecture ensuite — l'ordre
   même d'une ligne de source (« TAF 17 Sept 11:00 · 2h46 ago ») —, et le délai
   ferme la rangée à DROITE, là où le bout d'une barre écrit le sien. */
.rg-when-in { left: auto; right: 0; }
/* DEUX FAITS, DEUX NOMBRES. Ce `min` retenait l'heure de fin avec --rail-hour,
   le retrait de la FENÊTRE à gauche : les deux valaient 32 px, donc l'un servait
   de mesure à l'autre. Le jour où la fenêtre est partie du titre (--rail-hour: 0
   sur la rangée NOTAM), l'heure de fin s'est mise à pendre hors du rail de sa
   propre largeur. Ce qu'il lui faut est la place qu'ELLE demande à droite, et
   cela s'appelle --rail-end : 35 px, « 00:00 » en IBM Plex à 11 px plus ses
   4 px d'air (mesuré : 32 laissaient l'heure sortir de 3 px). */
.rg-h-end {
    left: min(var(--at), calc(100% - var(--rail-end)));
    transform: translate(0, -50%);
    padding: 0 0 0 4px;
}
/* The section's rows are set in from the card's edges like every other row of
   the card: the header line above them already is. */
/* DE L'AIR SOUS LA TÊTE DE LA SECTION (décision du propriétaire) : le nom d'une
   source et ce qu'il nomme se tiennent à 13 px partout sur la carte — mais ce
   qui suit un METAR est une grille de libellés, qui portent leur propre
   interligne, quand ce qui suit « NOTAM » est un TITRE en gras. Deux titres à
   13 px se lisent comme un seul bloc. Les 8 px sont pris par le corps de la
   liste, donc la ligne de source garde exactement l'air qu'elle a ailleurs. */
.rg-notam-body { padding: 8px 14px 2px; }
/* La dernière entrée rend sa marge : sous elle, l'air est celui du filet entre
   deux terrains (§ 5.3), pas une marge de plus (§ 11.7). */
.notam-list > .notam-entry:last-child { margin-bottom: 0; }

@media (max-width: 840px), (max-height: 500px) {
    .rg-src { gap: 6px; }
}

/* UN CHAMP ACTIF SE LÈVE (docs/ui/grammaire.md, « Le sol et les encres ») : au repos il est un creux
   (--surface-2 ou son voile) ; en saisie il passe sur --card, la réserve, et se
   dessine par un ANNEAU DE 1 PX EN --line — la clarté seule ne se lit pas en
   plein soleil. Écrit en :focus-within / :focus-visible, jamais sur le focus nu. */
.airport-field:focus-within, .config-field:focus-within,
.auth-overlay.is-sheet .airport-field:focus-within,
.auth-overlay.is-sheet .config-field:focus-within,
.field-overlay .picker-in-panel .airport-field:focus-within,
.field-overlay .picker-in-panel .config-field:focus-within,
.route-wp-input:focus-visible, .auth-overlay.is-sheet .route-wp-input:focus-visible,
.auth-view input:focus-visible {
    background: var(--card);
    border-color: transparent;
    box-shadow: 0 0 0 1px var(--contour);
}

/* PRESSÉ (docs/ui/grammaire.md, « Les états ») : --surface-2 le temps du contact, et rien
   d'autre — ni rétrécissement ni opacité (la règle 1.10 réserve l'opacité à ce
   qui est fini ou hors garde). La classe .btn-press est posée par
   bindButtonPress (main.js) au pointerdown et tenue au moins 180 ms, parce que
   :active meurt au lever du doigt et qu'iOS ne l'applique pas à un bouton.
   Écrit en dernier et doublé sur :root : l'état du contact passe devant le fond
   de repos et devant l'état choisi, quel que soit leur sélecteur. Ce qui est
   plein garde une encre lisible sur le creux. */
:root:root :is(.round-icon-btn, .settings-card-add, .shortcut-pill, .view-mode-btn,
    .view-switcher-btn, .wx-dropdown-btn, .primary-btn, .small-btn, .icon-btn, .danger-btn,
    .auth-primary-btn, .auth-secondary-btn, .auth-provider-btn, .auth-close, .source-seg).btn-press {
    background: var(--surface-2);
}
:root:root :is(.primary-btn, .auth-primary-btn).btn-press { color: var(--text); }

/* LA CARTE EMBARQUÉE SUIT LA GRAMMAIRE COMME LE RESTE (docs/ui/grammaire.md, « Les arrondis », « Les couches, le mouvement et les icônes ») : rien ne bouge plus de 200 ms — ni le pouls de la position, ni la roue d'attente
   (« pas de spinner », § 5.7), ni le voile du geste coopératif. Doublé sur
   :root pour passer devant la feuille de la bibliothèque, chargée après. */
:root .maplibregl-user-location-dot::before,
:root .maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon { animation: none; }
:root .maplibregl-cooperative-gesture-screen,
:root .maplibregl-cooperative-gesture-screen.maplibregl-show { transition-duration: 0.2s; transition-delay: 0s; }
/* Le crédit des cartes secondaires (mini-carte des éditeurs) : MapLibre le peint
   en noir à 75 % sur un voile blanc à 50 %, soit 2,8:1 sur un fond sombre. Il
   prend la recette du crédit de la carte météo : --surface, encre --muted. */
:root .maplibregl-map:not(#briefingMap) .maplibregl-ctrl.maplibregl-ctrl-attrib { background-color: var(--surface); }
:root .maplibregl-map:not(#briefingMap) .maplibregl-ctrl-attrib-inner,
:root .maplibregl-map:not(#briefingMap) .maplibregl-ctrl-attrib a { color: var(--muted); }
/* THE WARNINGS SECTION (owner's decision, planche ALERTES): the rows are NOTAM
   rows, so everything here is what a NOTAM row does not already carry — the
   level's dot on the title line, in the section's count and beside the level's
   word in the fold, the zone line, the fold of the whole section. The colour
   is the dot's, nowhere else: the bar stays grey. */
.wx-dot {
    display: inline-block;
    flex: none;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    box-shadow: 0 0 0 1px var(--soft-line);
}
.wx-dot[data-level="yellow"] { background: var(--yellow); }
.wx-dot[data-level="orange"] { background: var(--orange); }
.wx-dot[data-level="red"] { background: var(--red); }
.wx-entry .notam-l1 > .wx-dot { align-self: center; margin: 0 7px 0 4px; }
/* A WARNING LEVEL'S ROW (planche ALERTES V.6, Report › Content): the dot before
   the severity, the colour under the severity — the sub starts where the word does. */
/* Warnings from a level (planche ALERTES-SIGNES V.1): each choice wears the
   Report's dot before its severity. */
.wx-from-row .view-mode-btn > .wx-dot { margin-right: 5px; vertical-align: 1px; }
/* The Alerts page's Warnings view: the levels the Report shows, dot and severity. */
/* THE ALERTS ARE A TAB OF THE BRIEFING PANEL (planche ALERTES P.1): the
   segmented under the panel's head, signed in only. On the Alerts tab the
   briefing's cards step aside and the head names the alerts instead. */
.panel-tabs { display: flex; margin-top: 4px; }
.panel-tabs[hidden] { display: none; }
.panel-tabs .view-mode-btn { flex: 1 1 0; }
.panel-tabs .alerts-badge { position: static; display: inline-block; margin-left: 6px; vertical-align: 1px; }
.panel-tabs .alerts-badge[hidden] { display: none; }
.alerts-tab:not([hidden]) { display: flex; flex-direction: column; gap: var(--card-gap); padding-top: 22px; }
#sidebarPanel.is-alerts > .settings-accordion { display: none; }
#sidebarPanel.is-alerts .panel-head #scheduleOpenBtn,
#sidebarPanel:not(.is-alerts) .panel-head .panel-alerts-head { display: none; }
#sidebarPanel.is-alerts .panel-head #setupMoreBtn { visibility: hidden; }
/* A source's page, or the history, stands like a panel page: its own head,
   over the panel's head and its tabs. */
#sidebarPanel:has([data-page-open]) > .panel-tabs { display: none; }
.alerts-tab[data-page-open] { padding-top: 0; }
.alerts-levels .sheet-v > .wx-dot { margin: 0 5px 0 8px; vertical-align: 1px; }
/* « Ring for » (planche ALERTES N.3): the report's rank dot, ink then mid grey. */
.alerts-rank-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-right: 6px;
    border-radius: 50%;
    vertical-align: 2px;
    background: var(--text);
}
.alerts-rank-dot.is-mid { background: color-mix(in srgb, var(--text) 55%, var(--card)); }
.wx-count {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-xs);
    color: var(--text);
    font-weight: var(--fw-medium);
}
.wx-zone {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 14px;
}
.wx-zone + .wx-body { padding-top: 4px; }
.wx-body + .wx-zone { margin-top: 14px; }
.wx-zone-name { font-weight: var(--fw-semibold); color: var(--heading); font-size: var(--fs-md); }
.wx-zone .section-map-btn { margin-left: auto; }
/* The zone on the map, when its icon opened it: an outline, grey like the bar. */
.map-warning-zone { stroke: var(--muted); stroke-width: 2; fill-opacity: 0.08; }
/* MÉTÉOSAT SUR LA CARTE (planche SAT S.3) : la clé du brouillard, quatre
   pastilles qui SONT la couleur, sans nom de ligne ni couleur en lettres. La
   couleur voyage en --swatch, comme la bande d'une rampe en --band-rgb. */
.wx-key { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: var(--fs-xs); color: var(--text); }
.wx-key-item { display: inline-flex; align-items: center; gap: 5px; }
.wx-swatch { display: inline-block; width: 14px; height: 8px; border-radius: 2px; background: rgb(var(--swatch)); box-shadow: 0 0 0 1px var(--soft-line); }
/* The radar alert's strengths: the legend's swatch, then its word, with the
   air a dot keeps from its word in a list row. */
.radar-alert-swatch { margin-right: 10px; vertical-align: 1px; }
/* Cloud type's two named families (planche CT P.3): the names one width, so
   the two rows' swatches start together; the items closer, two lines in all. */
.ct-key-row { align-items: center; }
.ct-key-row .rp-scale-name { min-width: 3.2rem; }
.ct-key-row .wx-key { gap: 3px 8px; }
.ct-key-row .wx-key-item { gap: 3px; }
.ct-key-row .wx-swatch { width: 10px; }

/* LE PAPIER (planche PDF) : la page Report imprimée telle quelle, par
   l'imprimeur du serveur (`?print`, report-print.js) comme par le navigateur du
   pilote. Le papier ne se touche pas, donc rien de ce qui se touche ne s'y
   imprime ; il ne défile pas, donc rien ne colle et aucun voile ne court sous
   ce qui collait. Le fond est la feuille elle-même : aucun sol n'est imprimé. */
@media print {
    html, body { background: none; }
    .app-menu, .page-header, #statusBar, .install-banner, .sidebar, .panel-scrim,
    .map-view, .route-profile-view, .auth-overlay, .app-splash,
    .zone-tool, .zone-chev, .rg-raw-btn, .section-map-btn, .notam-row-map {
        display: none !important;
    }
    .main { --col-pad: 0px; --main-gutter: 0px; padding: 0; }
    .briefing-zone-head, .briefing-zone-sub { position: static !important; }
    .briefing-zone .briefing-zone-head::after, .briefing-zone .briefing-zone-sub::after { display: none; }
    /* AUCUNE OMBRE (PDF A.9) : la visionneuse de l'iPhone rendait celles des
       cartes en grands aplats gris. Une carte se lit par son fond. */
    *, *::before, *::after { box-shadow: none !important; }
    /* UN TERRAIN PASSE À LA PAGE SUIVANTE ENTRE DEUX BLOCS, JAMAIS DANS UN
       BLOC (PDF A.6), et son nom ne reste pas seul au pied d'une page. */
    .sum-grid, .rg-mc, .rg-tr, .notam-entry { break-inside: avoid; }
    .briefing-zone-head, .briefing-zone-sub, .airport-title, .rg-src { break-after: avoid; }
    .paper-annex { margin-top: 30px; }
}
/* LE METAR À GAUCHE, LE TAF À DROITE DÈS QUE LA CARTE A LA PLACE (planche
   ÉCRAN-LARGE, et le papier avec elle : PDF W.1, W.2) : une carte de terrain
   d'au moins 720 px met le METAR sur deux lignes de trois colonnes, le TAF sur
   ce qui reste, leurs têtes sur la même ligne. Mesuré sur la carte et non sur
   l'écran (un iPad paysage, panneau ouvert, a une carte de 630 à 677 px) ; en
   dessous, rien ne change. Décodé, METAR et TAF sont dans un même bloc ; brut,
   chacun le sien. Un terrain sans TAF garde son METAR sur toute la largeur. */
.airport-card { container: wxcard / inline-size; }
@container wxcard (min-width: 720px) {
    .rg-blk:has(> .rg-tf) {
        display: grid;
        grid-template-columns: minmax(0, 45fr) minmax(0, 55fr);
        column-gap: 20px;
        align-items: start;
    }
    .rg-blk:has(> .rg-tf) > * { grid-column: 1; }
    .rg-blk:has(> .rg-tf) > .rg-tf { grid-column: 2; grid-row: 1 / span 8; }
    /* Côte à côte, la tête « TAF » n'a plus de METAR au-dessus d'elle à
       séparer : elle prend l'air du METAR. */
    .rg-blk:has(> .rg-tf) > .rg-tf > .rg-src { padding-top: 12px; }
    .rg-blk:has(> .rg-tf) .rg-mc { grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr) minmax(0, 1fr); }
    /* T, DP ET H TOUS LES TROIS (ÉCRAN-LARGE L.3, PDF W.4) : la colonne étroite
       ne retire pas l'humidité comme le fait un téléphone. */
    .rg-blk:has(> .rg-tf) .rg-c-tdp { grid-template-columns: repeat(3, max-content); column-gap: 10px; }
    .rg-blk:has(> .rg-tf) .rg-c-tdp .rg-i:nth-child(3) { display: inline-block; }
    .rg-blk:has(> .rg-tf) .rg-c-tdp .rg-lb-h { display: inline; }
    /* Brut, METAR et TAF ont chacun leur bloc : le premier à gauche, le second
       à côté, et ce qui suit repart sous les deux. */
    .rg-blk:not(.wx-warnings):has(+ .rg-blk:not(.wx-warnings)) { float: left; width: 40%; }
    .rg-blk:not(.wx-warnings) + .rg-blk:not(.wx-warnings) { margin-left: calc(40% + 16px); }
    .rg-blk:not(.wx-warnings) + .rg-blk:not(.wx-warnings) ~ * { clear: both; }
}
/* LA PAGE DU PAPIER (`?print`, report-print.js pose `data-paper-view`) est
   mise en page pour la feuille DÈS SON DESSIN, et non au seul moment
   d'imprimer : la tresse du TAF se mesure à la largeur qu'elle a quand elle
   est peinte, et une colonne qui ne naîtrait qu'à l'impression la laisserait
   à sa largeur d'écran. */
/* LE PAPIER EST BLANC (PDF A.10) : un bloc ne porte pas le fond gris de
   l'écran — de l'encre partout, et un gris sale sur une imprimante noir et
   blanc. Les terrains se séparent par leur filet et leur nom. */
html[data-paper-view] .rg-blk { background: none; }
/* LES NOTAM SUR DEUX COLONNES (PDF N.6), dans l'ordre de la liste : la
   colonne se remplit de haut en bas, puis la suivante. En Raw, sans leur
   barre de temps (N.7) : le texte brut seul. */
html[data-paper-view] .notam-list { column-count: 2; column-gap: 22px; }
html[data-paper-view="raw"] .notam-row-line { display: none; }
/* LE FILTRE QUI A ÉCARTÉ UN NOTAM DE L'ANNEXE (PDF N.5) : dans l'encre du
   texte, sur la ligne de critères dont il prend la taille. */
.notam-filtered-by { color: var(--text); font-weight: var(--fw-semibold); }

.paper-head { display: none; }
@media print {
    .paper-head {
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        padding-bottom: 10px;
        margin-bottom: 6px;
        border-bottom: 1px solid var(--line);
    }
    .paper-logo { height: 24px; width: auto; }
    .paper-meta {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        font-size: var(--fs-2xs);
        color: var(--muted);
    }
    .paper-meta b { color: var(--text); font-weight: 600; }
}

/* THE OPERATOR'S BACK-OFFICE (/operator; planche EXPLOITANT, decided by the
   owner): the app's own shell — its page bar, its settings cards and sheets —
   around a page of cards. One column on a phone and a portrait tablet, the
   detail as its own page; on the screen wide enough for the fixed Briefing
   (FIXED_BRIEFING_QUERY), the detail stands beside the list, in the glass of
   the app's right-hand sheets. */
.op-body { margin: 0; background: var(--bg); }
.op-signin { box-sizing: border-box; display: grid; place-items: center; min-height: 100vh; padding: 24px var(--main-gutter); }
.op-signin-panel { max-height: none; padding: 24px; }
.op-signin-panel .auth-title { text-align: center; }
.op-shell { min-height: 100vh; margin-left: var(--rail-w); }
.op-page,
.op-detail { box-sizing: border-box; padding: 0 var(--main-gutter) calc(var(--app-menu-clear) + var(--menu-safe) + 28px); overflow-x: clip; }
.op-page > .tz-page { max-width: 40rem; margin: 0 auto; }
.op-page .overlay-header,
.op-detail .overlay-header { display: flex; align-items: center; gap: 10px; margin: 0 calc(-1 * var(--main-gutter)) 12px; --head-fade: var(--bg); }
.op-detail .overlay-header { --head-fade: var(--glass-thick); }
.op-shell a.round-icon-btn,
.app-menu a.view-switcher-btn { text-decoration: none; }
/* Nothing reorders the operator's minima: the card keeps its grip's place, not
   the grip, so the name and its thresholds still start where the pilot's do. */
.op-shell .config-grip { visibility: hidden; }
/* A minimum's bar (B.5) is the app's, held over the page bar in portrait and
   on the panel's foot beside the list, edge to edge of the column it keeps. */
.op-shell .setup-bar {
    bottom: calc(var(--app-menu-clear) + var(--menu-safe));
    margin: var(--card-gap) calc(-1 * var(--main-gutter)) 0;
    padding-left: var(--main-gutter);
    padding-right: var(--main-gutter);
}
/* The Overview's cards side by side where two fit (the portrait tablet). */
.op-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(18.75rem, 1fr)); gap: var(--card-gap); align-items: start; }
.tz-page.op-grid > .settings-card + .settings-card { margin-top: 0; }
@media (min-width: 69.4375rem) and (min-height: 501px) {
    .op-shell.has-detail { display: grid; grid-template-columns: minmax(0, 1fr) var(--panel-w); }
    .op-detail {
        position: sticky;
        top: 0;
        height: 100vh;
        overflow-y: auto;
        background: var(--glass-thick);
        box-shadow: var(--glass-drop-side), inset 1px 0 0 var(--glass-line);
    }
}
