/* ikc-base.css — the design system's CSS layer, for EVERY shell that hosts a
   graph-ui bundle.
 *
 * EXTRACTED 2026-08-16, because it had been in index.html only. studio.html
 * loads the same renderer and had none of it: no token defaults, no typeface,
 * no state classes, no focus ring, no reduced-motion honour. So the one page
 * built to show the widget vocabulary was showing it without the design system,
 * and nothing said so — the same shape as a token nothing reads, one layer out.
 * A rule that lives in one of two shells is a rule half the product does not
 * have. `devbox run validate-dsl` now fails a shell that omits this file.
 *
 * The values here are DEFAULTS ONLY — the first paint, before
 * graph-ui.web.theme writes the active theme's tokens onto the root. The source
 * of truth is dsl/instances/themes.edn (ADR-graph-ui-v0-009-01); these mirror
 * its Amber theme so there is no flash of an unthemed page. */

:root {
  --ground:#f7db83; --panel:#f9e7a6; --raised:#fdf5d8; --card:#fdf6dd;
  --field:#fffbe9; --gutter:#efe1a8; --hover:#f3e6b0; --border:#e6c96a; --border-2:#eed893;
  --divider:#887334;
  --text:#34495d; --heading:#2b3d50; --muted:#6f7c86; --faint:#a08f5f;
  --accent:#2f9e80; --accent-2:#7fd0b8; --accent-soft:#cdebe0; --sel:#6fc9ad; --sel-text:#17403a;
  --good:#3f9e6f; --warn:#c0851f; --crit:#cf5a5a;
}

body { background: var(--ground); color: var(--text); }

/* EVERY :divider WIDGET IS PAINTED WITH THE TOKEN --divider (fault report
   710c30ff). Mantine paints a Divider with its own grey, which no theme
   declares. --divider gives 3:1 or more against each surface of each theme
   (WCAG 1.4.11, T-board-037), so a declaration names no colour. A :color
   prop on one :divider still wins, because Mantine writes it inline. The
   doubled class outweighs Mantine's own rule for the light and the dark
   colour scheme, whatever the load order of the two files. */
.mantine-Divider-root.mantine-Divider-root { --divider-color: var(--divider); }

/* THE TYPEFACE IS A TOKEN (2026-08-15). Set once on the root from the theme's
   own --font-ui / --font-mono, so changing the face in themes.edn — or in a
   DTCG file exported from Penpot — changes every island at once. Before this
   the face was a Tailwind default nobody had written down: a theme could
   restyle everything about the type except which type it was. */
:root { font-family: var(--font-ui); }

/* AND SO IS THE SIZE (2026-08-19). The face was made a token above and the size
   was left behind, so `--type-body-size` was declared, themed, and overridden
   everywhere by Tailwind's own `text-sm` — 14px against the theme's 15px. A
   theme could restate its body type and nothing moved.
   On BODY and not on :root, because `--type-body-size` is in rem and rem means
   the ROOT size. The fallback is amber's own value, for the moment before
   theme/apply-theme! writes the measured token set. */
/* `html body`, not `body`, and the extra element is load-bearing. Tailwind runs
   from a CDN script and injects its preflight at RUNTIME, so it lands after every
   <link> whatever the order in the shell. Its preflight sets
   `body { line-height: inherit }` over `html { line-height: 1.5 }`, which beat a
   plain `body` rule here and held the type at 1.5 against the token's 1.55.
   One extra element takes the specificity to 0,0,2 and no order can lose it. */
html body { font-size: var(--type-body-size, 0.9375rem);
            line-height: var(--type-body-line-height, 1.55); }
code, pre, .font-mono { font-family: var(--font-mono); }

/* STATE, MOTION AND FOCUS AS TOKENS (2026-08-15). These were Tailwind
   utilities — opacity-40, transition-colors, outline-2 — which is to say
   decisions nobody could find and no theme could restate. A theme that wants
   a calmer interface, a more visible focus ring, or a stronger disabled state
   now says so in themes.edn and every island follows. */
.ikc-dim      { opacity: var(--opacity-dim); }
.ikc-disabled { opacity: var(--opacity-disabled); }
/* Added 2026-08-16 with its readers. The class existed for two of the three
   steps and the third had no way to be asked for, so `opacity-75` stayed a
   Tailwind literal in core.cljs and --opacity-strong stayed a name nothing
   read — the same crack as :elevation-floating, one scale over. */
.ikc-strong   { opacity: var(--opacity-strong); }
/* ONE hover emphasis, replacing hover:opacity-90 on a button and
   hover:opacity-80 on a link — the same decision made twice, differing by a
   step nobody chose. */
.ikc-hover:hover { opacity: var(--opacity-hover); }

