/* Gateka Core — the shared UI vocabulary (ux-system-epic §D1).
   Linked from templates/gk_base.html; also servable standalone over the
   CDN (gateka.style.css_link) to a site that never includes _theme.html.

   THE CLOSED LIST. A panel writes itself out of these and nothing else —
   no hardcoded colour, no magic spacing. If something is missing, the
   vocabulary is incomplete and the fix belongs HERE, not in the panel.
   tests/unit/test_ui_vocabulary.py pins this list against the file.

     panel      .gk-panel .gk-panel-head .gk-panel-title .gk-panel-sub
     layout     .gk-stack .gk-cluster .gk-card-grid (--tight --fit)
                .gk-separator
     card       .gk-card .gk-card-head .gk-card-title .gk-card-body
                (data-tone paints the state stripe; aria-pressed makes
                 the card a toggle, same axis as .gk-chip)
     button     .gk-btn (--primary --danger --outline --ghost --sm)
     field      .gk-field (--sm, data-dirty)
     badge      .gk-badge (data-tone=danger|warning|success|info)
     chip       .gk-chip .gk-chip-count .gk-chip-dot (aria-pressed, data-tone)
     tabs       .gk-tabs .gk-tab (aria-selected)
     meter      .gk-meter .gk-meter-fill (data-tone)
     table      .gk-table .gk-table-scroll
     empty      .gk-empty (--center --loading --denied, data-tone)
                .gk-empty-action — the call to action; an absence that does
                not say what to DO is an observation, not a door (§D6)
     text       .gk-kv .gk-kv-k .gk-code .gk-muted .gk-truncate
                .gk-list .gk-prewrap
     disclosure .gk-details .gk-summary
     palette    .gk-cmdk .gk-cmdk-box .gk-cmdk-field .gk-cmdk-input
                .gk-cmdk-list .gk-cmdk-item .gk-cmdk-label .gk-cmdk-hint
                .gk-kbd
     menu       .gk-menu .gk-menu-group .gk-menu-item .gk-menu-label
                .gk-menu-desc .gk-menu-foot
                (le menu ANCRÉ à un bouton — la palette ci-dessus est un
                 dialogue plein écran et ne peut pas l'être. aria-expanded
                 sur le déclencheur, aria-checked sur la ligne choisie.
                 .gk-menu-desc est ce qui fait le travail : deux entrées
                 sans phrase se lisent comme deux mots — §D2 bis)
     motion     .gk-enter .gk-stagger .gk-stagger-item .gk-reveal
                .gk-ribbon .gk-ribbon-track
                (§C — one arrival, a ranked container, a scroll reveal.
                 They live in THIS list and not in a gk-motion.css: a
                 motion class is a vocabulary class, and a second closed
                 list is a second source. Tokens stay in _theme.html.)

   Tokens live in templates/_theme.html — spacing, type, radius, tone,
   motion — NOT here. Every rule below reads them with a literal fallback
   so the sheet still paints standalone. Architect consult
   2026-08-14-lot-d1-de-ux-system-epic-571a79 (extend_existing).

   Pre-existing blocks (.gk-status, .gk-member, .gk-modal, .gk-toggle,
   .gk-editor-*) are NOT part of the §D1 list and are left alone. */

/* ── Design Tokens ──────────────────────────────────────────────────────
   This block used to DECLARE the palette. It no longer does, and that is
   the point of the 2026-08-14 change (ux-system-epic §D1, architect
   consult 2026-08-14-lot-d1-de-ux-system-epic-571a79 → extend_existing).

   `templates/_theme.html` declares --gk-bg, --gk-text, --gk-border,
   --gk-radius, --gk-font, --gk-danger — and this file redeclared the SAME
   six names with DIFFERENT values (#fafafa vs #f8f9fa, #1a1a2e vs #1a202c,
   rgba(0,0,0,.1) vs #e5e7eb, Inter vs system-ui) under a looser dark
   selector ([data-theme=dark] against html[data-theme=dark]). Two sources
   for one notion. It went unnoticed for as long as it did because NOTHING
   LINKED THIS FILE: zero <link> in gateka-core, oxygen-core or
   gateka-data, and gateka.style.css_link() had no caller. The day the
   <link> landed in gk_base.html those six lines would have repainted the
   admin palette of all 18 products as a side effect.

   So: the six duplicated names are gone. Redeclaring them as
   `--gk-bg: var(--gk-bg, …)` would NOT have worked either — a custom
   property that references itself is a cycle, invalid at computed-value
   time, and paints nothing.

   What remains are the names this sheet alone owns, each ALIASED to the
   bridge var _theme.html publishes, with the old literal kept as the
   fallback. No cycle (the names differ), and the fallback is what keeps
   the sheet usable STANDALONE over the CDN by a third-party site that
   never includes _theme.html. Same reasoning one level down: rules below
   read `var(--gk-space-3, 12px)` rather than assuming the token exists. */