:where(button, a, [role="button"], input, textarea, select, summary) {
  transition: color var(--motion-fast) var(--motion-ease),
              background-color var(--motion-fast) var(--motion-ease),
              border-color var(--motion-fast) var(--motion-ease),
              opacity var(--motion-fast) var(--motion-ease);
}
:where(button, a, [role="button"], input, textarea, select, summary):focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* A reader who asks for less motion gets none, which is what the token being
   a token makes expressible at all. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important;
                           animation-duration: 0.01ms !important; }
}

/* WAITING, MADE VISIBLE (2026-08-17). George: "some UI indication that click is
   accepted and will take time to be accomplished". graph-ui.web.busy decides
   WHETHER to show these; this file decides what they look like, because that is
   the design decision and it belongs with the other ones.

   THIS IS WHAT --motion-slow WAS WAITING FOR. It was the single token in
   themes.edn that nothing read, and its own note said why it was declared
   anyway: "Nothing in the running UI animates over 300ms — panels and menus
   appear rather than slide — so this waits for the first thing that travels.
   … Consume or cut it when the canvas gains motion." A progress sweep is the
   first thing on this canvas that travels. */
@keyframes ikc-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(300%); }
}

/* The bar itself is a full-width track; the moving part is the ::after. An
   indeterminate bar is the honest form here — the cascade's length is not known
   in advance (it is one dispatch plus one :ui/read per affected source), so a
   percentage would be invented. */
.ikc-busy-bar {
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: var(--focus-width);       /* the thinnest line the system already names */
  overflow: hidden; pointer-events: none;
}
.ikc-busy-bar::after {
  content: ""; position: absolute; inset-block: 0;
  width: 33%;
  background: var(--accent);
  animation: ikc-sweep var(--motion-slow) var(--motion-ease) infinite;
}
.ikc-busy-bar[data-phase="slow"]::after { background: var(--warn); }

/* Reduced motion needs its own rule, not just the blanket one above: an
   `infinite` animation with duration forced to 0.01ms does not stop, it completes
   thousands of times a second, which is a strobe — the opposite of what the
   reader asked for. So the sweep is switched OFF and the track fills instead. The
   indication survives; only the travel goes. */
@media (prefers-reduced-motion: reduce) {
  .ikc-busy-bar::after { animation: none !important; width: 100%;
                         opacity: var(--opacity-strong); }
}

/* A FIXED SLOT IN THE CHROME (2026-08-17). George: "it should live in a fixed
   space, the realignment of the elements after it is disturbing and breaks 'Do
   not make me think' principle." The readout was an ordinary inline flex child,
   so the session dropdown moved every time it appeared. The slot is now rendered
   in every phase and only its contents change — nothing outside it can move.

   Width in ch, not px: an island's sizes belong to the reader
   (ADR-graph-ui-v0-019-01), and the one intrinsic px here is the icon scale, which
   is a token. `--icon-sm` for the ring plus 4.5ch for at most "120s" or "×12".

   tabular-nums is not a flourish. Proportional digits change width as the seconds
   tick, so "9s" -> "10s" -> "11s" would jitter inside the very slot that exists to
   stop things moving. */