:root, [data-theme="light"] {
  --gk-accent:       var(--accent, #3b82f6);
  --gk-accent-hover: var(--gk-primary-hover, #2563eb);
  --gk-bg2:          var(--surface, #ffffff);
  --gk-bg3:          var(--bg3, rgba(0,0,0,.03));
  --gk-text2:        var(--text2, #555);
  --gk-text3:        var(--text3, #999);
}
/* Standalone-dark only. When _theme.html IS present every var() above
   already resolved to a token that html[data-theme="dark"] flipped, so
   this block changes nothing; when it is absent, these literals are the
   only dark values a CDN consumer gets. */
[data-theme="dark"] {
  --gk-accent:       var(--accent, #3b82f6);
  --gk-accent-hover: var(--gk-primary-hover, #60a5fa);
  --gk-bg2:          var(--surface, #0f1520);
  --gk-bg3:          var(--bg3, rgba(255,255,255,.04));
  --gk-text2:        var(--text2, #94a3b8);
  --gk-text3:        var(--text3, #475569);
}

/* ── Shared Components ── */

/* Task status badge */
.gk-status { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .3px; }
.gk-status--backlog { background: #64748b22; color: #64748b; }
.gk-status--todo { background: #3b82f622; color: #3b82f6; }
.gk-status--in_progress { background: #f59e0b22; color: #f59e0b; }
.gk-status--review { background: #a855f722; color: #a855f7; }
.gk-status--done { background: #22c55e22; color: #22c55e; }

/* Member badge */
.gk-member { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; border: 1px solid; border-radius: 12px; padding: 2px 8px 2px 2px; }
.gk-member-initial { width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: #fff; }
.gk-member--owner { border-color: #3b82f6; }
.gk-member--owner .gk-member-initial { background: #3b82f6; }
.gk-member--developer { border-color: #8b5cf6; }
.gk-member--developer .gk-member-initial { background: #8b5cf6; }
.gk-member--reviewer { border-color: #f59e0b; }
.gk-member--reviewer .gk-member-initial { background: #f59e0b; }
.gk-member--client { border-color: #22c55e; }
.gk-member--client .gk-member-initial { background: #22c55e; }

/* Card */
/* `min-width: 0` for the same reason as .gk-table-scroll: a card is a grid
   item, and `min-width: auto` would let unbreakable content (a long file
   path) widen the track instead of wrapping inside it. */
.gk-card { background: var(--gk-bg2, #fff); border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); border-radius: var(--gk-radius, 10px); padding: var(--gk-space-3, 12px) var(--gk-space-4, 16px); min-width: 0; transition: border-color var(--gk-duration, 180ms) var(--gk-ease, ease); }
.gk-card:hover { border-color: var(--gk-accent, #3b82f6); }
/* Only cards that are actually actionable get a press state — a static
   card that shrinks under the finger is a lie about what it does. */
.gk-card[role="button"], .gk-card a.gk-card, a.gk-card { touch-action: manipulation; }
.gk-card[role="button"]:active, a.gk-card:active { transform: scale(.995); }
/* A card that is NOT actionable should not pretend on hover either. The
   accent border says "click me"; on an inventory grid nothing happens. */
.gk-card[data-static]:hover { border-color: var(--gk-border, rgba(0,0,0,.1)); }
/* The state stripe (§D4). A card that says WHICH state its row is in used
   to do it with `border-left: 3px solid #16a34a` written per panel — five
   literals in _apps_kanban.html alone, none of which can invert. This is
   the SAME --_tone the badge, the empty state and the code span read, so
   a tile and the badge inside it cannot drift to two different greens.
   No new class: a stripe is not a notion, it is a tone on a card. */
.gk-card[data-tone] { border-left: var(--gk-stripe, 3px) solid var(--_tone); }
.gk-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--gk-space-2, 8px); }
.gk-card-title { font-size: var(--gk-text-base, 13px); font-weight: var(--gk-weight-bold, 700); display: flex; align-items: center; gap: var(--gk-space-1, 4px); min-width: 0; }
.gk-card-title > i { color: var(--gk-accent, #3b82f6); flex: none; }
/* The one thing a card title must survive: a name longer than the card.
   Declared as a free utility, not scoped to the title — a table cell
   needs the same behaviour, and scoping it here would have forced a
   second declaration there. */
.gk-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gk-card-body { display: flex; flex-direction: column; gap: var(--gk-space-1, 4px); }

/* A card that TOGGLES (§D4). The preference tiles were `.prof-opt-card`
   with `.is-selected` — a <button> announced to a screen reader as
   unpressed in every state, which is the exact defect `.gk-chip` was
   built to stop. So the state is `aria-pressed`, on the axis the chip
   already declared, rather than a sixth selected-card class.

   NOT a `.gk-chip`: a chip is a one-line pill (4/12px) and these tiles
   carry a 32px provider logo, a name and a check. NOT a new
   `.gk-option-tile` either — a tile you select IS a card, and adding a
   card name to the list built to stop the 46th definition would be the
   joke writing itself (consult …-05fb14, Q2).

   `font: inherit` and `text-align: left` because the element is a
   <button>: without them it renders in the UA's 13.33px Arial, centred,
   and reads as a form control dropped into the grid.

   ⚠ The reset is on `button.gk-card` and NOT on `[aria-pressed]` alone —
   widened 2026-08-15 by §D4 lot 3, which found the gap by hitting it. A
   card that ACTIVATES (opens a record, follows a link) is a <button>
   just as much as a card that TOGGLES, and it carries no `aria-pressed`
   because it has no pressed state to announce. Under the old selector
   such a card got the UA's centred 13.33px Arial and read as a form
   control; the panel's own fix would have been a local `font: inherit`,
   i.e. a panel overriding a primitive — the shape §D1 exists to stop.

   The two halves stay separate on purpose: this half is the ELEMENT
   reset (any button-shaped card), the half below is the STATE (only a
   card that toggles). Folding them would paint a pressed ring on a card
   that cannot be pressed. */
button.gk-card, .gk-card[aria-pressed] { cursor: pointer; font: inherit;
  font-size: var(--gk-text-base, 13px); color: var(--gk-text, inherit);
  text-align: left; width: 100%; position: relative; }
/* The ring is the same shape `.gk-field:focus` uses, at the accent — a
   pressed tile has to read as pressed with the border alone lost to a
   forced-contrast palette, which is why the border-colour change is
   doubled by a fill-tinted ring rather than replaced by one. */
.gk-card[aria-pressed="true"] { border-color: var(--gk-accent, #3b82f6);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gk-accent, #3b82f6) 15%, transparent); }

/* Button
   `transition: opacity` alone gave a tap NO acknowledgement on touch: no
   press state existed anywhere in the platform. `touch-action: manipulation`
   drops the legacy 300ms double-tap-zoom delay, and an explicit
   tap-highlight replaces the browser's default grey flash. This is the part
   that works on every device — including iPhone, where navigator.vibrate()
   does not exist at all.

   ⚠ The base `.gk-btn` now paints a complete NEUTRAL button (surface,
   border, ink) instead of `border: none` and nothing else. It had to:
   templates already write bare `class="gk-btn"` (_audit_panel.html), and
   under the old rule the day this sheet got linked those buttons would
   have rendered transparent-on-transparent — worse than the unstyled
   browser default they had while nothing linked it. A base primitive has
   to be usable with no modifier. */
.gk-btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--gk-space-1, 4px);
  padding: var(--gk-space-2, 8px) var(--gk-space-4, 16px);
  background: var(--gk-bg2, #fff); color: var(--gk-text, #1a202c);
  border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  border-radius: var(--gk-radius-sm, 6px);
  font-size: var(--gk-text-base, 13px); font-weight: var(--gk-weight-medium, 600); cursor: pointer;
  font-family: var(--gk-font, system-ui, sans-serif);
  transition: opacity var(--gk-duration-fast, 120ms) var(--gk-ease, ease), transform var(--gk-duration-fast, 120ms) var(--gk-ease, ease), background var(--gk-duration-fast, 120ms) var(--gk-ease, ease);
  touch-action: manipulation;
  -webkit-tap-highlight-color: color-mix(in srgb, var(--gk-accent, #3b82f6) 18%, transparent); }
.gk-btn:hover:not(:disabled) { opacity: .9; }
.gk-btn:active:not(:disabled) { transform: scale(.97); opacity: .85; }
/* The ring used to live here, ENUMERATED across five selectors
   (.gk-btn, .gk-summary, .gk-card[tabindex], .gk-chip, .gk-tab). §D3
   replaced it with one base rule — see « The focus ring » below. An
   enumeration is a list someone has to remember to extend, and measured
   on beta.gateka.fr/apps it had already been forgotten twelve times. */
.gk-btn:disabled, .gk-btn[data-disabled] { opacity: .5; cursor: not-allowed; }
/* `display: inline-flex` above beats the UA stylesheet's
   `[hidden] { display: none }`. Without this rule a button the server or
   the JS marks `hidden` stays on screen — measured twice: the apps
   kanban's « Désinstaller » next to the confirm step it had just opened,
   and again after a successful uninstall. Same trap as .gk-cmdk[hidden]. */
.gk-btn[hidden] { display: none; }
.gk-btn--primary { background: var(--gk-accent, #3b82f6); border-color: var(--gk-accent, #3b82f6); color: #fff; }
.gk-btn--danger { background: var(--gk-danger, #ef4444); border-color: var(--gk-danger, #ef4444); color: #fff; }
.gk-btn--outline { background: none; color: var(--gk-text2, #555); }
/* An outline button takes a tone. This is not decoration: ADR-0160 §1.2
   argues a brick uninstall is destructive but NOT alarmist (it removes a
   mount, it destroys no data), so the apps kanban deliberately painted
   its own outlined red button rather than use a solid fill. That reading
   was right and had nowhere to live in the vocabulary — one rule on the
   existing tone axis is where it lives now, instead of a sixth --danger
   variant per panel. */
.gk-btn--outline[data-tone] { border-color: color-mix(in srgb, var(--_tone) 45%, transparent);
  color: color-mix(in srgb, var(--_tone) 72%, var(--gk-text, #1a202c)); }
.gk-btn--outline[data-tone]:hover:not(:disabled) { opacity: 1;
  background: color-mix(in srgb, var(--_tone) 8%, transparent); }
.gk-btn--ghost { background: none; border-color: transparent; color: var(--gk-text2, #555); }
.gk-btn--ghost:hover:not(:disabled) { background: var(--surface-hover, rgba(0,0,0,.05)); opacity: 1; }
.gk-btn--sm { padding: var(--gk-space-1, 4px) var(--gk-space-3, 12px); font-size: var(--gk-text-xs, 11px); }

/* Field — a text input, a textarea, a select.
   The vocabulary shipped with a button, a badge, a table and an empty
   state, and no INPUT. That gap is not theoretical: the zones drawer had
   to invent `.gkz-field` with its own border, its own radius, its own
   focus ring and its own dirty colour — a 46th card in input form, on the
   one surface epic §B bis says must share the system ("the ux and way to
   build portal should use the same type of ux"). Added HERE rather than
   there, which is the rule §D1 states for exactly this case.

   `min-width: 0` and `box-sizing` are not cosmetic: a field is almost
   always a flex item (a row of them in a collection editor), and a bare
   <input>'s default `min-width: auto` resolves to its ~20-character size
   attribute and refuses to shrink — the same defect .gk-table-scroll
   carries a comment about, measured again in the drawer at 390px.

   Unsaved is expressed as `data-dirty`, not a modifier class, following
   data-tone / data-static above: a state kept in two places is a state
   that can disagree with itself. */
.gk-field { width: 100%; box-sizing: border-box; min-width: 0; display: block;
  padding: var(--gk-space-2, 8px) var(--gk-space-3, 12px);
  background: var(--gk-bg2, #fff); color: var(--gk-text, #1a202c);
  border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  border-radius: var(--gk-radius-sm, 6px);
  font-family: var(--gk-font, system-ui, sans-serif);
  font-size: var(--gk-text-base, 13px); line-height: var(--gk-leading, 1.55);
  resize: vertical;
  transition: border-color var(--gk-duration-fast, 120ms) var(--gk-ease, ease),
              box-shadow var(--gk-duration-fast, 120ms) var(--gk-ease, ease); }
/* :focus and not :focus-visible — unlike a button, a text field IS focused
   by the mouse click that starts the edit, and the ring is what says which
   box the keystrokes are going into. */
.gk-field:focus { outline: none; border-color: var(--gk-accent, #3b82f6);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gk-accent, #3b82f6) 22%, transparent); }
.gk-field::placeholder { color: var(--gk-text3, #999); }
.gk-field[data-dirty] { border-color: var(--gk-warning, #d97706);
  background: color-mix(in srgb, var(--gk-warning, #d97706) 8%, var(--gk-bg2, #fff)); }
.gk-field--sm { padding: var(--gk-space-1, 4px) var(--gk-space-2, 8px); font-size: var(--gk-text-xs, 11px); }

/* Modal */
.gk-modal-overlay { position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,.5); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; }
.gk-modal { background: var(--gk-bg2, #fff); border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); border-radius: var(--gk-radius-lg, 14px); padding: var(--gk-space-5, 24px); max-width: 400px; width: 90%; box-shadow: 0 16px 40px rgba(0,0,0,.3); }
.gk-modal-title { font-size: 15px; font-weight: var(--gk-weight-bold, 700); margin-bottom: var(--gk-space-2, 8px); }
.gk-modal-message { font-size: var(--gk-text-base, 13px); color: var(--gk-text2, #555); line-height: 1.5; margin-bottom: 20px; }
.gk-modal-actions { display: flex; gap: var(--gk-space-2, 8px); justify-content: flex-end; }

/* Toggle switch */
.gk-toggle { width: 40px; height: 22px; border-radius: 11px; border: none; cursor: pointer; background: var(--gk-border, rgba(0,0,0,.1)); position: relative; transition: background var(--gk-duration, 180ms); padding: 0; }
.gk-toggle--on { background: var(--gk-accent); }
.gk-toggle-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: transform var(--gk-duration, 180ms); }
.gk-toggle--on .gk-toggle-knob { transform: translateX(18px); }

/* ── §D1 primitives ─────────────────────────────────────────────────────
   The six the epic names — a panel, a card, a button, a badge, a table,
   an empty state — plus the two layout primitives without which a panel
   still has to invent its own spacing (a vertical stack and a horizontal
   cluster), and a separator.

   Names are borrowed from Radix / shadcn-ui — card, button, badge, table,
   empty, separator — and STATE is expressed as data attributes rather
   than modifier classes (`data-tone`, `data-disabled`, `data-static`).
   Nothing is installed: those packages are React-first and Gateka renders
   Jinja with vanilla JS, so ADR-0008 governs nothing here — we follow the
   specification, we do not take the dependency. What is deliberately NOT
   borrowed is the Root/Trigger/Content split and `asChild`, which have no
   meaning in a server-rendered template.

   ⚠ This list is CLOSED. It is what makes the system greppable — not the
   prefix, which `gk-` already shares with legacy panel classes
   (.gk-notes-card, .gk-seo-kw-*). tests/unit/test_ui_vocabulary.py pins
   it: adding a class here without adding it there fails, and that is the
   only thing standing between this file and a 46th definition of a card. */

/* ── The focus ring (ux-system-epic §D3) ────────────────────────────────
   « Le clavier comme chemin PRINCIPAL. Pas comme accessibilité ajoutée
   après coup. » So the ring is declared ONCE, for anything the browser
   can focus, instead of being listed per component.

   Measured connected on beta.gateka.fr/apps, 2026-08-14, by classifying
   which loaded rule wins on :focus-visible for every stop of a real Tab
   walk (30 distinct controls):

     12  carried --gk-focus-ring        (the five enumerated selectors)
     12  fell through to the BROWSER's own outline
      6  had their outline suppressed by gk-nav.css and replaced by a
         5 %-opacity pill

   Three affordances for one notion, on one page. The middle twelve are
   the point: nobody chose the browser outline — it is what a control
   gets when the enumeration forgets it, and it neither reads the theme
   nor matches the other two.

   Selector shape, and why it is NOT `:where()`. `:where()` has
   specificity zero, which is attractive (it can never repaint a panel
   that already decided) but it also loses to any class rule that sets
   `box-shadow` — `.gk-field:focus` is exactly that, and a field would
   have kept its own ring, which is correct, while a card inside a
   panel that shades itself would have silently lost one. Bare element
   selectors sit at (0,1,1) and `[tabindex]` at (0,2,0): high enough to
   beat a plain class, low enough that `.gk-field:focus` (0,2,0) still
   wins where a component genuinely owns its focus paint.

   The transparent `outline` is not decoration. A box-shadow is painted,
   so Windows High Contrast / forced-colors mode drops it entirely; a
   transparent outline is re-coloured by the OS and is the only thing
   those users see. It also survives an `overflow: hidden` ancestor,
   which clips a shadow. Both are why `outline: none` alone — what the
   five enumerated rules each said — was never quite right.

   :focus-visible and never :focus: a mouse click should not leave a
   ring behind. The one deliberate exception is `.gk-field`, which uses
   :focus because a text field IS focused by the click that starts the
   edit — see its own rule and comment above. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid transparent; outline-offset: 2px;
  box-shadow: var(--gk-focus-ring, 0 0 0 2px #fff, 0 0 0 4px #3b82f6); }


/* ── `hidden` must actually hide (§D4 lot 2) ─────────────────────────────
   The UA stylesheet's `[hidden] { display: none }` is a type selector at
   specificity (0,0,1). Every primitive above that declares a `display` is
   (0,1,0) and beats it, so the attribute becomes INERT on exactly the
   classes a panel is most likely to toggle — and nothing errors, nothing
   logs, the element simply paints.

   This file paid it three times before the rule existed: `.gk-cmdk` opened
   the palette over the page on every load, `.gk-btn` left « Désinstaller »
   on screen beside the confirm step it had just opened, and `.gk-badge`
   painted a « Relevés 0 » count on the finances tab bar that had been
   correctly invisible for months under the class it replaced. Three is a
   pattern, so it is answered once, for all of them.

   Yes, this is an enumeration — the shape ADR-0185 §5 distrusts, because
   somebody has to remember to extend it. Here nobody does:
   test_every_primitive_that_sets_display_also_answers_hidden derives the
   list from the file and fails the day a primitive declares a `display`
   without joining this rule. An enumeration a test maintains is a list;
   an enumeration a human maintains is a future bug. */
.gk-card-body[hidden],
.gk-card-grid[hidden],
.gk-card-head[hidden],
.gk-card-title[hidden],
.gk-chip[hidden],
.gk-cluster[hidden],
.gk-cmdk-box[hidden],
.gk-cmdk-field[hidden],
.gk-details[hidden],
.gk-field[hidden],
.gk-kv[hidden],
.gk-kv-k[hidden],
.gk-meter-fill[hidden],
.gk-panel-title[hidden],
.gk-stack[hidden],
.gk-summary[hidden],
.gk-tab[hidden],
.gk-tabs[hidden] { display: none; }

/* Panel — the outer shell of a /features pane: padding, title, subtitle.
   Every one of the 93 panels re-invented this, which is why `padding:
   14px 18px` and `12px 14px` both exist for the same intent. */
.gk-panel { padding: var(--gk-space-3, 12px) var(--gk-space-4, 16px); color: var(--gk-text, inherit); font-size: var(--gk-text-base, 13px); line-height: var(--gk-leading, 1.55); }
.gk-panel-head { margin-bottom: var(--gk-space-4, 16px); }
.gk-panel-title { margin: 0 0 var(--gk-space-1, 4px); font-size: var(--gk-text-xl, 18px); font-weight: var(--gk-weight-bold, 700); display: flex; align-items: center; gap: var(--gk-space-2, 8px); }
.gk-panel-sub { margin: 0; color: var(--gk-text2, #555); font-size: var(--gk-text-sm, 12px); }

/* Stack / cluster — vertical and horizontal rhythm. The gap is a custom
   property so a caller can step it without a new class. */
.gk-stack { display: flex; flex-direction: column; gap: var(--gk-stack-gap, var(--gk-space-3, 12px)); }
.gk-stack--tight { --gk-stack-gap: var(--gk-space-1, 4px); }
.gk-stack--loose { --gk-stack-gap: var(--gk-space-5, 24px); }
.gk-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gk-cluster-gap, var(--gk-space-2, 8px)); }
.gk-cluster--tight { --gk-cluster-gap: var(--gk-space-1, 4px); }
/* Card grid. The minimum column width is a system decision, not a
   per-panel one — that is exactly the magic number this replaces.
   Named `gk-card-grid` and not `gk-grid`: templates/index.html already
   defines `.gk-grid` for the marketing services grid, and taking the
   shorter name would have been the system's very first duplicate. */
.gk-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--gk-grid-min, 300px), 1fr)); gap: var(--gk-space-3, 12px); }
/* An absence inside a grid spans the GRID. Added 2026-08-15 by §D4 lot 3,
   which first wrote this rule in the panel it needed it in — i.e. a panel
   overriding a primitive, the shape §D1 exists to stop, caught on re-read.
   It belongs here because it is true everywhere: dropped into one column,
   « aucun concurrent trouvé » lays out as a narrow card and reads as one
   more result rather than as the statement that there are none. The
   `.gk-seo-comp-row` it replaced said this with `style="grid-column:1/-1"`
   inline, on an element that also carried a card class. */
.gk-card-grid > .gk-empty { grid-column: 1 / -1; }
/* A grid of small tiles — a provider chooser, not a content grid. The
   300px default would put one tile per row on a phone and two on a
   desktop for a 13px label. Declared as a modifier on the same custom
   property, following .gk-stack--tight, rather than as a second grid.

   140px is INHERITED, not chosen: it is the minimum `.prof-card-grid`
   was already using. Measured at 390 and at 1440, 160 renders the same
   column count — so there is no evidence for a different number, and
   inventing one would make this a system decision nobody took. */
.gk-card-grid--tight { --gk-grid-min: 140px; }
/* A grid of a FIXED, small set — four RGPD buckets, two sub-scores, three
   competitors. `auto-fill` keeps the empty tracks so the column rhythm
   stays put as items are added, which is right for a catalogue and wrong
   here: measured 2026-08-15 at 1440px, the four RGPD cards landed in four
   of seven 140px tracks and left 43% of the row blank. The panel they
   replaced said `auto-fit` and stretched.

   A modifier on the same axis as --tight, not a second grid: the choice
   between fill and fit is a statement about whether the SET grows, and
   that is a property of the data the panel shows. Naming it makes the
   panel say which one it has instead of inheriting whichever the system
   happened to pick. */
.gk-card-grid--fit { grid-template-columns: repeat(auto-fit, minmax(var(--gk-grid-min, 300px), 1fr)); }

.gk-separator { border: 0; border-top: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); margin: var(--gk-space-2, 8px) 0 0; }
.gk-separator--dashed { border-top-style: dashed; }

/* Badge — a tinted chip. The tone SURFACE and the tone INK are both
   derived from one semantic colour with color-mix(), which is what makes
   this one declaration instead of two (light + dark):
     · the surface is the tone at 14% over whatever is beneath it, so it
       composes on a white card and on a #1a1e27 one alike;
     · the ink is the tone pulled 38% toward --gk-text, which is dark in
       light mode and light in dark mode — so contrast holds in both
       without a second block. Hardcoding #b91c1c on #fee2e2 (what the
       panels did) reads correctly in exactly one theme.
   color-mix() is safe here: this sheet already relied on it for the
   button tap-highlight, and it is baseline across Chrome/Firefox/Safari
   since 2023. */
/* `data-tone` is declared ONCE, globally, and every tone-aware primitive
   reads the private --_tone it sets. That is why a badge, an inline code
   span and an empty state can all say data-tone="danger" and mean the
   same red — instead of each carrying its own --danger modifier class.
   A primitive that must not inherit a parent's tone re-declares --_tone
   on itself (.gk-badge does), which wins over inheritance. */
[data-tone="danger"]  { --_tone: var(--gk-danger, #ef4444); }
[data-tone="warning"] { --_tone: var(--gk-warning, #d97706); }
[data-tone="success"] { --_tone: var(--gk-success, #16a34a); }
[data-tone="info"]    { --_tone: var(--gk-accent, #3b82f6); }

.gk-badge { --_tone: var(--gk-text3, #999);
  display: inline-flex; align-items: center; gap: var(--gk-space-1, 4px);
  padding: var(--gk-space-05, 2px) var(--gk-space-2, 8px); border-radius: var(--gk-radius-full, 999px);
  font-size: var(--gk-text-xs, 11px); font-weight: var(--gk-weight-medium, 600); line-height: 1.5;
  background: color-mix(in srgb, var(--_tone) 14%, transparent);
  color: color-mix(in srgb, var(--_tone) 62%, var(--gk-text, #1a202c)); }
.gk-badge[data-tone="danger"]  { --_tone: var(--gk-danger, #ef4444); }
.gk-badge[data-tone="warning"] { --_tone: var(--gk-warning, #d97706); }
.gk-badge[data-tone="success"] { --_tone: var(--gk-success, #16a34a); }
.gk-badge[data-tone="info"]    { --_tone: var(--gk-accent, #3b82f6); }
.gk-badge--upper { text-transform: uppercase; letter-spacing: .4px; }
.gk-badge--mono { font-family: var(--gk-mono, monospace); border-radius: var(--gk-radius-sm, 6px); font-weight: 400; }
/* `[hidden]` LOSES to an author display rule — the third time this file
   pays it (.gk-btn[hidden], .gk-cmdk[hidden]). Found by the capture, not
   by reading: converting the finances tab counts onto .gk-badge made
   `<span class="gk-badge" id="fin-releves-count" hidden>0</span>` paint a
   « Relevés 0 » pill that had been correctly invisible for months, because
   the class it replaced set no `display` at all. A badge is the primitive
   MOST likely to be server-rendered-then-toggled — a count only exists
   when there is something to count. */
.gk-badge[hidden] { display: none; }

/* Chip — a filter that TOGGLES. Deliberately not a .gk-badge modifier:
   a badge is a static <span> that labels, a chip is a <button> that has a
   pressed state, a hover, a focus ring and a keyboard contract. Folding
   the two would put an interaction on the one class whose contract is
   that it has none.

   Pressed is `aria-pressed="true"`, not `.is-active`. The accessibility
   attribute and the paint then cannot disagree — which is exactly the
   failure `.ak-state-pill.is-active` had: it was a <button> announced to
   a screen reader as unpressed in every state. Same discipline as
   data-tone / data-dirty above. */
.gk-chip { --_tone: var(--gk-accent, #3b82f6);
  display: inline-flex; align-items: center; gap: var(--gk-space-1, 4px);
  padding: var(--gk-space-1, 4px) var(--gk-space-3, 12px);
  border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  border-radius: var(--gk-radius-full, 999px);
  background: var(--gk-bg2, #fff); color: var(--gk-text, #1a202c);
  font: inherit; font-size: var(--gk-text-sm, 12px); cursor: pointer;
  touch-action: manipulation;
  transition: background var(--gk-duration-fast, 120ms) var(--gk-ease, ease),
              border-color var(--gk-duration-fast, 120ms) var(--gk-ease, ease); }
.gk-chip:hover { background: var(--gk-bg3, rgba(0,0,0,.05)); }
.gk-chip[aria-pressed="true"] {
  background: var(--_tone); border-color: var(--_tone);
  color: var(--gk-bg2, #fff); font-weight: var(--gk-weight-medium, 600); }
/* The count rides INSIDE the chip, so it must invert with it. */
.gk-chip-count { font-size: var(--gk-text-xs, 11px); opacity: .8;
  padding: 0 var(--gk-space-1, 4px); border-radius: var(--gk-radius-full, 999px);
  background: var(--gk-bg3, rgba(0,0,0,.05)); }
.gk-chip[aria-pressed="true"] .gk-chip-count { background: color-mix(in srgb, var(--gk-bg2, #fff) 25%, transparent); }
/* The dot shows the tone even while the chip is UNPRESSED — that is the
   whole point of it: a filter row where « Installées » is green before
   you click it. It defaults to muted rather than to the chip's --_tone,
   because --_tone also drives the pressed fill and an untoned chip must
   not sprout a blue dot it never had. */
.gk-chip-dot { width: var(--gk-space-2, 8px); height: var(--gk-space-2, 8px);
  border-radius: var(--gk-radius-full, 999px); flex: none;
  background: var(--gk-text3, #999); }
.gk-chip[data-tone] .gk-chip-dot { background: var(--_tone); }
.gk-chip[aria-pressed="true"] .gk-chip-dot { background: var(--gk-bg2, #fff); }

/* Tabs — the most duplicated notion in the tree after the card: 25
   distinct `-tab` prefixes on 2026-08-14 (.admin-tab, .feat-tab, .fin-tab,
   .gk-edt-tab, .gkarch-tab, .gk-llm-tab, .gk-sec-tab, .cp-tab,
   .ak-panel-tab, .gk-tasks-tab, …), every one of them an underline strip.

   `aria-selected`, not `.is-active`, for the reason the chip states.
   `flex-wrap` is measured, not defensive: at 390px an eleven-tab bar
   clipped its right-most item (_tasks_panel.html, SEEN gate 2026-08-04). */
.gk-tabs { display: flex; flex-wrap: wrap; gap: var(--gk-space-05, 2px);
  border-bottom: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  margin-bottom: var(--gk-space-3, 12px); }
/* `inline-flex` and a gap, not a `margin-right` on the icon: nearly every
   tab bar in the tree carries `<i>` + label, and _finances_panel.html was
   about to keep a `.gk-tab i` rule of its own to say so — a panel
   overriding a primitive, which is the shape §D1 exists to stop (caught
   by bin/find-class-collisions, not by review). */
.gk-tab { display: inline-flex; align-items: center; gap: var(--gk-space-1, 4px);
  background: none; border: 0; cursor: pointer; font: inherit;
  font-size: var(--gk-text-base, 13px); font-weight: var(--gk-weight-medium, 600);
  color: var(--gk-text2, #555);
  padding: var(--gk-space-2, 8px) var(--gk-space-3, 12px);
  border-bottom: 2px solid transparent; margin-bottom: calc(-1 * var(--gk-border-width, 1px));
  transition: color var(--gk-duration-fast, 120ms) var(--gk-ease, ease); }
.gk-tab:hover { color: var(--gk-text, #1a202c); }
.gk-tab[aria-selected="true"] { color: var(--gk-accent, #3b82f6); border-bottom-color: var(--gk-accent, #3b82f6); }

/* Meter — a progress bar. Five implementations on 2026-08-14
   (_lorvert_sales_plan, _arch_audit_panel, _tasks_roadmap_tab,
   _architecture_board_tab, _system_monitor), each with its own height,
   its own radius and its own hardcoded fill colour.
   The WIDTH is the one thing a server must state inline — it is data,
   not style — so `style="width: N%"` on the fill is intended here and is
   the only inline style the vocabulary sanctions. */
.gk-meter { height: var(--gk-space-05, 2px); height: 6px;
  background: var(--gk-bg3, rgba(0,0,0,.05));
  border-radius: var(--gk-radius-full, 999px); overflow: hidden; }
.gk-meter-fill { --_tone: var(--gk-success, #16a34a);
  display: block; height: 100%; background: var(--_tone);
  transition: width var(--gk-duration, 180ms) var(--gk-ease, ease); }

/* Table */
.gk-table { width: 100%; border-collapse: collapse; font-size: var(--gk-text-xs, 11px); }
.gk-table th { text-align: left; font-weight: var(--gk-weight-medium, 600); color: var(--gk-text3, #999); padding: var(--gk-space-1, 4px) var(--gk-space-2, 8px); border-bottom: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); white-space: nowrap; }
.gk-table td { padding: var(--gk-space-1, 4px) var(--gk-space-2, 8px); border-bottom: var(--gk-border-width, 1px) solid color-mix(in srgb, var(--gk-border, rgba(0,0,0,.1)) 50%, transparent); vertical-align: top; }
.gk-table tr:last-child td { border-bottom: 0; }
/* A wide table must scroll in its own box; the page body must not.
   `min-width: 0` is the whole fix and it is not optional: this wrapper is
   a flex item (of .gk-card.gk-stack), and a flex item's default
   `min-width: auto` refuses to shrink below its content. Measured on
   beta 2026-08-14 at 390px: the runs table rendered 481px wide and pushed
   the document to scrollWidth 413 — `overflow-x: auto` alone had no
   effect at all, because the box it was scrolling was never constrained. */
.gk-table-scroll { overflow-x: auto; min-width: 0; max-width: 100%; }
/* Truncation inside a cell needs a width to truncate against; the cap is
   a system default expressed as a token, like --gk-grid-min. */
.gk-table td.gk-truncate { max-width: var(--gk-truncate-max, 320px); }

/* Empty state — the primitive most often skipped, which is why "nothing
   here" reads as "it is broken" on half the panes. */
.gk-empty { color: var(--gk-text2, #555); font-size: var(--gk-text-sm, 12px); padding: var(--gk-space-3, 12px) 0; }
.gk-empty--center { text-align: center; padding: var(--gk-space-6, 32px) var(--gk-space-3, 12px); }
.gk-empty i { display: block; font-size: var(--gk-text-xl, 18px); margin-bottom: var(--gk-space-2, 8px); opacity: .5; }
/* "Nothing here" and "it broke" are the same slot with a different tone. */
.gk-empty[data-tone] { color: color-mix(in srgb, var(--_tone) 62%, var(--gk-text, #1a202c)); }

/* ── The three states of an absence (ux-system-epic §D6) ─────────────────
   Measured 2026-08-14: 85 of gateka-core's 88 record-list loops rendered
   LITERALLY NOTHING on a zero-length sequence. A blank rectangle is not
   one message, it is three collapsed into silence, and the reader cannot
   tell them apart:

       empty    there is nothing — and here is what to do about it
       loading  it has not arrived yet
       denied   it exists and you may not see it

   They are modifiers, not new primitives: `.gk-empty` already carries the
   slot and `data-tone` already carries "it broke". A `.gk-loading` beside
   it would be a second definition of one notion (ADR-0185), and the whole
   point of the closed list is that the 46th card never arrives quietly.

   ⚠ They must never LOOK alike, and the distinction cannot rest on colour
   alone — 8% of men cannot use it, and `prefers-contrast` users may lose
   the tone entirely. So each state is separated on a second, non-colour
   channel: loading MOVES, denied carries a RULE, empty has neither. */

/* Loading. The ring is drawn from borders rather than a glyph, for the
   reason the disclosure marker is: FontAwesome is not loaded on every
   surface this sheet serves, and a missing-font icon is exactly the
   `.fa-globe::before` bug the ux-review brick exists to catch.

   The ring is DECORATION. The macro always renders the word as well —
   under `prefers-reduced-motion` the reset below freezes the animation,
   and a frozen spinner with no text is a broken page, not a loading one. */
.gk-empty--loading::before { content: ""; display: block;
  width: var(--gk-text-xl, 18px); height: var(--gk-text-xl, 18px);
  margin: 0 auto var(--gk-space-2, 8px);
  border: var(--gk-border-width, 1px) solid currentColor;
  border-top-color: transparent; border-radius: var(--gk-radius-full, 999px);
  opacity: .55; animation: gk-empty-spin 900ms linear infinite; }
@keyframes gk-empty-spin { to { transform: rotate(360deg); } }

/* ── The HOLD: no indicator under 200ms ─────────────────────────────────
   A wait shorter than the perception threshold makes the screen BLINK for
   nothing, and a blink is worse than silence: the reader sees something
   happen, learns nothing from it, and has no way to ask what it was. So
   the loading state hides ITSELF for --gk-loading-delay and shows only if
   the wait outlives the threshold. The DATA is never delayed — only the
   admission that it has not arrived.

   This is the piece that makes the state safe to put EVERYWHERE. Without
   it, converting the 26 mute regions of §D6 bis would trade one defect for
   another: a panel that flickers on every fast fetch is a panel whose
   operator learns to ignore the indicator.

   Three choices in one declaration, each load-bearing:

   * on `animation-delay`, NOT `animation-duration`. `_motion_reset.html`
     collapses `animation-duration` to `.01ms !important` under
     prefers-reduced-motion and does not touch delays (§D5 ter paid for
     that asymmetry on `.gk-stagger-item`). So the hold SURVIVES the reset
     while the ring above still stops turning: the information stays, the
     movement falls — which is what §C asks for, both clauses at once.
   * `backwards` fill, so the `from` keyframe applies DURING the delay.
     That is the whole mechanism: 200ms of `visibility: hidden`, then the
     1ms animation ends and the box reverts to its natural state.
   * the base state stays VISIBLE. Written the other way —
     `.gk-empty--loading { visibility: hidden }` plus a reveal — a browser
     that never runs the animation would hide every loading box in the parc,
     in silence. That is the `.gk-reveal` rule of §D5 ter, verbatim: a
     motion class must degrade towards SHOWN.

   `visibility` rather than `opacity` on purpose: it also keeps the box out
   of the accessibility tree during the hold, so a screen reader is not told
   "Chargement…" about a 40ms request either — the `role="status"` the macro
   emits would otherwise announce every flicker. And it reserves the box's
   space, so nothing reflows at the 200ms mark. */
.gk-empty--loading { animation: gk-empty-wait 1ms step-end
  var(--gk-loading-delay, 200ms) backwards; }
@keyframes gk-empty-wait { from { visibility: hidden; } }

/* §C is binding: honoured PARTOUT. `_motion_reset.html` already collapses
   every animation to .01ms globally, which stops the spin — this rule is
   here so the ring degrades to a legible static circle rather than a
   sliver frozen mid-turn, and so the sheet keeps the contract when served
   STANDALONE over the CDN to a site that never includes the reset. */
@media (prefers-reduced-motion: reduce) {
  .gk-empty--loading::before { animation: none; border-top-color: currentColor; opacity: .35; }
}

/* Denied. A refusal is not an emptiness, and the difference has to survive
   greyscale: the rule on the inline edge is the non-colour channel. The
   tone is warning, not danger — the user did nothing wrong, and painting a
   permission boundary in the error colour teaches them to ignore errors. */
.gk-empty--denied { --_tone: var(--gk-warning, #d97706);
  color: color-mix(in srgb, var(--_tone) 62%, var(--gk-text, #1a202c));
  border-inline-start: var(--gk-border-width-thick, 2px) solid var(--_tone);
  padding-inline-start: var(--gk-space-3, 12px);
  background: color-mix(in srgb, var(--_tone) 6%, transparent);
  border-radius: var(--gk-radius-sm, 6px); }

/* The call to action. "Aucune facture" is an observation; "Aucune facture —
   créez-en une" is a door, and the door is the entire point of §D6. The
   button is the vocabulary's own `.gk-btn`; this rule only gives it room. */
.gk-empty-action { margin-top: var(--gk-space-3, 12px); }
/* An empty state is the one primitive that is routinely TOGGLED — the
   loading box hides when the rows arrive. `hidden` is the UA's own
   display:none and it loses to any author `display` rule, which is how
   server-rendered hidden steps ended up painting OPEN under a
   `display: flex`. Nothing above sets display on .gk-empty today; this
   line is what keeps that true when someone adds a flex variant. */
.gk-empty[hidden] { display: none; }

/* Key/value rows — Stripe-style dense metadata. The label column width is
   a token, not a per-panel `min-width: 76px`. */
.gk-kv { display: flex; flex-direction: column; gap: var(--gk-space-05, 2px); font-size: var(--gk-text-xs, 11px); color: var(--gk-text2, #555); }
.gk-kv-k { display: inline-block; min-width: var(--gk-kv-label, 76px); font-weight: var(--gk-weight-medium, 600); color: var(--gk-text3, #999); }

/* Disclosure — <details>/<summary>. Keyboard-reachable natively; the
   summary just needs a visible affordance and the shared focus ring. */
.gk-details > .gk-summary { cursor: pointer; font-size: var(--gk-text-xs, 11px); color: var(--gk-text2, #555); font-weight: var(--gk-weight-medium, 600); user-select: none; border-radius: var(--gk-radius-sm, 6px);
  display: flex; align-items: center; gap: var(--gk-space-2, 8px); list-style: none; }
.gk-details > .gk-summary:hover { color: var(--gk-text, inherit); }
/* A summary that lays out a ROW — a title, a snippet, a date badge — has
   to be a flex container, and the browser deletes the disclosure marker
   the moment `display` stops being `list-item`. Measured on beta
   2026-08-14: nesting the row in a <span> to keep the native marker
   instead pushed the marker onto a line of its own, costing a row per
   card. So the vocabulary paints its own marker. Drawn from borders, not
   from a glyph: FontAwesome is not loaded on every surface this sheet
   serves, and a missing-font marker is the `.fa-globe::before` bug the
   ux-review brick exists to catch. */
.gk-details > .gk-summary::-webkit-details-marker { display: none; }
.gk-details > .gk-summary::before { content: ""; flex: none;
  width: 0; height: 0; border-left: 5px solid currentColor;
  border-top: var(--gk-space-1, 4px) solid transparent;
  border-bottom: var(--gk-space-1, 4px) solid transparent;
  transition: transform var(--gk-duration-fast, 120ms) var(--gk-ease, ease); }
.gk-details[open] > .gk-summary::before { transform: rotate(90deg); }
/* In a summary row the truncating element is the one that absorbs the
   slack — otherwise the trailing badge floats mid-row instead of sitting
   at the right edge, and the snippet never actually truncates. */
.gk-details > .gk-summary > .gk-truncate { flex: 1; min-width: 0; }

/* A plain list with the scale's indent instead of the browser's 40px.
   Added because rewriting _agents_panel.html hit it — which is the whole
   point of rewriting a panel before declaring the vocabulary done. */
.gk-list { margin: var(--gk-space-2, 8px) 0 0; padding-left: var(--gk-space-5, 24px); font-size: var(--gk-text-xs, 11px); }
.gk-list > li { margin-bottom: var(--gk-space-1, 4px); }
.gk-list > li:last-child { margin-bottom: 0; }

.gk-muted { color: var(--gk-text2, #555); }
/* Operator-authored free text (agent notes, error payloads) keeps its own
   line breaks. Same reason as .gk-list: found by the rewrite, not guessed. */
.gk-prewrap { white-space: pre-wrap; }
/* `overflow-wrap: anywhere` because the content is file paths and
   capability names with no spaces in them. Without it a
   `/home/ubuntu/gateka-core/gateka/bricks/…/reviewer.py` runs straight
   out through the right edge of its card — visible in the 390px capture,
   invisible to any amount of reading the stylesheet. */
.gk-code { font-family: var(--gk-mono, monospace); font-size: var(--gk-text-xs, 11px); background: var(--gk-bg3, rgba(0,0,0,.05)); border-radius: var(--gk-radius-sm, 6px); padding: 1px 5px; color: var(--gk-text, inherit); overflow-wrap: anywhere; }
.gk-code[data-tone] { color: color-mix(in srgb, var(--_tone) 62%, var(--gk-text, #1a202c)); }
.gk-code[data-copy] { cursor: copy; }

/* ── §D2 command palette ────────────────────────────────────────────────
   The second client of the vocabulary. It writes itself out of the tokens
   and adds nine names — a dialog, its box, the search field, the input,
   the list, an item, an item's label and hint, and a keyboard cap. Every
   other part of it (the empty state, the badges) is a primitive above.

   ⚠ `[hidden]` LOSES to an author display rule. `.gk-cmdk` is a flex
   container and `.gk-cmdk-item` is a flex row, so without the two
   `[hidden] { display: none }` rules below the palette renders OPEN over
   the page on every load, and a filtered-out entry stays visible. The
   attribute is the state the JS toggles — the rules are what make it
   mean anything. tests/unit/test_command_palette.py pins both.

   Motion: one 120ms fade-and-lift, under §C's 200ms ceiling, and the
   global prefers-reduced-motion reset at the foot of this file already
   flattens it to .01ms — which is why it is an animation and not a
   JS-driven transition nothing could have reset. */
.gk-cmdk { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: flex-start; justify-content: center;
  padding: var(--gk-space-7, 48px) var(--gk-space-3, 12px); background: var(--gk-scrim, rgba(0,0,0,.45)); }
.gk-cmdk[hidden] { display: none; }
.gk-cmdk-box { display: flex; flex-direction: column; width: 100%; max-width: var(--gk-cmdk-width, 560px); max-height: var(--gk-cmdk-height, 66vh);
  background: var(--gk-bg2, #fff); border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  border-radius: var(--gk-radius-lg, 14px); box-shadow: var(--gk-shadow, 0 4px 12px rgba(0,0,0,.10)); overflow: hidden;
  animation: gk-cmdk-in var(--gk-duration-fast, 120ms) var(--gk-ease-out, ease-out); }
@keyframes gk-cmdk-in { from { opacity: 0; transform: translateY(-4px); } }
.gk-cmdk-field { display: flex; align-items: center; gap: var(--gk-space-2, 8px); flex: none;
  padding: var(--gk-space-3, 12px) var(--gk-space-4, 16px); color: var(--gk-text3, #999);
  border-bottom: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); }
/* `font: inherit` and not a size of its own — the input is the box's own
   text, and a bare <input> otherwise falls back to the browser's 13.33px
   Arial and reads as a form field pasted into the dialog. */
.gk-cmdk-input { flex: 1; min-width: 0; border: 0; outline: none; background: none; font: inherit;
  font-size: var(--gk-text-md, 14px); color: var(--gk-text, inherit); }
.gk-cmdk-input::placeholder { color: var(--gk-text3, #999); }
/* `min-height: 0` for the same reason .gk-table-scroll needs `min-width:
   0`: this is a flex item, its default `min-height: auto` refuses to
   shrink below its content, and the list would then push the box past
   its max-height instead of scrolling inside it. */
.gk-cmdk-list { list-style: none; margin: 0; padding: var(--gk-space-1, 4px); overflow-y: auto; min-height: 0; }
.gk-cmdk-item { display: flex; align-items: center; gap: var(--gk-space-2, 8px); cursor: pointer;
  padding: var(--gk-space-2, 8px) var(--gk-space-3, 12px); border-radius: var(--gk-radius-sm, 6px);
  font-size: var(--gk-text-base, 13px); color: var(--gk-text, inherit); }
.gk-cmdk-item[hidden] { display: none; }
/* The highlight is `aria-selected`, not a class: the same attribute the
   listbox pattern already requires for screen readers, so there is one
   state to keep true instead of two that can disagree. */
.gk-cmdk-item[aria-selected="true"] { background: var(--surface-hover, rgba(0,0,0,.05)); }
.gk-cmdk-item > i { flex: none; width: var(--gk-space-4, 16px); text-align: center; color: var(--gk-accent, #3b82f6); }
.gk-cmdk-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gk-cmdk-hint { flex: none; color: var(--gk-text3, #999); font-size: var(--gk-text-xs, 11px); }
.gk-kbd { font-family: var(--gk-mono, monospace); font-size: var(--gk-text-xs, 11px); line-height: 1.6;
  color: var(--gk-text3, #999); padding: 0 var(--gk-space-1, 4px);
  border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); border-radius: var(--gk-radius-sm, 6px); }

/* ── §D2 bis — le menu ancré ────────────────────────────────────────────
   La palette au-dessus est un DIALOGUE : `position: fixed; inset: 0`, un
   scrim, une boîte centrée. Elle ne peut pas être ancrée à un bouton, et
   c'est pourquoi cinq familles de menu déroulant ont été écrites à côté
   d'elle sans jamais la croiser — mesuré le 2026-08-16 sur gateka/**.html
   + **.css : `.dx-menu`, `.fm-menu`, `.crm-stage-menu`, `.header-auth-menu`,
   `.gk-members-picker-dropdown`. Aucune dans la liste fermée.

   Consult architecte 2026-08-16-la-barre-de-saisie-du-messenger-2e71bf →
   `extend_existing`. Radix DropdownMenu reste rejeté comme DÉPENDANCE
   (React-first, 2026-08-14 …-571a79) et repris comme SPÉCIFICATION : les
   noms, l'état en attribut ARIA, le contrat clavier.

   ⚠ Ce lot ne convertit aucune des cinq familles ci-dessus. `.gk-menu`
   arrive donc avec deux clients écrits par le même auteur (les deux menus
   du composer de `chat.html`). C'est la dette, nommée : le prochain lot qui
   touche `_header_auth_menu.html` ou `_crm_pane.html` CONVERTIT, il n'ajoute
   pas.

   L'état est `aria-expanded` sur le déclencheur et `aria-checked` sur la
   ligne choisie — jamais `.is-active`. Même discipline que `.gk-chip` et
   `.gk-tab` : l'attribut lu par un lecteur d'écran et la peinture ne
   peuvent alors pas se contredire.

   Le sous-titre (`.gk-menu-desc`) n'est pas une décoration. « Créer » et
   « Planifier » côte à côte sont deux mots ; « Créer — apporter des
   modifications directement » est une phrase qui dit ce que la ligne FAIT.
   C'est la même règle que `.gk-empty-action` porte pour une absence.

   L'ANCRE appartient au consommateur. Ce bloc pose `position: absolute` et
   s'arrête là : au-dessus d'une barre de saisie le menu s'ouvre vers le
   haut, sous un avatar d'en-tête vers le bas, et une modificatrice par côté
   serait une décision que le système n'a pas à prendre. Le consommateur
   fournit l'ancêtre positionné et le décalage.

   L'animation est une `animation` de 120 ms et jamais une transition
   pilotée en JS : `_motion_reset.html` est l'adresse unique du contrat
   `prefers-reduced-motion` depuis §D5, et il ne peut aplatir que ce qui
   est déclaratif. Même idiome que `gk-cmdk-in` juste au-dessus. */
.gk-menu { position: absolute; z-index: var(--gk-menu-z, 60);
  display: flex; flex-direction: column;
  min-width: var(--gk-menu-width, 240px);
  max-width: min(var(--gk-menu-max, 320px), calc(100vw - var(--gk-space-5, 24px)));
  max-height: var(--gk-menu-height, 60vh); overflow-y: auto;
  padding: var(--gk-space-1, 4px);
  background: var(--gk-bg2, #fff);
  border: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  border-radius: var(--gk-radius, 10px);
  box-shadow: var(--gk-shadow, 0 4px 12px rgba(0,0,0,.10));
  animation: gk-menu-in var(--gk-duration-fast, 120ms) var(--gk-ease-out, ease-out); }
.gk-menu[hidden] { display: none; }
@keyframes gk-menu-in { from { opacity: 0; transform: translateY(4px); } }

/* Le filet entre deux blocs est porté par le BLOC SUIVANT, pas par un
   `.gk-separator` que l'auteur doit penser à intercaler. Un séparateur posé
   à la main se perd au premier réordonnancement, et il en faut exactement
   un de moins que de blocs — la seule règle qu'un auteur ne peut pas rater
   est celle qu'il n'écrit pas. */
.gk-menu-group { display: flex; flex-direction: column; }
.gk-menu-group[hidden] { display: none; }
.gk-menu-group + .gk-menu-group {
  margin-top: var(--gk-space-1, 4px); padding-top: var(--gk-space-1, 4px);
  border-top: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1)); }

.gk-menu-item { display: flex; align-items: center; gap: var(--gk-space-3, 12px);
  width: 100%; box-sizing: border-box; text-align: left;
  background: none; border: 0; cursor: pointer; font: inherit;
  font-size: var(--gk-text-base, 13px); color: var(--gk-text, inherit);
  padding: var(--gk-space-2, 8px) var(--gk-space-3, 12px);
  border-radius: var(--gk-radius-sm, 6px); touch-action: manipulation; }
.gk-menu-item[hidden] { display: none; }
.gk-menu-item:hover:not(:disabled),
.gk-menu-item[aria-selected="true"] { background: var(--surface-hover, rgba(0,0,0,.05)); }
.gk-menu-item:disabled, .gk-menu-item[data-disabled] { opacity: .5; cursor: not-allowed; }
/* La colonne d'icônes. Une largeur fixe et non un `margin-right` : sans
   elle les libellés se décalent d'une ligne à l'autre selon la largeur du
   glyphe, ce qui est exactement ce que « alignée sur une colonne » exclut.
   Même rule que `.gk-cmdk-item > i`. */
.gk-menu-item > i { flex: none; width: var(--gk-space-4, 16px); text-align: center;
  color: var(--gk-text2, #555); }
/* La coche de la ligne choisie, dessinée depuis des bordures et non depuis
   un glyphe : FontAwesome n'est pas chargé sur toutes les surfaces que
   cette feuille sert, et une coche en police manquante est le bug
   `.fa-globe::before` que la brique ux-review existe pour attraper. Même
   raison que le marqueur de `.gk-summary`. */
.gk-menu-item[aria-checked="true"]::after { content: ""; flex: none;
  width: var(--gk-space-1, 4px); height: var(--gk-space-2, 8px);
  margin-bottom: var(--gk-space-05, 2px);
  border: solid var(--gk-accent, #3b82f6); border-width: 0 2px 2px 0;
  transform: rotate(45deg); }

/* Le libellé EST la colonne de texte : le nom, puis la phrase qui dit ce
   que la ligne fait. `flex: 1` pour qu'il absorbe le mou — sinon la coche
   flotte au milieu de la ligne au lieu de tenir le bord droit. */
.gk-menu-label { flex: 1; min-width: 0; display: flex; flex-direction: column;
  gap: var(--gk-space-05, 2px); font-weight: var(--gk-weight-medium, 600); }
.gk-menu-label[hidden] { display: none; }
.gk-menu-desc { color: var(--gk-text2, #555); font-size: var(--gk-text-xs, 11px);
  font-weight: 400; line-height: 1.4; }

/* Le pied du menu — le raccourci clavier annoncé. Il n'est pas une entrée :
   rien à y cliquer, et le mettre dans un `.gk-menu-group` le rendrait
   survolable comme une action qui n'existe pas. */
.gk-menu-foot { display: flex; align-items: center; gap: var(--gk-space-2, 8px);
  margin-top: var(--gk-space-1, 4px); padding: var(--gk-space-2, 8px) var(--gk-space-3, 12px);
  border-top: var(--gk-border-width, 1px) solid var(--gk-border, rgba(0,0,0,.1));
  color: var(--gk-text3, #999); font-size: var(--gk-text-xs, 11px); }
.gk-menu-foot[hidden] { display: none; }

/* ── Motion (ux-system-epic §C, architect consult …-eb82be step 2) ───────
   Owner, 2026-08-15: « i want pages with nice animation », then « we will
   deploy that on all sites, so you need a universal approach ». Universal,
   here, means Tier 0: three names in THIS closed list, zero bytes of JS,
   shipping to every product that links this sheet. GSAP stays where
   ADR-0105 §4 put it — opt-in, per marketing component, never on an
   operational surface.

   They are in the §D1 list and not in a `gk-motion.css` on purpose. A
   motion class is a UI vocabulary class; a second closed list with its own
   test is a second source for one notion, which is mechanically a future
   drift and exactly what §D1 exists to prevent. And the selectors are HERE
   rather than in `_theme.html`, which declares custom properties only —
   which is precisely why adding to it can never repaint anything. The
   tokens these read (--gk-duration, --gk-ease, --gk-stagger-step) are
   already there and stay there.

   ⚠ THE TRAP, paid once on 2026-08-15 ("the animation is not smooth").
   A CSS transition needs a STARTING image. An element going from « not
   rendered » to « rendered » has none, so the transition never runs — it
   JUMPS. `<details>` does exactly that to its non-`<summary>` children
   (`content-visibility: hidden` when closed). So none of the three below
   transitions a visibility: `gk-enter` is an ANIMATION (which plays on
   first render and needs no previous frame), and `gk-stagger` / `gk-reveal`
   move opacity and transform on an element that stays rendered throughout.
   A future addition here must keep that property or inherit the bug.

   Budget (§C): 200ms binds an INTERACTION CLASS, not a surface class —
   anything answering a gesture (click, hover, focus, toggle), a marketing
   page's nav button included. It does not bind ambient scroll choreography
   on a storyteller surface. All three read the duration tokens, which are
   capped by test_motion_contract.py, so the ceiling is enforced by a token
   rather than by review. */

/* gk-enter — one arrival, one shot. `both` fills the from-state backwards,
   so the element is already at its start values before the first frame. */
@keyframes gk-enter {
  from { opacity: 0; transform: translateY(var(--gk-space-2, 8px)); }
  to   { opacity: 1; transform: none; }
}
.gk-enter { animation: gk-enter var(--gk-duration, 180ms) var(--gk-ease-out, ease-out) both; }

/* gk-stagger — a container whose ranked children arrive one after another.
   The rank is `--i`, written by the template on each child; the child opts
   in by carrying `.gk-stagger-item`, so a container may hold furniture that
   is not part of the sequence.

   ⚠ The ramp is live only while the container is IN. A stagger on the way
   OUT is a delay before something disappears — waiting, not information —
   and the language pill's `[open]` rule had that property before the fold.
   « In » has two spellings and no more: `[data-gk-in]` on the container for
   anything a server or a script declares, and an `[open]` parent for
   `<details>`, which owns its state natively and cannot mirror it without
   JS. A third would be a third source.

   ⚠ AND THE BOUND. `rank × step` has no ceiling: its cost is the LAST
   child's, so it grows with a child count this sheet does not control. The
   pill shipped `calc(30ms + var(--i) * 50ms)` — 250ms at three languages,
   440ms at six, and no number in the rule above 200. `min()` caps it at two
   steps, so the worst case is 60 + 120 = 180ms whatever the child count.
   That is `test_a_rank_driven_stagger_is_bounded`, and it reads this file.

   ⚠ AND IT HAS TO COMPOSE WITH `gk-enter`, which it did not until
   2026-08-15 (consult `…-67a2ab`). This rule declared `transition-delay`
   alone, so it presupposed a child that already carries a transition and a
   from-state — true of `.gk-langpill-seg`, which `gk-nav.css` paints, and
   FALSE of any list a panel renders. The vocabulary's own arrival is an
   ANIMATION, and `transition-delay` does not offset an animation: written
   `class="gk-enter gk-stagger-item"`, every child arrived at once. Nothing
   went red — a stagger that does not stagger looks exactly like a list.
   So the ramp is declared on BOTH axes: the child that transitions reads
   the first, the child that animates reads the second, and an
   `animation-delay` on an element with no animation is inert.

   ⚠ AND THE DURATION IS PART OF THE BOUND, not a third rule. `.gk-enter`
   reads `--gk-duration` (180ms); offset by two steps the last child lands
   at 240ms, past §C's ceiling — and no test could see it, because the
   delay and the duration live in two declarations and `_offenders` reads
   one at a time. `test_the_stagger_step_is_declared_and_is_a_step_not_a_duration`
   already asserts `2 × step + --gk-duration-fast ≤ 200`; before this line
   it asserted a property of a configuration nothing produced. Now a
   staggered arrival IS the fast one, 60 + 120 = 180ms, and
   `test_a_staggered_arrival_is_bounded_across_both_declarations` measures
   the composition rather than its halves. */
[open] > .gk-stagger > .gk-stagger-item,
.gk-stagger[data-gk-in] > .gk-stagger-item {
  transition-delay: min(calc(var(--i, 0) * var(--gk-stagger-step, 30ms)),
                        calc(2 * var(--gk-stagger-step, 30ms)));
  animation-delay: min(calc(var(--i, 0) * var(--gk-stagger-step, 30ms)),
                       calc(2 * var(--gk-stagger-step, 30ms)));
  animation-duration: var(--gk-duration-fast, 120ms);
}

/* gk-reveal — scroll choreography, driven by an IntersectionObserver.

   ⚠ CSS-ONLY MEANS VISIBLE. The hidden start state is armed by the
   observer (`data-gk-reveal` on an ancestor, `data-gk-seen` on the element
   as it enters), never by this sheet on its own. Written the other way
   round — `.gk-reveal { opacity: 0 }` unconditionally — a page whose script
   fails, is blocked, or was never wired renders BLANK, and it renders blank
   silently. No observer ships in the chrome today; that is exactly why this
   has to degrade to « shown ». */
.gk-reveal {
  transition: opacity var(--gk-duration, 180ms) var(--gk-ease-out, ease-out),
              transform var(--gk-duration, 180ms) var(--gk-ease-out, ease-out);
}
[data-gk-reveal] .gk-reveal { opacity: 0; transform: translateY(var(--gk-space-4, 16px)); }
[data-gk-reveal] .gk-reveal[data-gk-seen] { opacity: 1; transform: none; }

/* The universal reset in `_motion_reset.html` zeroes durations, not DELAYS
   — a staggered child would still wait 60ms before appearing instantly. So
   the delay is zeroed here, where the delay is declared. And the reveal
   SHOWS: a reduced-motion reader who never scrolls an element into view
   must still get the content, which a never-armed observer would withhold.

   ⚠ No `*` selector in this block. The UNIVERSAL reset has exactly one home
   (`test_universal_reset_has_exactly_one_definition`); this is a
   per-component block, which that test allows and deliberately so.

   ⚠ BOTH delays, or the half that was added last survives the reset. The
   ramp is declared on `transition-delay` AND `animation-delay` above; a
   reduced-motion reader zeroed on one of the two waits 60ms in front of an
   element that then appears instantly — the exact defect this block exists
   to prevent, reintroduced by the line that completed the ramp. */
@media (prefers-reduced-motion: reduce) {
  [open] > .gk-stagger > .gk-stagger-item,
  .gk-stagger[data-gk-in] > .gk-stagger-item {
    transition-delay: 0ms !important;
    animation-delay: 0ms !important;
  }
  [data-gk-reveal] .gk-reveal { opacity: 1; transform: none; }
}

/* gk-ribbon — L'ORNEMENT INFINI. Le cinquième nom, et le premier qui
   ne s'arrête pas. Consult 2026-08-16 …-le-bandeau-fournisseurs-45bc25,
   verdict extend_existing, après trois tours sur le bandeau fournisseurs
   de lorvert (« still shit », owner).

   ⚠ CE N'EST PAS UN CARROUSEL, ET LE CRITÈRE N'EST PAS RHÉTORIQUE.
   ADR-0190 §3 n'interdit pas le mouvement automatique : il interdit
   L'ÉCHANGE — l'instant choisi par une minuterie où l'on retire la carte
   au lecteur. Le plaidoyer « un bandeau fournisseurs n'est pas une
   surface de lecture » est un plaidoyer de cas spécial, et l'architecte
   l'a refusé comme inutile. Trois propriétés tranchent, et elles se
   testent :
     (a) transform CONTINUE — aucune trame où l'élément k est échangé ;
     (b) rien d'inatteignable animation coupée : chaque bannière reste
         dans le DOM et reste jointe au clavier par la piste scroll-snap ;
     (c) arrêt au `:hover` ET au `:focus-within`.
   Un ruban qui manque une seule des trois EST un carrousel à minuterie,
   et §3 le refuse. C'est ADR-0190 §3bis.

   ⚠ LE PLAFOND DE 200 ms NE MORD PAS ICI, et ce fichier le disait déjà :
   « 200ms binds an INTERACTION CLASS, not a surface class ». Un ornement
   infini n'annonce aucun changement d'état, donc il ne promet rien qu'une
   durée puisse trahir. D'où un jeton À LUI, `--gk-ribbon-duration` —
   JAMAIS `--gk-duration`. Le plafond est tenu par jeton et non par revue :
   replier 40 s dans l'ensemble plafonné garantit qu'on le ramènera un jour
   à 180 ms, et le ruban mourra sans que personne comprenne pourquoi.

   ⚠ ET LA FORME EST CONTRAINTE PAR LE RESET, pas par le goût.
   `_motion_reset.html` impose `animation-iteration-count: 1 !important` et
   `animation-duration: .01ms`. Un ruban infini ne s'arrête donc pas en
   mouvement réduit : il SAUTE à son état final. Ce n'est inoffensif que si
   la fin est visuellement IDENTIQUE au début — vrai pour exactement une
   forme : piste = liste + copie exacte, largeur 200 %, translateX(0 →
   -50 %). Tout autre képhrame (-100 %, pas par élément) dépose le lecteur
   réduit sur une bande décalée ou vide. La duplication n'est donc pas une
   commodité de rendu, c'est la condition d'accessibilité. */
.gk-ribbon { overflow: hidden; }
.gk-ribbon-track {
  display: flex;
  width: max-content;
  animation: gk-ribbon var(--gk-ribbon-duration, 40s) linear infinite;
}
/* `display:flex` ci-dessus est (0,1,0) et bat la regle UA `[hidden]`
   (0,0,1) : sans cette ligne, une piste que le serveur marque `hidden`
   defile quand meme, et rien n'erre. Le fichier l'a paye trois fois
   (.gk-cmdk, .gk-btn, .gk-badge) — la regle est donc portee par le
   primitif, pas par la revue. */
.gk-ribbon-track[hidden] { display: none; }
.gk-ribbon:hover > .gk-ribbon-track,
.gk-ribbon:focus-within > .gk-ribbon-track { animation-play-state: paused; }
@keyframes gk-ribbon { to { transform: translateX(-50%); } }

/* ── Editor (Obsidian-style) ── */
.gk-editor-sections { padding: 8px 12px; }
.gk-editor-section { border-left: 3px solid transparent; transition: border-color var(--gk-duration, 180ms); }
.gk-editor-section:hover:not(.gk-editor-section--editing) { border-left-color: var(--gk-accent, #3b82f6); opacity: .85; cursor: pointer; }
.gk-editor-section--editing { border-left-color: var(--gk-accent, #3b82f6); background: var(--gk-bg3, rgba(0,0,0,.04)); border-radius: 0 6px 6px 0; overflow: hidden; }
.gk-editor-section--editing .cm-editor { background: transparent; min-height: auto; }
.gk-editor-section--editing .cm-editor .cm-scroller { min-height: auto; }

/* Preview rendering */
.gk-editor-preview { padding: 4px 12px; font-size: 14px; line-height: 1.6; color: var(--gk-text, #333); }
.gk-editor-preview h1 { font-size: 20px; font-weight: 800; margin: 16px 0 8px; }
.gk-editor-preview h2 { font-size: 16px; font-weight: 700; margin: 14px 0 6px; border-bottom: 1px solid var(--gk-border, rgba(0,0,0,.08)); padding-bottom: 4px; }
.gk-editor-preview h3 { font-size: 14px; font-weight: 600; margin: 12px 0 4px; }
.gk-editor-preview p { margin: 6px 0; }
.gk-editor-preview ul, .gk-editor-preview ol { margin: 6px 0; padding-left: 24px; }
.gk-editor-preview li { margin: 3px 0; }
.gk-editor-preview a { color: var(--gk-accent, #3b82f6); text-decoration: none; }
.gk-editor-preview a:hover { text-decoration: underline; }
.gk-editor-preview code { background: var(--gk-bg3, rgba(0,0,0,.05)); color: var(--gk-accent, #3b82f6); padding: 1px 5px; border-radius: 3px; font-size: 0.9em; font-family: var(--gk-mono, monospace); }
.gk-editor-preview pre { background: var(--gk-bg, #fafafa); border: 1px solid var(--gk-border, rgba(0,0,0,.1)); border-radius: 8px; padding: 12px 16px; overflow-x: auto; margin: 8px 0; }
.gk-editor-preview pre code { background: none; color: var(--gk-text, #333); padding: 0; font-size: 13px; }
.gk-editor-preview blockquote { border-left: 3px solid var(--gk-accent, #3b82f6); padding-left: 12px; margin: 8px 0; color: var(--gk-text2, #666); font-style: italic; }
.gk-editor-preview table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 13px; }
.gk-editor-preview th { background: var(--gk-bg3, rgba(0,0,0,.04)); font-weight: 600; text-align: left; padding: 6px 10px; border-bottom: 2px solid var(--gk-border, rgba(0,0,0,.1)); }
.gk-editor-preview td { padding: 5px 10px; border-bottom: 1px solid var(--gk-border, rgba(0,0,0,.1)); }
.gk-editor-preview strong { font-weight: 700; }
.gk-editor-preview em { font-style: italic; color: var(--gk-text2, #666); }
.gk-editor-preview hr { border: none; border-top: 1px solid var(--gk-border, rgba(0,0,0,.1)); margin: 16px 0; }
.gk-editor-preview img { max-width: 100%; border-radius: 6px; }

/* ── Reduced motion ──────────────────────────────────────────────────
   MOVED to templates/_motion_reset.html by §D5. It was here, and this file
   is not loaded by every surface that animates: chat.html, chat_invite.html,
   mentions-legales.html and _invoice_print.html are standalone documents
   with no <link> to it, so `prefers-reduced-motion` was silently unhonoured
   on all four — chat.html runs a 1s infinite pulse AND scroll-behavior:
   smooth with no escape.

   A stylesheet cannot reach a document that does not link it, so the
   contract had to become a Jinja include. `_theme.html` includes it, and so
   do those four directly. Do NOT restore a copy here: one notion, one
   address, or the two diverge the first time someone tunes `.01ms`. */