.ikc-busy-slot {
  display: inline-flex; align-items: center; gap: 0.35ch;
  flex: 0 0 auto;
  width: calc(var(--icon-sm) + 4.5ch);
  overflow: hidden; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The hourglass: a ring that turns, sized from the icon scale. */
@keyframes ikc-spin { to { transform: rotate(360deg); } }
.ikc-busy-spinner {
  display: inline-block; flex-shrink: 0;
  width: var(--icon-sm); height: var(--icon-sm);
  border: var(--focus-width) solid var(--border-2);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: ikc-spin var(--motion-slow) linear infinite;
}
/* Slow says itself in colour on BOTH surfaces at once, so the words did not have
   to stay in the chrome to keep the meaning. */
.ikc-busy-spinner[data-phase="slow"] { border-top-color: var(--warn); }

/* THE ROW'S HOURGLASS OVERLAYS, IT DOES NOT DISPLACE. Same principle as the slot
   above, one level down and solved the other way round: reserving a gutter on
   every row would cost it on all 2029 of them, so the ring is taken out of flow
   at the row's trailing edge instead. A row WITH an accessory (a page count, a
   reference's text) would otherwise have had it shoved left mid-click.

   The transform lives on this wrapper and never on the ring: `.ikc-busy-spinner`
   animates `transform: rotate()`, so a centring `translateY(-50%)` on the same
   element would be overwritten by the first keyframe and the ring would sit high.
   Stretching top/bottom and centring with flex needs no transform at all. */
.ikc-busy-row {
  position: absolute; top: 0; bottom: 0; right: 0.25ch;
  display: flex; align-items: center;
  pointer-events: none;              /* never intercept the click it reports on */
}
/* ═══════════════════════════════════════════════════════════════════════════
   X-RAY — F-graph-ui-v0-010-01. THE DRAWING IS NOT HERE ANY MORE.
 *
 * It was, twice, and neither version worked. Rings and name plates were drawn on
 * each node's own wrapper (`position: relative` + a `::before` carrying
 * `attr(data-xray-name)`), and George's verdict on the result was "the element is
 * gone, scrambled, overlaped".
 *
 * WHY CSS COULD NOT DO IT. A stylesheet places each plate knowing only that
 * plate's own element — not how deep it sits, and not whether a sibling already
 * claimed the pixel. A parent and its first child share a top edge constantly in
 * flex-column layouts, so their plates overprinted into unreadable garbage. That
 * was guaranteed by the MEDIUM, not by a bad offset, which is why nudging the
 * plate from straddling the edge to sitting above it only moved the collision.
 * A second, quieter cost: `position: relative` on 68 wrappers re-parents the
 * containing block of anything absolutely positioned inside them.
 *
 * It now draws in ONE fixed overlay (graph-ui.web.xray/overlay) from rects
 * measured each frame, where "does this label overlap one already placed?" is
 * decidable — and is decided, by a pure, unit-tested function. Nothing is added
 * to any inspected element, so there is no rule to write here.
 *
 * `data-xray` on the render root and `data-xray-name`/`data-xray-detail` on each
 * wrapper are still stamped by render-node — they are how the overlay FINDS and
 * NAMES things, and are deliberately styleless.
 * ═══════════════════════════════════════════════════════════════════════════ */


@media (prefers-reduced-motion: reduce) {
  /* Same reasoning as the bar. A still ring, dimmed, still reads as "pending". */
  .ikc-busy-spinner { animation: none !important; opacity: var(--opacity-dim); }
  /* And MANTINE's loader, for the same reason — the daily-note strip uses Button
     `loading` rather than a section of its own, so this is the one place the
     blanket 0.01ms rule above would strobe a component we did not write. */
  .mantine-Loader-root, .mantine-Button-loader,
  .mantine-Loader-root *, .mantine-Button-loader * {
    animation: none !important;
    opacity: var(--opacity-dim);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE PHONE — PLAN-ikc-v3-015-01 phase 6.
 *
 * Two rules, both of them here rather than in a widget, because both are true
 * of EVERY control and every page: what a widget tree declares per node is
 * :narrow (dsl/contexts/ui.edn v2.0-SNAPSHOT), and that says which nodes fold,
 * hide or scroll — a different question from these two.
 *
 * 1. NOTHING SCROLLS THE PAGE SIDEWAYS. A trapped horizontal scrollbar is the
 *    failure dev/layout_check.mjs already fails a build for on the desktop, and
 *    on a phone it also steals the swipe. The island decides what scrolls
 *    INSIDE itself (:scroll-area, :narrow :scroll); the document never does.
 *
 * 2. A FINGER IS 44px. --touch-target is the declared minimum
 *    (dsl/instances/themes.edn), and `pointer: coarse` is the browser telling
 *    us there is a finger rather than a mouse — so the rule applies to a phone
 *    and a tablet and leaves every desktop render byte-identical.
 *
 *    It is written once, for the ELEMENTS, and not per component. Twenty
 *    components draw a control between them; a list naming which ones to make
 *    bigger is only as complete as the last person who remembered it, and the
 *    smell of that mistake is a correct app with one un-tappable button.
 *
 *    `min-height`, never `height`: a control that is already taller keeps its
 *    height, and a text label still wraps inside it.
 * ═══════════════════════════════════════════════════════════════════════════ */

@media (pointer: coarse) {
  /* Rule 1. Scoped to a finger deliberately. On a DESKTOP, hiding the page's
     horizontal overflow would make a too-wide island unreachable instead of
     scrollable — Studio needs about 1450px and a 1200px window would simply
     lose the right of it, with nothing saying so. A phone has the opposite
     problem: the swipe is the only navigation it has. */
  html, body { max-width: 100%; overflow-x: hidden; }

  button,
  [role="button"],
  summary,
  select,
  input[type="checkbox"],
  input[type="radio"] {
    min-height: var(--touch-target);
  }

  /* An icon-only control must be as WIDE as it is tall, or it is a 44px-tall
     sliver 28px across. Text buttons are excluded: their width is their label,
     and forcing a minimum on those spreads a toolbar apart. */
  button:empty,
  .mantine-ActionIcon-root {
    min-width: var(--touch-target);
  }

  /* MANTINE SETS ITS OWN MIN-HEIGHT ON TWO OF THEM (lane AL-P, row C-04,
     measured at 390 x 844 on a probe bus, 2026-10-21). An ActionIcon takes
     `min-height: var(--ai-size)`, 28 px for "sm", 18 px for "xs". A native
     select takes `min-height: var(--input-height)`, 36 px. A rule on the
     element name has less weight than the class of the library, so the
     generic rule above lost to both. !important, as the reading controls
     below, and for the same reason: the component writes the size itself. */
  .mantine-ActionIcon-root,
  .mantine-NativeSelect-input {
    min-height: var(--touch-target) !important;
  }

  /* THE CROSS OF A TAG CHIP (row C-04). It is a 22 px button inside a chip
     that is 22 px high. The cross is the finger target and the chip is not,
     so the button takes the full size and a negative margin gives the extra
     back to the row: the chip keeps its height, and the target reaches over
     its neighbours' padding. */
  .mantine-Pill-remove {
    min-height: var(--touch-target) !important;
    min-width: var(--touch-target) !important;
    margin-block: calc((1.375rem - var(--touch-target)) / 2);
    margin-inline: calc((1.375rem - var(--touch-target)) / 2);
  }
  /* The icon keeps its own size. Mantine draws it as a share of the button,
     so a 44 px button would draw a cross of 30 px, and it writes that share
     in the style of the svg, so only !important reaches it. */
  .mantine-Pill-remove svg {
    width: 0.5rem !important;
    height: 0.5rem !important;
  }

  /* The chrome bar's own controls are spans with an :on-click, not buttons
     (graph-ui.web.core/tbi), so the selector above cannot reach them. They are
     the one hand-named case, and they are named by what they ARE. */
  .ikc-touch-target { min-width: var(--touch-target); min-height: var(--touch-target); }

  /* The theme swatches of the top bar are buttons, so the rule above already
     makes each one a 44 px target. The padding of their row then added 8 px,
     and the top bar was 55 px high, not one row of targets (fault report
     189f8375, a Boox reader in portrait). The row keeps its side padding. */
  .ikc-theme-swatches { padding-block: 0; }

  /* THE READER'S SIZE CONTROLS ARE AS WIDE AS A FINGER (George, 2026-10-05).
     A− and A+ are text buttons two characters wide, so the text-button
     exclusion above would leave them 44 px tall and about 30 px across. The
     row is named by its import id, which the renderer puts on the wrapper of
     every node (data-import-id), and an island prefixes it.
     !important, because the :button widget writes min-width 0 inline so a
     label can shrink (graph-ui.web.widgets/widget-button). */
  [data-import-id$="reading-controls"] button { min-width: var(--touch-target) !important; }

  /* A CLIPBOARD IMAGE FROM AN ANDROID KEYBOARD (fault report 08e4f4c1, "cannot
     paste image on Samsung s26 Ultra"). Chrome on Android offers a keyboard
     image only to a field it calls rich, and a plain textarea is not one. The
     keyboard then inserts nothing, and nothing says why. This property makes
     the field rich. Chrome then sends the image as a paste event with a file,
     which the field already reads (prompt-area/image-file-of). Only a field
     that takes an image carries data-image-input, and only a finger gets the
     rule, so a desktop is not changed. T-responsive-016. */
  textarea[data-image-input] { -webkit-user-modify: read-write; }
}

/* THE READER'S TEXT SIZE, IN ITS EDITOR (George, 2026-10-05). The widget puts
   a type token on its own wrapper, never a pixel size, and CodeMirror takes it
   from there. The scroller carries it so the line numbers in the gutter keep
   the height of the lines they number. !important, because a CodeMirror theme
   can set a size on .cm-content and is injected after this file. */
.ikc-editor-sized .cm-editor,
.ikc-editor-sized .cm-scroller,
.ikc-editor-sized .cm-content {
  font-size: inherit !important;
  line-height: inherit !important;
}

/* ── THE READING PANE ON A PHONE (lane PH, PLAN-ikc-v3-054-01, 2026-09-29) ────
 *
 * Two rules, each for one of George's two screenshots of 2026-09-29. Both
 * find their nodes by the import id that the renderer puts on every root
 * (render.cljs), and the ids are declared in dsl/alatus/reading-area.edn.
 * T-responsive-011 and T-responsive-013 read that file and this one, so a
 * rename in one of them fails the suite.
 *
 * NEITHER RULE SETS A MAXIMUM WIDTH. George sets the width, not the app.
 */

/* Rule A. A SHORT WINDOW SCROLLS THE HEADER AWAY.
 *
 * "In horizontal view I see only the not scrollable area of the page and
 * cannot go to the content." Measured in Firefox at 915 × 412 with touch: the
 * page header was 500 px high in a reading pane of 344 px. The body switch
 * (reading-what) had :flex 1 and min-height 0, so it got 0 px, and the column
 * did not scroll. 915 px is above --viewport-narrow, so no narrow rule
 * applied. The same was true at 844 × 390, and at 768 × 355 with the narrow
 * dock.
 *
 * The body now has a floor: the whole height of the column, or 24rem if that
 * is less. And the column scrolls. On a desk window the body is higher than
 * 24rem, so nothing overflows and the header stays in place, as the concept
 * of the reading surface says. On a short window the column overflows, and a
 * scroll moves the header up and away. Then the body fills the pane.
 *
 * No width and no height is asked. So this rule adds no row to the display
 * profile, and it changes no island whose column has room.
 */
[data-import-id$="/reading-column"] { overflow-y: auto; }
[data-import-id][data-import-id$="/reading-what"] { min-height: min(100%, 24rem); }

/* Rule B. THE RIGHT GUTTER FOLDS UNDER THE TEXT (SPEC-alatus-v0-002-02 row
 * C-61, the phone column).
 *
 * "In vertical view the gutter eats half the space." Measured in Firefox at
 * 390 × 844: the text was 205 px of a pane of 390 px, and the 15ch gutter was
 * 125 px beside it. George, 2026-08-15: "The right gutter is the place for
 * meta information, no extra gutters." So the gutter is not removed. It moves.
 *
 * ONE RULE FOR EACH BODY. Every placement of the part right-gutter has an
 * import id that ends in `right-gutter`: the document, the conversation, the
 * outline body and the editable body. The rule stacks the parent of such a
 * placement. So a new body gets the fold if it names its placement as the
 * concept names it (CONCEPT-graph-ui-v0-013-01).
 *
 * THE ROOT SAYS NARROW, NOT THE WIDTH. graph-ui.web.responsive/set-narrow!
 * writes data-narrow on <html> below --viewport-narrow. A media query here
 * would hold the number a second time.
 *
 * THE ROWS STOP FOLLOWING THEIR MARKS. Beside the text, widget-highlight-list
 * puts each row at the height of its mark, with an inline top. Under the
 * text a mark is above the gutter, so that height means nothing. The rows
 * then flow as a list, and the list has a height. The inline style is why
 * `!important` is here, on these two properties only.
 */
[data-narrow] :has(> [data-import-id$="right-gutter"]) {
  flex-direction: column;
  align-items: stretch;
}
[data-narrow] [data-import-id$="right-gutter"] > * {
  max-width: none;
  width: 100%;
}
[data-narrow] [data-import-id$="right-gutter"] [style*="position: absolute"] {
  position: static !important;
  top: auto !important;
}

/* ── COLOUR UTILITIES — the design system's own, 2026-09-01 ───────────────────
 *
 * WHY THESE EXIST, AND WHY THEY ARE NOT TAILWIND. Until today the renderer said
 * `text-gray-200` and meant `var(--text)`. It worked only because
 * css/ikc-tailwind.js handed the CDN a map bending Tailwind's whole palette onto
 * the tokens. So every colour decision was written as a NICKNAME and resolved
 * through a second table — the token was the declaration and `gray-200` was a
 * word for it. validate-dsl already refuses `shadow-md`, `z-50` and
 * `opacity-40` for exactly that reason: "a Tailwind utility that duplicates a
 * token is the token's replacement". Colour was the largest case and the one
 * the gate did not cover: 198 uses.
 *
 * WHY NOT text-[var(--text)], WHICH THE CODE ALREADY USES 107 TIMES. Hiccup's
 * shorthand — [:div.text-gray-200.p-2] — is a Clojure KEYWORD, and a keyword
 * cannot contain [ ] or ( ). So the arbitrary-value form is available ONLY
 * where an element already writes {:class "…"}. That is the whole explanation
 * for the codebase's 40/60 split, measured 2026-09-01: 107 token-form classes
 * in strings, ZERO in shorthand. These class names are plain identifiers, so
 * they work in both.
 *
 * THEY DUPLICATE NO VALUE. Each is one property pointing at one var(), so
 * themes.edn stays the single source of truth and a theme switch still moves
 * everything. Only the names that are actually used are here; validate-dsl
 * refuses a colour nickname, so the set grows on purpose.
 */
.ikc-fg-heading  { color: var(--heading) }
.ikc-fg-text     { color: var(--text) }
.ikc-fg-muted    { color: var(--muted) }
.ikc-fg-faint    { color: var(--faint) }
.ikc-fg-border   { color: var(--border) }
.ikc-fg-accent   { color: var(--accent) }
.ikc-fg-accent-2 { color: var(--accent-2) }
.ikc-fg-good     { color: var(--good) }
.ikc-fg-warn     { color: var(--warn) }
.ikc-fg-crit     { color: var(--crit) }

.ikc-bg-ground   { background-color: var(--ground) }
.ikc-bg-panel    { background-color: var(--panel) }
.ikc-bg-raised   { background-color: var(--raised) }
.ikc-bg-card     { background-color: var(--card) }
.ikc-bg-border   { background-color: var(--border) }

.ikc-bd-border   { border-color: var(--border) }
.ikc-bd-raised   { border-color: var(--raised) }
.ikc-bd-accent   { border-color: var(--accent) }
.ikc-bd-good     { border-color: var(--good) }
.ikc-bd-warn     { border-color: var(--warn) }
.ikc-bd-crit     { border-color: var(--crit) }

/* ── dockview, THEMED ONCE (ADR-graph-ui-v0-026-01) ───────────────────────────
 *
 * dockview ships ten themes of its own and applies one by class. Every one is a
 * fixed palette: `.dockview-theme-light` and `.dockview-theme-abyss` know
 * nothing about themes.edn, so a dock drew the Amber island in near-black —
 * seen on the first render, 2026-09-02.
 *
 * THE CLASS, NOT THE INSTANCE. The wrong repair is to style one island's dock.
 * dockview reads its own CSS custom properties, so mapping THOSE onto the
 * tokens themes each dock that will ever exist, and a theme switch moves them
 * with everything else. No island says anything about it.
 *
 * `.ikc-dock-theme` is handed to createDockview as its THEME, so dockview puts
 * the class on its own shell and none of its ten themes is applied over it.
 *
 * Only the colour variables are set. Sizes, radii and transitions are
 * dockview's own mechanism, not decisions this design system has made.
 */
/* AN ISLAND'S STACKING STAYS INSIDE THE ISLAND (2026-09-07).
 *
 * George: "Top bar island selector, when opened should be above other elements,
 * now the reader-v3 line between the panes stays above it." Measured the same
 * day: `.dv-sash` computes to `z-index: 99`, `position: absolute`, and its
 * `.dv-sash-container` is `z-index: auto` — so the 99 was not confined to the
 * dock at all. It competed in the ROOT stacking context, where the picker's
 * menu sits, and 99 beats it. The symptom is precise: a 4px vertical line
 * painted straight across an open dropdown.
 *
 * RAISING THE MENU WOULD BE THE WRONG REPAIR. dockview's stack goes to 1001
 * (drop indicators, the drag compass, floating titlebars), so a number that
 * clears today's sash is a number the next dockview feature beats. And
 * dsl/instances/themes.edn declares FOUR ordered layers ending at
 * `--layer-modal: 50`; a fifth literal in ClojureScript is the thing that
 * declaration exists to prevent.
 *
 * `isolation: isolate` makes this element a stacking context, so EVERY z-index
 * dockview uses — 99, 999, 1001 — is ordered against its siblings inside the
 * dock and against nothing outside it. The dock then takes its own place in the
 * application's ladder as one element. It changes no layout and no paint order
 * within the dock, which is why the sashes, the drop indicators and the drag
 * compass all still stack over the panes exactly as before.
 *
 * THIS IS THE SAME LESSON THE LADDER ALREADY RECORDS one screen down: the four
 * layers describe the renderer's OWN stacking, and Mantine's 1001 exposed that
 * in August. dockview is the second library to do it. The answer this time is
 * to contain the library rather than to out-number it.
 */
.ikc-dock { isolation: isolate; }

.ikc-dock-theme {
  /* the surface a group of panes sits on */
  --dv-group-view-background-color: var(--body);
  --dv-tabs-and-actions-container-background-color: var(--panel);

  /* the active pane's tab reads as the current one */
  --dv-activegroup-visiblepanel-tab-background-color: var(--card);
  --dv-activegroup-visiblepanel-tab-color: var(--heading);
  --dv-activegroup-hiddenpanel-tab-background-color: var(--panel);
  --dv-activegroup-hiddenpanel-tab-color: var(--muted);

  /* a group that does not hold the focus states it by going quieter */
  --dv-inactivegroup-visiblepanel-tab-background-color: var(--panel);
  --dv-inactivegroup-visiblepanel-tab-color: var(--muted);
  --dv-inactivegroup-hiddenpanel-tab-background-color: var(--panel);
  --dv-inactivegroup-hiddenpanel-tab-color: var(--faint);

  --dv-tab-divider-color: var(--border);
  --dv-separator-border: var(--border);
  --dv-paneview-header-border-color: var(--border);
  --dv-paneview-active-outline-color: var(--accent);

  /* the drag handle between panes, and where a pane would land */
  --dv-sash-color: var(--border);
  --dv-active-sash-color: var(--accent);
  --dv-drag-over-background-color: var(--accent-soft);
  --dv-drag-over-border-color: var(--accent);
  --dv-edge-dock-indicator-color: var(--accent);

  --dv-icon-hover-background-color: var(--hover);
  --dv-floating-titlebar-background-color: var(--panel);
  --dv-scrollbar-background-color: var(--border);
}

/* THE GRIP (George, 2026-09-08: "Put a slim drag grip on each pane's top edge.
   Make the pane border more pronounced and to show focus"). dockview moves a pane
   by dragging its TAB, and the tab strip was hidden on 2026-09-07 because it drew
   a title and a ✕ over every pane. So the strip stays, 10px tall, and the tab
   inside it is drawn as a short bar — no title, no ✕ — which is the drag handle.
   Where a pane sits is a runtime value the person owns (the session frame keeps
   it), so this grip is how "below another pane" happens: never a declaration.
   The active group — the one that holds the focus — takes the accent on its
   border and on its grip; every other group has the plain border. */
.ikc-dock-theme { --dv-tabs-and-actions-container-height: 10px; }
.ikc-dock-theme .dv-groupview { border: 1px solid var(--border); box-sizing: border-box; }
.ikc-dock-theme .dv-groupview.dv-active-group { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.ikc-dock-theme .dv-tabs-and-actions-container { background: var(--panel); }
.ikc-dock-theme .dv-active-group > .dv-tabs-and-actions-container { background: var(--accent-soft); }
.ikc-dock-theme .dv-tab { flex: 1 1 0; min-width: 0; padding: 0; height: 100%;
                          display: flex; align-items: center; justify-content: center; cursor: grab; }
.ikc-dock-theme .dv-tab:active { cursor: grabbing; }
.ikc-dock-theme .dv-tab .dv-default-tab { display: none; }   /* the title and the ✕ */
.ikc-dock-theme .dv-tab::before { content: ""; width: 36px; height: 4px; border-radius: 2px; background: var(--border); }
.ikc-dock-theme .dv-tab:hover::before,
.ikc-dock-theme .dv-active-group .dv-tab::before { background: var(--accent); }

/* PROGRESSIVE SUMMARIZATION (RESEARCH-ikc-v3-021-01 D5, PLAN-ikc-v3-026-01 D).
   A highlight's LAYER, 1 to 5, drawn ON TOP of its colour wash: layer 1 is the
   wash alone (the passage you saved), 2 is bold (the part you made bold), 3 is
   bold and underlined (the best of the bold), 4 doubles the line (a summary in
   your own words), 5 is italic too (a remix). One class per layer, so a layer-3
   span inside a layer-2 span is bold and washed twice. Declared in
   dsl/contexts/ui.edn (:markdown), drawn here and nowhere else. */
.ikc-hl-layer-2 { font-weight: 600; }
.ikc-hl-layer-3 { font-weight: 600; text-decoration: underline; text-decoration-thickness: 1px; }
.ikc-hl-layer-4 { font-weight: 600; text-decoration: underline double; }
.ikc-hl-layer-5 { font-weight: 700; text-decoration: underline double; font-style: italic; }

/* RENDERED MARKDOWN STAYS IN ITS BLOCK (PLAN-ikc-v3-054-01, lane PT). A line
   with no place to break, as a border of box characters is, scrolls inside the
   markdown it is in. Before, it went past the right edge of the reading pane.
   graph-ui.web.markdown/block-class gives every rendered text this class. */
.ikc-md { max-width: 100%; overflow-x: auto; }

/* A CONSOLE TABLE FITS THE PANE. The renderer draws the box characters of the
   Claude Code console as a <table> (graph-ui.web.markdown/split-box-tables).
   A cell wraps at its spaces, so the columns share the width of the pane. A
   word breaks only when it is wider than its cell. A table that is still too
   wide scrolls inside its block, with its words whole. George, 2026-09-29: "a
   reflow based on the content and typical width of a reading pane, I may drag
   browser wider in order to accomodate reasonable table". */
.ikc-box-table th, .ikc-box-table td { overflow-wrap: break-word; }

/* The coach mark (PLAN-ikc-v3-028-01 Phase D): the target wears a ring, the
   rest of the island a light wash. The wash lets pointer events through, so a
   person can still act on the target it points at. */
.ikc-coach-target  { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.ikc-coach-overlay { position: fixed; inset: 0; z-index: 50; pointer-events: none;
                     background: color-mix(in srgb, var(--text) 18%, transparent); }
.ikc-coach-anchor  { position: fixed; z-index: 60; }
.ikc-coach-badge   { position: fixed; z-index: 60; min-width: 22px; height: 22px; padding: 0 6px;
                     border-radius: 11px; background: var(--accent); color: var(--panel);
                     font-size: 12px; font-weight: 700; line-height: 22px; text-align: center;
                     box-shadow: var(--elevation-raised); pointer-events: none; }
.ikc-coach-card    { background: var(--panel); box-shadow: var(--elevation-floating); }
/* --panel is the theme's opaque pane tone; --surface is not a theme variable here, and a
   card painted with it was transparent (2026-09-08). The elevation is the theme's own. */
/* Every rule the mark needs lives HERE and not in a framework utility: the
   generated stylesheet holds only the utilities it saw, and a class it did not
   see draws nothing — which is how the wash blocked every click on 2026-09-08. */

/* ═══════════════════════════════════════════════════════════════════════════
   THE SCROLLBAR — 2026-09-11, PLAN-graph-ui-v0-016-02 follow-up.
 *
 * THE ONE PIECE OF CHROME NO TOKEN COULD REACH. This stylesheet styled no
 * scrollbar at all, so every scrollable pane in every island got the
 * browser's own — an OVERLAY bar on this platform: it appears while you
 * scroll, takes no space, and leaves nothing to aim at. George, 2026-09-11,
 * on the Studio's tabs: the absence of horizontal scroll. The content
 * scrolled; the bar did not exist.
 *
 * IT IS TOKENS, NOT A LOOK. dsl/instances/themes.edn declares the four —
 * scrollbar-size in :base beside touch-target, and three colours per theme —
 * so a theme owns its own chrome and the theme editor can move it. A rule
 * with literals here would be the thing the design system exists to refuse.
 *
 * BOTH SPELLINGS. `scrollbar-color` is the standard and Firefox honours it;
 * the ::-webkit- rules are what Chrome reads. Naming both is how one
 * declaration reaches both engines.
 *
 * NOT ON THE DOCUMENT. The page itself never scrolls sideways (rule 1 above),
 * so these apply to the elements that DO scroll and leave the document alone.
 * ═══════════════════════════════════════════════════════════════════════════ */
* {
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
  scrollbar-width: thin;
}
*::-webkit-scrollbar {
  width:  var(--scrollbar-size);
  height: var(--scrollbar-size);
}
*::-webkit-scrollbar-track {
  background: var(--scrollbar-track);
}
*::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--radius-sm);
  /* A thumb that touches the track's edges reads as a block; the border is the
     track showing through, which is what makes it look like a thumb. */
  border: 2px solid var(--scrollbar-track);
  background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
*::-webkit-scrollbar-corner      { background: var(--scrollbar-track); }

/* ═══════════════════════════════════════════════════════════════════════════
   A CONTROL'S OWN SURFACE — 2026-09-11.
 *
 * Mantine paints its default Button, Input and Select from four variables of
 * its own. Until today nothing pointed them at this project's tokens, so a
 * control kept Mantine's greys whatever the theme said — half of why a person
 * editing a swatch could not find what it moved.
 *
 * FOUR TOKENS, DECLARED PER THEME (dsl/instances/themes.edn :control). This
 * is the bridge and not the decision: the values are the theme's.
 *
 * `control-active` has no Mantine variable to take, so it is applied below to
 * the one state this codebase already spells: Mantine's `light` variant is
 * what graph-ui.web.widgets/tree-row-button gives the CURRENT row — today in
 * the daily nav, the focused tile, and the open level in the Studio's tabs.
 * ═══════════════════════════════════════════════════════════════════════════ */
/* THE SELECTOR MUST MATCH MANTINE'S OWN, and a plain :root does not. Mantine
   sets these under `:root[data-mantine-color-scheme='light']` — specificity
   (0,1,1) — so a bare `:root` (0,0,1) lost, silently: measured 2026-09-11,
   --mantine-color-default stayed #fff in Amber and #2e2e2e in Midnight while
   --control-face said #fdf5d8 and #242a35. Same shape, and ikc-base.css is the
   last stylesheet index.html loads, so ours wins on order. */
:root[data-mantine-color-scheme='light'],
:root[data-mantine-color-scheme='dark'] {
  --mantine-color-default:        var(--control-face);
  --mantine-color-default-border: var(--control-border);
  --mantine-color-default-hover:  var(--control-hover);
}

/* THE CURRENT CONTROL. `light` is Mantine's variant for a control that is in
   force, and tree-row-button uses it for :current and :today. Pointing it at a
   token is what lets a theme say how "this one is selected" looks. */
.mantine-Button-root[data-variant='light'] {
  background: var(--control-active);
}

/* ═══════════════════════════════════════════════════════════════════════════
   A CODE BLOCK GETS ITS NATIVE SCROLLBARS BACK — 2026-09-11.
 *
 * George: horizontal scroll is not present in the Declaration tab. It was
 * present and invisible, which is the same thing from a chair.
 *
 * Mantine's CodeHighlight wraps its own content in a ScrollArea, and a
 * ScrollArea hides the native bars (`scrollbar-width: none`, and
 * `::-webkit-scrollbar { display: none }`) so it can draw its own. Its own
 * VERTICAL bar appears; its HORIZONTAL bar reports data-state="hidden" and
 * zero height, because it lays content out with `min-width: 100%` and measures
 * no overflow to report. So a wide declaration scrolled 324 pixels with
 * nothing on screen saying it could.
 *
 * THE NATIVE BARS ARE THE ANSWER NOW, and they were not before this morning:
 * the scrollbar tokens give them a size and a colour, so a native bar is 11
 * pixels of real chrome that follows the theme. Undoing Mantine's hiding for
 * THIS component only is smaller than fighting its own scrollbar logic.
 *
 * SPECIFICITY IS THE WHOLE TRICK. Mantine's rules are one class (0,1,0); a
 * descendant selector is two (0,2,0) and wins without !important.
 * ═══════════════════════════════════════════════════════════════════════════ */
.mantine-CodeHighlight-scrollarea .mantine-ScrollArea-viewport {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.mantine-CodeHighlight-scrollarea .mantine-ScrollArea-viewport::-webkit-scrollbar {
  display: block;
  width:  var(--scrollbar-size);
  height: var(--scrollbar-size);
}
.mantine-CodeHighlight-scrollarea .mantine-ScrollArea-viewport::-webkit-scrollbar-track {
  background: var(--scrollbar-track);
}
.mantine-CodeHighlight-scrollarea .mantine-ScrollArea-viewport::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--radius-sm);
  border: 2px solid var(--scrollbar-track);
  background-clip: padding-box;
}
.mantine-CodeHighlight-scrollarea .mantine-ScrollArea-viewport::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
}
