/* ===========================================================================
   @arag CMS — editor chrome
   ---------------------------------------------------------------------------
   Load AFTER src/index.css and cms.css:

     <link rel="stylesheet" href="../src/index.css">
     <link rel="stylesheet" href="cms.css">
     <link rel="stylesheet" href="editor.css">

   Same ground rules as cms.css: unlayered by design (beats all six @arag
   layers with no specificity trick), tokens for every colour/length/radius/
   shadow/duration, logical properties throughout, no priority-raising
   declarations, and no parent-selector pseudo-class (stated without naming
   it, on purpose — see the same note in cms.css and menu.css: the CI
   invariant is a grep for the literal selector, and writing it even inside a
   comment fails the check).

   What belongs here vs. what does not
   ------------------------------------
   This file styles the EDITOR'S OWN chrome: the three-column shell, the
   overlay hit-test layer, the resizer toolbar, and small layout helpers for
   the palette and inspector panels. It never styles a `.cms-*` or `.button` /
   `.field` / `.card` / `.menu` / `.masthead` class beyond what those
   components already expose through their own local custom property API —
   the palette and inspector are built FROM those real components, not
   reinvented.

   cms.css documents that it must never declare `gap` because unlayered CSS
   would defeat every `u-gap-*` an author picked on RENDERED CONTENT. That
   constraint does not apply here: every selector in this file is scoped to
   `.editor-*` classes that only ever wrap the editor's own chrome and never
   wrap a `.cms-section`/`.cms-block` subtree, so a `gap` declared here can
   never collide with an author's utility class.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   Page shell
   --------------------------------------------------------------------------- */

/* Fixed viewport block-size, not a minimum: the shell below is a flex child
   sized to "whatever is left after the masthead", and that subtraction is
   flexbox's job, not a calc() that guesses the masthead's rendered height.
   --masthead-min-block-size is a LOCAL custom property (masthead.css) that
   only inherits to .masthead's own descendants — the shell is its sibling,
   not its child, so reading it here would silently fall back every time
   anyway. Flex layout sidesteps the question entirely. */
.editor-body {
  display: flex;
  flex-direction: column;
  block-size: 100dvb;
}

.editor-shell {
  display: grid;
  /* Palette and inspector widths are now runtime state, not a fixed pair of
     tokens: editor.js drags/collapses them by writing --editor-aside-start
     and --editor-aside-end (a plain pixel length — a pointer gesture's
     result, not an authored value, so it is exempt from the raw-length ban
     the same way editor.js's stage-preset width already is) directly on this
     element. The two are declared here with the OLD fixed tokens as their
     value, so a page that loads before editor.js runs — or a browser where
     it throws — renders exactly the three-column layout this file shipped
     before: nothing changes on first paint.

     --editor-track-start/-end are the intermediate step that actually feeds
     the grid, and are what a collapsed side overrides (below) — a bare rail
     width (--size-touch, matching .editor-panel__rail's own inline-size),
     deliberately NOT run through the clamp(), since the whole point of
     collapsing is to go narrower than "the narrowest a dragged width may
     be". Splitting resize and collapse into two custom
     properties like this means the collapsed override only ever has to
     restate one keyword-like value, never the clamp expression.

     The clamp floor is 0.75x --size-column-min rather than the token itself:
     --size-column-min is already this package's "minimum comfortable
     column", and the palette's OWN default width already sits exactly at
     that floor (it always has — see the fixed value this replaces). A floor
     equal to the default would leave the palette no room to shrink at all,
     which defeats the point of a drag handle and the narrow-then-fold-to-
     one-column behaviour below. 0.75x is still derived from the same token
     via calc(), not a new literal, and stays comfortably above the point
     nothing can render. The ceiling, --size-container-sm (40rem), is picked
     for the same reason a stage preset never exceeds it: past that width a
     side panel is not "a bit wider", it is half the screen, so the canvas
     stops being the thing you are actually looking at. */
  --editor-aside-start: var(--size-card-min);
  --editor-aside-end: var(--size-card-min);
  --editor-track-start: clamp(calc(var(--size-column-min) * 0.75), var(--editor-aside-start), var(--size-container-sm));
  --editor-track-end: clamp(calc(var(--size-column-min) * 0.75), var(--editor-aside-end), var(--size-container-sm));
  /* The two `auto` tracks are the resize handles — sized to their own
     inline-size (below), never guessed at here. */
  grid-template-columns: var(--editor-track-start) auto 1fr auto var(--editor-track-end);
  /* A single explicit row filling the shell's own (flex-derived) block-size,
     rather than an implicit "auto" row that would content-size instead of
     filling it. align-items: stretch (the grid initial value, restated for
     clarity) then gives each column that same block-size, which is what
     lets each one scroll independently below. */
  grid-template-rows: minmax(0, 1fr);
  align-items: stretch;
  flex: 1 1 auto;
  min-block-size: 0;
}

/* A collapsed side is a fixed rail width, not a clamped drag value — see the
   long note above. --size-touch, matching .editor-panel__rail's own
   inline-size exactly: this is the grid track a collapsed rail actually
   renders into, so the two must never drift apart or the rail either clips
   or leaves a gap. Both attributes can apply at once (they target different
   custom properties), so a shell with both panels collapsed needs no third
   rule. */
.editor-shell[data-palette-collapsed] {
  --editor-track-start: var(--size-touch);
}

.editor-shell[data-inspector-collapsed] {
  --editor-track-end: var(--size-touch);
}

/* Explicit grid-column on all five direct children, rather than leaving them
   to implicit auto-placement. A collapsed resizer is HIDDEN (see
   setPanelCollapsed in editor.js), and an auto-placed grid removes a hidden
   item from the flow and re-packs everything after it one cell earlier —
   which silently hands the canvas's 1fr track to the wrong column and pushes
   the inspector off the end of the track list entirely. Pinning every child
   to the column it always occupies makes hiding one item inert to the rest,
   the same way it would be in a flex row with an explicit order. */
.editor-palette {
  grid-column: 1;
}
#palette-resizer {
  grid-column: 2;
}
.editor-canvas-wrap {
  grid-column: 3;
}
#inspector-resizer {
  grid-column: 4;
}
.editor-inspector {
  grid-column: 5;
}

.editor-panel__title {
  margin-block: var(--space-0);
  padding-block-end: var(--space-3);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------------
   Palette / Inspector — scrolling side panels
   ---------------------------------------------------------------------------
   Each aside is now a rail (the collapse/expand button, always visible) plus
   a body (the title and the panel's real content, the part `hidden` removes).
   The rail never scrolls or gets padding of its own; the body keeps exactly
   the padding and independent scroll this file always gave the whole aside,
   just one level deeper. Nothing that already targets `.editor-panel__title`,
   `.editor-palette__list` or `#inspector-body` needed to change for that move
   — none of them selected on a direct-child relationship to the aside.
   --------------------------------------------------------------------------- */

.editor-palette,
.editor-inspector {
  display: flex;
  min-block-size: 0;
  min-inline-size: 0;
  background-color: var(--color-surface);
}

.editor-palette {
  border-inline-end: var(--border-width-1) solid var(--color-border-subtle);
}

.editor-inspector {
  border-inline-start: var(--border-width-1) solid var(--color-border-subtle);
}

/* The rail: a fixed-width strip the full block-size of the aside (stretch is
   the flex default, restated for clarity), holding only the toggle button.
   It is what stays on screen — at the panel's own outer edge — once the body
   is hidden, which is the "still a way back" requirement's whole mechanism. */
.editor-panel__rail {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* --size-touch, not --space-7: this rail holds the ONLY always-reachable
     control on a collapsed panel, and on a narrow shell (below, the Narrow
     shell section) it is also the sole surviving grid column — the body's
     two-column button grid gives up --space-3 more per side than it used to
     so the toggle keeps a real touch-sized hit area on both axes, not just
     block-size. --button-min-block-size already met the floor; this is what
     brings inline-size up to match it, via .editor-panel__toggle below. */
  inline-size: var(--size-touch);
  padding-block-start: var(--space-3);
}

.editor-panel__toggle {
  --button-padding-inline: var(--space-2);
  --button-padding-block: var(--space-2);
  /* button.css only restores the --size-touch block-size floor under
     data-size="sm" behind (pointer: coarse) — a deliberate density choice
     for a dense row of precise-pointer buttons (see button.css's own note
     and docs/responsive.html#touch-targets). That reasoning does not carry
     over here: this is not a dense list, it is the ONE way back into a
     collapsed panel, so it stays touch-sized under every pointer, the same
     unconditional way menu.css, tabs.css, scrollspy.css and collapse.css
     each set their own min-block-size straight to --size-touch. */
  --button-min-block-size: var(--size-touch);
  /* button.css never sets an inline-size on a non-icon-only button (rule: a
     button is always as wide as its label) — this one's "label" is the
     visually-hidden marker text, so nothing else would ever push it out to
     the rail's own --size-touch width. Pinned explicitly so the CLICKABLE
     button matches the rail it sits in, not just the rail's own box. */
  min-inline-size: var(--size-touch);
}

/* Border-drawn corner, same technique collapse.css uses for its own marker
   and for the same reason: no glyph, no icon font, nothing a translator would
   ever need to touch. Which corner points where depends on BOTH which edge
   the panel collapses to (data-side) and its current state, because "point
   toward the edge" (collapse) and "point back toward the canvas" (expand) are
   opposite directions on the two sides. */
.editor-panel__toggle-marker {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  inline-size: var(--space-4);
  block-size: var(--space-4);
  color: var(--color-text-muted);
}

.editor-panel__toggle-marker::before {
  content: "";
  display: block;
  inline-size: 50%;
  aspect-ratio: 1;
  border-inline-end: var(--border-width-2) solid currentColor;
  border-block-end: var(--border-width-2) solid currentColor;
  transition: transform var(--transition-base);
}

.editor-panel__toggle[data-side="start"][aria-expanded="true"] .editor-panel__toggle-marker::before {
  transform: rotate(135deg); /* points to inline-start: collapsing that way */
}

.editor-panel__toggle[data-side="start"][aria-expanded="false"] .editor-panel__toggle-marker::before {
  transform: rotate(-45deg); /* points back to inline-end: expand, toward the canvas */
}

.editor-panel__toggle[data-side="end"][aria-expanded="true"] .editor-panel__toggle-marker::before {
  transform: rotate(-45deg); /* points to inline-end: collapsing that way */
}

.editor-panel__toggle[data-side="end"][aria-expanded="false"] .editor-panel__toggle-marker::before {
  transform: rotate(135deg); /* points back to inline-start: expand, toward the canvas */
}

.editor-panel__body {
  flex: 1 1 auto;
  min-block-size: 0;
  min-inline-size: 0;
  overflow-y: auto;
  padding: var(--space-5);
}

/* ---------------------------------------------------------------------------
   Resize handles (shell)
   ---------------------------------------------------------------------------
   The WAI-ARIA splitter pattern: a generic element (not a button — it is not
   activated, it is dragged or stepped with arrow keys), role="separator",
   keyboard-reachable, with the accessible name and aria-valuenow/-min/-max
   editor.js keeps in sync. Hidden while its own side is collapsed — there is
   nothing to drag when the track is pinned to the rail width. */
.editor-resizer {
  position: relative;
  inline-size: var(--space-4);
  cursor: col-resize;
  touch-action: none;
}

.editor-resizer::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 50%;
  inline-size: var(--border-width-1);
  translate: -50% 0;
  background-color: var(--color-border-subtle);
  transition: background-color var(--transition-base);
}

@media (hover: hover) {
  .editor-resizer:hover::before {
    background-color: var(--color-border-strong);
  }
}

.editor-resizer:focus-visible {
  outline: var(--border-width-2) solid var(--focus-ring-color);
  outline-offset: calc(var(--border-width-2) * -1);
}

/* ---------------------------------------------------------------------------
   Narrow shell
   ---------------------------------------------------------------------------
   Below this width the three-column grid above stops being viable at all: a
   floor-width palette plus a floor-width inspector (2 * 0.75 * 16rem = 24rem)
   already exceeds it, so "squeeze the canvas to make room" runs the canvas
   itself past zero and out the side of the page — the bug this section
   exists to close. Below the breakpoint, both grid tracks are pinned to the
   rail width UNCONDITIONALLY (not just while [data-*-collapsed], which is the
   only other place these two custom properties are written) and an EXPANDED
   panel instead floats over the canvas as its own positioned layer, the same
   width it would have dragged the grid track to. The canvas's own track
   therefore never changes size when a panel opens or closes at this width —
   nothing for stageEl's ResizeObserver (editor.js) to react to, and nothing
   for the overlay hit-test boxes to be wrong about in the gap before it does.

   ponytail: 40rem is a literal, matching --size-container-sm exactly — the
   same reason cms.css's own container tiers are literals (see that file's
   Collapse section): a media condition cannot read a custom property.
   --size-container-sm is the right number to mirror, not an arbitrary one:
   it is already this file's ceiling on how wide a dragged panel may become
   (panelCeilPx() in editor.js), so this is exactly the width below which
   "the panel, at its widest, IS most of the shell" stops being a drag-time
   edge case and becomes the normal case worth designing for on purpose.
   Keep in step with src/tokens.css's --size-container-sm. */
@media (width < 40rem) {
  .editor-shell {
    /* The floating panels below anchor to this box, not to the viewport —
       matches masthead-relative sizing the same way .editor-body does (see
       its own note): no calc() has to guess the masthead's height because
       the panel's inset-block: 0 is already relative to a box that starts
       below it. */
    position: relative;
    --editor-track-start: var(--size-touch);
    --editor-track-end: var(--size-touch);
  }

  /* The resizer has nothing left to do: the grid track it used to widen is
     now fixed, and a dragged width feeds a floating layer instead (below).
     Dragging a splitter that is hidden behind the very panel it resizes is
     not a keyboard-reachable affordance either, so this is a removal, not a
     visual detail — the WAI-ARIA splitter pattern in the Resize handles
     section above is for the grid-column layout that only applies past this
     breakpoint. */
  .editor-resizer {
    display: none;
  }

  .editor-palette,
  .editor-inspector {
    /* A grid item that turns position: absolute keeps its assigned grid AREA
       as its containing block for every percentage and inset below — it is
       NOT promoted to the whole grid container the way a plain block child
       would be. Both still carry the single-track grid-column from the Page
       shell section above (column 1 / column 5), so without this, "100%" in
       max-inline-size below resolved against THAT column's own
       --size-touch width, not the shell's — verified: it clamped every
       width, collapsed or expanded, down to ~1px, with the rail rendering
       outside its own box. Spanning every line puts the containing block
       back to the shell's own padding box, matching inset-inline-start/-end
       below, which anchor to the shell for the same reason. */
    grid-column: 1 / -1;
    position: absolute;
    inset-block: 0;
    /* var(--z-modal): above .editor-overlay's --z-overlay (the canvas
       hit-test layer, editor.css's Overlay section) — both sit in the same
       stacking context (.editor-canvas-wrap creates no context of its own,
       so .editor-overlay's z-index compares directly against this element's,
       not against its parent's), and an open panel is meant to sit ON the
       canvas, blocking it, the same relationship a drawer has to the page
       behind it. */
    z-index: var(--z-modal);
    box-shadow: var(--shadow-lg);
    /* Never wider than "the shell minus the OTHER side's rail": the other
       toggle must stay reachable by click even while this one is open, so
       both panels can be reached without a keyboard, and opening one can
       never fully hide the other's own way back. */
    max-inline-size: calc(100% - var(--size-touch));
  }

  .editor-palette {
    inset-inline-start: 0;
    /* Expanded width mirrors what the grid track would have been at any
       wider viewport: the SAME custom property (--editor-aside-start),
       already kept current by applyPanelWidth() whenever the panel is not
       collapsed, regardless of which layout mode is reading it. */
    inline-size: var(--editor-aside-start);
  }

  .editor-inspector {
    inset-inline-end: 0;
    inline-size: var(--editor-aside-end);
  }

  /* Collapsed is --size-touch explicitly, NOT left to shrink-to-fit: an
     auto-width absolutely-positioned flex container, with only one inset set
     and a max-inline-size also in play, is exactly the combination the CSS2
     abspos sizing algorithm resolves last and least predictably — verified
     collapsing to a ~1px box with the rail rendering 44px outside it, taking
     the always-reachable "way back" this whole section exists to guarantee
     out past the shell's own edge. An explicit width matching the rail's own
     inline-size (editor-panel__rail, above) removes the ambiguity outright,
     the same way the collapsed GRID track above it is a fixed --size-touch
     rather than an inferred one.

     PLUS --border-width-1, not bare --size-touch: each aside carries one
     inline border facing the canvas (.editor-palette's border-inline-end,
     .editor-inspector's border-inline-start — Palette/Inspector section,
     above), which eats into the CONTENT box the rail (also --size-touch,
     border-box) sits in. Without the addition the content box is one px
     narrower than the rail asks for, and the rail overflows its own
     border-box by exactly that px — verified: at 375 the inspector rail
     measured right: 361 against the shell's own right: 360, and
     document.scrollWidth read 361 against a clientWidth of 360, one-for-one.
     The border faces inward (toward the canvas), so this only ever grows
     the aside AWAY from the shell's own outer edge — inset-inline-start/-end:
     0 above still pins that edge exactly, nothing here can push it past the
     viewport. */
  .editor-shell[data-palette-collapsed] .editor-palette {
    inline-size: calc(var(--size-touch) + var(--border-width-1));
  }

  .editor-shell[data-inspector-collapsed] .editor-inspector {
    inline-size: calc(var(--size-touch) + var(--border-width-1));
  }
}

.editor-palette__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Palette groups. The collapse component (paletteGroupEl in editor.js) brings
   the trigger, marker, focus ring and open/closed behaviour; this only lays
   out what is inside one open group. The group headers themselves stay in
   .editor-palette__list, one level up, which is untouched flex-column full-
   width stacking — only the buttons below go into this grid. */
.editor-palette__group-items {
  display: grid;
  /* The column minimum is sized to fit the LONGEST SINGLE WORD in the palette
     plus its icon, not an arbitrary fraction of --size-column-min: at 1/3
     (the value this replaced, from before icons existed) the icon plus gap
     ate enough of an already-tight column that "Accordion", "Countdown" and
     "Blockquote" no longer fit unbroken, and the label rule below was
     leaning on overflow-wrap: anywhere to paper over it — breaking those
     words MID-WORD rather than only at a space, which is exactly what rule
     6 forbids (an ellipsis is banned for the same reason a mid-word break
     is: both destroy a reader's ability to read the whole string). Measured
     the actual worst case in a live DOM (icon + widest unbroken word +
     button padding): "Countdown" at ~104px. --size-column-min / 2 (8rem/
     128px) clears that with headroom for a slightly wider translated word later,
     expressed as a token fraction rather than a new literal, same
     discipline the /3 it replaces already used.

     auto-fill + minmax needs no container query: the track's own minimum
     decides the fold point on its own. Two-up at the palette's default
     width no longer fits at this minimum (128 * 2 > the ~220px content box
     the default width leaves after the rail and this group's own padding),
     so the grid folds to ONE column there and reflows to two once the
     panel is dragged wider — exactly the "drop to one column when narrow"
     shape a single-word label needs to never break. */
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--size-column-min) / 2), 1fr));
  gap: var(--space-2);

  /* The collapse component's own inner padding, re-pointed rather than
     overridden. --collapse-padding-inline is part of the local custom
     property API collapse.css publishes for exactly this, so this is the
     sanctioned way to reclaim the space: no .collapse__* part is restyled
     and every other collapse in the editor keeps the default.

     It is reclaimed because the arithmetic was brutal at the default width.
     A 256px palette minus the rail, minus 16px of this padding on each side,
     left a 176px content box and therefore two 68px columns — narrow enough
     that "+ Table of contents" wrapped to a 184px-tall stack of syllables.
     A palette button has to be readable at a glance or the palette is not
     one. */
  --collapse-padding-inline: var(--space-2);
}

/* The palette label is the whole point of the long-text-safe requirement:
   blocks.json labels are read straight in (English only — see
   buildPaletteItem), and a long English label ("Table of contents",
   "Testimonial") must wrap inside the button rather than clip. .button
   already sets no fixed width and `text-wrap: balance` (never nowrap or an
   ellipsis) — that wraps a MULTI-word label at the space between words, which
   is most of them.

   overflow-wrap: anywhere used to sit here as a fallback for the single-word
   labels ("Testimonial", "Blockquote", "Accordion") a too-narrow column
   could not otherwise wrap — but "anywhere" does not mean "only as a last
   resort in practice": once the column added the icon's own width and grew
   too tight, it started breaking words like "Accordion" MID-WORD
   ("Accordio|n") on every render, not just in some theoretical worst case,
   because "anywhere" also shrinks the box's own min-content size, so the
   browser reaches for it before it has to. A mid-word break is exactly the
   same failure ellipsis and truncation are banned for — it destroys a
   reader's ability to read the whole string, and would be worse in
   a language where a case-marking suffix carries the grammatical meaning,
   which rule 6 already calls out. The real fix was sizing the column to the word,
   not attaching a wrap mode that hides an undersized column: see the
   group's own grid-template-columns note above.

   With that column now wide enough for the longest actual EN word, plain
   `normal` briefly sat here instead — but `normal` has no fallback at all,
   so it traded a rare mid-word break for a silent overflow: a column
   dragged narrower than the grid minimum, or a single unbroken translated
   word this codebase does not carry yet (one eleven-letter word already runs
   past a tight chip), would spill out of the chip with nothing left to
   catch it, which is its own version of rule 6's no-fixed-width assumption.
   overflow-wrap: break-word is the middle ground `anywhere` and `normal`
   both miss: it only breaks a word that CANNOT fit its line at all, so an
   ordinary label still only ever wraps at a space, and the safety net from
   the paragraph above is back for the case sizing the column cannot cover
   by itself. */
.editor-palette__item {
  justify-content: start;
  text-align: start;
  min-inline-size: 0;
  /* Same reasoning as the group's padding above, through button.css's own
     local API. A two-up palette button spends its width on the label, not on
     the gutters either side of it. */
  --button-padding-inline: var(--space-2);
  /* Stage 3: a palette item is a drag source (palette -> canvas insert). A
     touch press must not scroll the palette column instead of starting the
     drag JS owns — see the Drag section of editor.js. */
  touch-action: none;
}

/* Quiet at rest, edge on hover/focus (critique problem 3): ~40 identical
   outline-blue chips read as a form full of buttons rather than a tray of
   chips to pick up. editor.js now builds these with data-variant="quiet",
   so the base rule already starts transparent; the two re-points below are
   what put the edge back the moment a pointer or keyboard actually reaches
   one, using the same border/surface tokens quiet's own hover already
   reads elsewhere. `[data-variant]` on both selectors matches this file's
   own specificity, the same trick the Overlay section uses for the same
   reason: an unqualified `.editor-palette__item` here would lose to
   button.css's `[data-variant="quiet"]` rule on source order otherwise. */
.button.editor-palette__item,
.button.editor-palette__item[data-variant] {
  --button-border-hover: var(--color-border);
}

.editor-palette__item:focus-visible {
  border-color: var(--color-border);
}

.editor-palette__item .button__label {
  overflow-wrap: break-word;
}

/* One glyph per block type (critique problem 4), built in editor.js from a
   small editor-local map (PALETTE_ICONS) rather than src/icons/ — that set
   is closed and none of these glyphs ship on a published page, so they do
   not belong in it. Sized through button.css's own --size-icon-driven
   .button__icon part, not a new rule — --size-icon is 1em, so it already
   tracks the label's own font-size step (--text-sm on this button) with no
   override needed: see buildPaletteItem.

   Colour is --color-text, not --color-text-muted: quiet's own --button-fg
   already puts the LABEL at --color-text-muted, and painting the icon the
   identical shade left it reading as faint filler next to the word it names
   rather than as a glyph worth scanning for. A small thin-stroke mark needs
   more weight than a run of text to read at a glance, not the same weight —
   full text colour is the floor this scan established, not a ceiling to
   stay under. */
.editor-palette__item .button__icon {
  color: var(--color-text);
}

/* ---------------------------------------------------------------------------
   Inspector body
   --------------------------------------------------------------------------- */

/* --space-4, not -5: a denser vertical rhythm for a cockpit panel, not a
   marketing form — VISUAL_DENSITY is allowed to run high here (see the
   critique's own dial reading). The grouped clusters below carry their own
   internal --space-3, so the column now reads as tighter without any field
   losing its own breathing room. */
#inspector-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.editor-inspector__title {
  margin-block: var(--space-0);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  overflow-wrap: break-word;
}

.editor-inspector__empty {
  margin-block: var(--space-0);
}

.editor-inspector__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Move up / Move down / Delete reads as a DIFFERENT kind of row than the
   breadcrumbs above it (critique problem 7: both used to be rows of small
   blue text and were easy to mistake for one continuous action list). The
   breadcrumb trail is now muted nav text (Breadcrumbs section, below); real
   bordered buttons here already read as the heavier control by contrast,
   and this separator plus the standalone Delete treatment below reinforce
   it. A region separator between "where am I" and "what can I do", not a
   decoration on either. */
.editor-breadcrumbs + .editor-inspector__actions {
  padding-block-start: var(--space-2);
  border-block-start: var(--border-width-1) solid var(--color-border-subtle);
}

/* Delete stays the danger family (colour still carries the meaning) but
   quiet rather than a full solid slab — the smaller of the two treatments
   button.css already offers, re-pointed to danger's own text colour instead
   of adding a third variant. See makeActionButton's call site in editor.js. */
.editor-inspector__delete {
  --button-fg: var(--color-danger-text);
}

@media (hover: hover) {
  .editor-inspector__delete:hover {
    --button-bg-hover: var(--color-danger-subtle);
    color: var(--color-danger-text);
  }
}

/* --text-xs and sentence case, not the small-caps-style letter-spaced label
   button.css and menu.css both warn against (see either file's own note):
   uppercase runs wider still in a translation, and this column is narrow. Muted
   colour alone carries "this is a section label" the way it already does
   everywhere else in this package. */
.editor-inspector__section-label {
  margin-block: var(--space-0);
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width-1) solid var(--color-border-subtle);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--color-text-muted);
}

/* Groups related fields into one bounded region — column span with start
   column, a block's whole Props cluster — instead of one long flat list
   (critique problem 6). Deliberately the same box .editor-rows__row already
   draws for one row of loop/table data, reused rather than invented a
   second time: an even 1px border on all four sides, never a side accent,
   per rule 9. Its own first section-label drops the top divider above
   (:first-child, below) — a second line right under the group's own border
   would read as a stray rule, not a divider. */
.editor-inspector__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  border: var(--border-width-1) solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

.editor-inspector__group > .editor-inspector__section-label:first-child {
  padding-block-start: var(--space-0);
  border-block-start: none;
}

/* ---------------------------------------------------------------------------
   Breadcrumbs
   ---------------------------------------------------------------------------
   buildBreadcrumbs (editor.js) — the selected block's ancestor chain, root
   first. Each crumb is a real .button[data-variant="link"] (button.css),
   unmodified beyond its own local API; this file only lays out the <ol> and
   draws the decorative separator between crumbs. WRAPS rather than a single
   line: a block label ("Table of contents") repeated three or
   four crumbs deep does not reliably fit the inspector column on one line,
   and there is no truncation, clamp or nowrap anywhere in this package —
   the same rule .editor-rows__head already follows for its own
   row header just above. */
.editor-breadcrumbs__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  margin-block: var(--space-0);
  padding-inline-start: var(--space-0);
  list-style: none;
}

.editor-breadcrumbs__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
}

/* Muted by default, so the trail reads as location context rather than a
   second row of actions (critique problem 7 — every crumb used to inherit
   `.button[data-variant="link"]`'s own brand-coloured, underlined default,
   so "Section" and "Move up" below it read as the same kind of
   control). Re-points the crumb BUTTON's own local, not `color` on the
   `<li>` — `--button-fg` is what the component actually reads (button.css),
   so setting `color` here would be silently overridden by it. Brand colour
   is reserved for the CURRENT crumb (aria-current, below), matching the
   refresh's own direction: colour marks the one thing that matters, not
   every clickable thing. */
.editor-breadcrumbs__item .button[data-variant="link"] {
  --button-fg: var(--color-text-muted);
  text-decoration-line: none;
}

@media (hover: hover) {
  .editor-breadcrumbs__item .button[data-variant="link"]:hover {
    --button-fg: var(--color-text);
    text-decoration-line: underline;
  }
}

.editor-breadcrumbs__item .button[data-variant="link"][aria-current="true"] {
  --button-fg: var(--color-brand-text);
  font-weight: var(--weight-semibold);
}

.editor-breadcrumbs__sep {
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------------
   Gate / save report (stage 5/6)
   ---------------------------------------------------------------------------
   A flex child of .editor-body, sitting between the masthead and the shell.
   Empty by default (editor.html ships it with no children) and must then
   take no visible space — `:not(:empty)` carries every declaration that
   would otherwise reserve a band nobody asked for; `:empty` alone is a
   different, unrelated selector from the one this file's own rules forbid
   (see the same note elsewhere in this file and in cms.css — grepped for
   literally, so it is not named here either), so `:empty` is fine to use.
   The .alert children it holds bring their own spacing; this file only
   stacks more than one of them and gives the group its own edge against the
   shell.
   --------------------------------------------------------------------------- */

.editor-gate-report {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.editor-gate-report:not(:empty) {
  padding: var(--space-4) var(--space-6);
  border-block-end: var(--border-width-1) solid var(--color-border-subtle);
  background-color: var(--color-surface);
}

/* ---------------------------------------------------------------------------
   Canvas
   ---------------------------------------------------------------------------
   The stage is the resizable device frame: width presets set its
   max-inline-size in JS (a viewport-emulation measurement, not a design
   value — see editor.js) so the author can see cms.css's own documented
   collapse tiers (48rem / 64rem) actually happen, the same requirement that
   made the mid-tier collapse a stage-2 item in the first place.

   The preset numbers (375 / 900 / 1200) are stage widths, not the container
   query's own thresholds: cms.css's container is `.cms-section`, and that
   element carries its own `padding-inline: var(--space-5)` (the page
   gutter) — so the container's MEASURED inline-size is always 2×--space-5
   narrower than the stage. A stage set to exactly 768px therefore measures
   736px, still short of the 48rem/768px mid-tier boundary — which is
   correct and faithful: a real 768px browser window shows the identical
   result on the published page, gutter and all. 900/1200 are chosen with
   enough margin past 48rem/64rem once that gutter is subtracted that each
   preset reliably lands on one side of a boundary instead of teetering on
   it; 375 already sits far enough under 48rem that the gutter cannot matter.
   --------------------------------------------------------------------------- */

.editor-canvas-wrap {
  /* Positioned so the drag-to-remove bar can pin itself to this element's
     block-end edge — the canvas viewport — rather than to the stage, which
     scrolls. See the Drag-to-remove section. */
  position: relative;
  min-block-size: 0;
  overflow: hidden;
  /* A recessed tray, not a shade away from the panels either side of it.
     --color-surface-sunken alone measured only a few percent darker than
     --color-surface on a normal display — palette, canvas and inspector
     blurred into one flat plane (critique problem 2). One more mix step
     toward --state-shade (the same token every hover/active/selected state
     in this package already derives from) is a tokens-only fix: still the
     sunken surface, just committed to reading as a tray the page sits in,
     the way .editor-canvas-stage's own --shadow-lg below now lifts the page
     OFF it rather than merely sitting on a same-toned field. */
  background-color: color-mix(in oklab, var(--color-surface-sunken), var(--state-shade) 6%);
}

.editor-canvas-scroll {
  block-size: 100%;
  overflow-y: auto;
  /* overflow-x, not the logical overflow-inline: same call as carousel.css
     and tabs.css (see either file) — the logical longhand is Firefox-only
     and unimplemented in Chromium, so it sits below the floor this package
     promises. hidden, not auto: the stage is always sized to this element's
     own content box (max-inline-size: 100%, below), so sideways scroll was
     never a real feature here, only a symptom. `#canvas-scroll`'s
     scrollWidth can still measure a few px past its clientWidth — verified
     with every element's own getBoundingClientRect against this container's,
     nothing renders past it in either direction, so it is a rounding
     artifact of measuring a percentage-sized child inside an auto-scrolling
     ancestor, not a real overflow. `overflow-x: hidden` is what makes it
     inert either way: with no horizontal axis to scroll, the number left
     over cannot become a scrollbar or a pointer-draggable pan. */
  overflow-x: hidden;
  padding: var(--space-6);
}

.editor-canvas-stage {
  position: relative;
  inline-size: 100%;
  max-inline-size: 100%;
  margin-inline: auto;
  background-color: var(--color-surface);
  /* --shadow-lg, not -md: pairs with the canvas tray's own deeper recess
     above so the page visibly LIFTS off the tray it sits in, the way a real
     builder's stage does, rather than merely sitting on a same-toned field. */
  box-shadow: var(--shadow-lg);
  /* Resizing the stage is a deliberate, discrete action (a toolbar button),
     not a drag — so a short transition reads as a snap to the next device
     size rather than a jump cut, without implying continuous resize (that is
     stage 3/4's drag+resize handles, not this file's job). */
  transition: max-inline-size var(--transition-base);
}

/* ---------------------------------------------------------------------------
   Overlay
   ---------------------------------------------------------------------------
   One transparent box per addressable element (`[data-cms-section]` and
   `[data-cms-block]`), sized and positioned in JS from each element's real
   `getBoundingClientRect()` — never from an assumed inset — because
   `.card__header` bleeds past its own padding box with a negative inline
   margin and an assumed inset would be wrong for exactly that part. See the
   design note in editor.js.

   The layer itself is `inset: 0` against `.editor-canvas-stage` (its nearest
   positioned ancestor), so it exactly covers the rendered page regardless of
   scroll: both the layer and every block are read via getBoundingClientRect
   at the same instant, in the same viewport coordinate space, so their
   difference is scroll-independent by construction.
   --------------------------------------------------------------------------- */

.editor-overlay {
  position: absolute;
  inset: 0;
  /* The layer itself never catches a click — only the boxes inside it do —
     so an empty stage (no sections yet) does not swallow anything. */
  pointer-events: none;
  z-index: var(--z-overlay);
}

.editor-overlay__box {
  position: absolute;
  box-sizing: border-box;
  pointer-events: auto;
  cursor: pointer;
  /* Stage 3: every box is a potential drag source (section reorder, block
     move). Without this a touch press drags the PAGE (scroll) instead of
     the block, because the browser claims the gesture for scrolling before
     JS ever sees enough pointermove events to cross the click/drag
     threshold. `none` hands the whole gesture to pointer events, which is
     the point — see the Drag section of editor.js. */
  touch-action: none;
  /* Transparent fill: the overlay must never repaint the block it sits over,
     only intercept the pointer for it. The outline is the only visible
     signal, and it is drawn INSET (a negative offset) so it never nudges
     neighbouring boxes or the stage's own scroll extent. */
  background-color: transparent;
  border-radius: var(--radius-xs);
  outline: var(--border-width-2) solid transparent;
  outline-offset: calc(var(--border-width-2) * -1);
  transition:
    outline-color var(--transition-base),
    background-color var(--transition-base);
}

/* Focus is a different MARK from selection, not a different shade of it,
   because Delete on a box removes the FOCUSED block (editor.js,
   bindOverlayBoxKeyboard) and Tab moves focus without selecting. Selection is
   the thin brand outline below; focus is a 2px band in the text colour with
   a 2px --color-bg halo outside it (the same 2px + 2px as the system's own
   --focus-ring-width and --focus-ring-offset, base.css:228):

     selected           2px brand (inset, on the box edge)
     focused            2px text + 2px halo (outset, off the box edge)
     both               2px brand inset + 2px text/2px halo outset

   Outset, not inset: this box sits OVER the real rendered block (see the
   Overlay section header above and the hover/selected rules' own notes on
   the same point), so an inset band does not ring the content, it paints
   over it — measured: a just-clicked block is both selected and
   focus-visible, and an inset band there blanked the leading strokes of its
   heading and clipped its buttons. Drawn as ordinary (non-inset) box-shadows
   instead, which extend from the box's own edge outward and therefore never
   overlap the content the box sits on; the first (text) shadow paints on
   top of the second (halo) one, so the halo only shows in the 2-4px ring
   beyond it, same layering the old inset pair used, just flipped outward.

   WHY 2 + 2 AND NOT 4 + 2. Outward means the ring is painted over whatever
   sits next to the box, and the overlay layer is above the content, so a
   ring wider than the gap to the next block lands on that block's first
   pixels. Blocks sit 8px apart in a stack by default, but 6, 4 and 0 happen
   (a tight u-gap, a body paragraph directly over its list), and the old
   6px extent reached a block 4px away by 2px, measured. The extent is now 4px:
   it clears any gap of 4px or more and costs a 3px gap one pixel of
   bg-coloured halo. A gap of 0 cannot be cleared by any outward ring; see
   AT A 0PX GAP below for what is done instead. Two equal bands, not a thinner text band and a thinner halo: each
   colour is still 2 CSS px thick, which is the SC 2.4.13 minimum, and the
   pair is symmetric on purpose, so on an inverse section, where the text
   band vanishes, the halo alone is still a 2px indicator. The colours are
   unchanged, so contrast is unchanged (see DECISIONS for the measured pairs).
   The one thing given up is ink: the text band was 4px thick on an ordinary
   section and is 2px now.

   Text and bg are the two ends of the neutral ladder, so on any section fill
   one of the two bands clears 3:1 — on an inverse section the text band
   vanishes and the halo carries it. Solid for sections too, where the
   selection is dashed.

   Drawn on :focus-visible, and also on plain :focus when the box is not the
   selection: that is exactly when Delete would take a block the author did not
   pick, so the ring must show whatever the last input was. A clicked box is
   both; whether it also draws the ring is the browser's :focus-visible call
   (Chrome says yes: the box is focused from script, the pointerdown having
   been taken for the drag tracker), and either way nothing disagrees.

   AT A 0PX GAP (a body paragraph directly over its list or its link) the
   ring's 4px has to land on something: the block's own content or the
   neighbour's first pixels. Its own content is the inset draw that was
   measured blanking glyphs, and a thinner ring on that one side is a fainter
   mark exactly where two blocks are hardest to tell apart. So it lands on the
   neighbour, for as long as focus is there — and what made that ambiguous was
   never where it landed but what painted over it: the boxes are siblings in
   one stacking context, so a LATER neighbour's box drew its own outline and
   wash on top of the ring. A selected neighbour's 2px brand outline sits on
   exactly the 2px the text band occupies, so the focused block lost one whole
   side of its mark to the selection colour.

   Hence the ring is on ::after and raised. The box itself has no z-index and
   so forms no stacking context, which puts its ::after in .editor-overlay's
   and above every other box: all four sides always paint, whatever the
   neighbour's state. Raising the BOX instead would raise its hit area with
   it, and a focused section would then take every click meant for a block
   inside it; the pseudo-element takes no pointer events, so hit-testing and
   the resize handles are exactly as they were.

   Forced colours draw the same ::after as an outline; see the forced-colors
   block after the selection rule below. */
/* The ring draws outward, so a Tab scroll that brings only the box into view
   would leave the ring's outer side under the scroll edge. scroll-margin makes
   the browser scroll the ring's full width into view too. */
.editor-overlay__box {
  scroll-margin: calc(var(--border-width-2) + var(--border-width-2));
}

.editor-overlay__box:focus-visible::after,
.editor-overlay__box:focus:not([data-state="selected"])::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  border-radius: inherit;
  pointer-events: none;
  box-shadow:
    0 0 0 var(--border-width-2) var(--color-text),
    0 0 0 calc(var(--border-width-2) + var(--border-width-2)) var(--color-bg);
}

/* Hover was outline-only: the sole affordance that a block on a busy canvas
   is interactive, easy to miss on an imprecise pointer (critique problem
   5). A light fill alongside the existing outline change gives hover a
   second, larger signal.

   NOT --color-surface-hover. That token (and --color-surface-selected,
   below) is meant for an element that paints its own background — a
   button, a row — where there is nothing under it to lose. This box is
   NOT that: it is an absolutely-positioned layer sitting OVER the real
   rendered block (see the Overlay section header above), so an OPAQUE fill
   does not tint the content underneath, it replaces it — verified: with
   --color-surface-hover here, a selected heading rendered as a flat
   colour box with no visible text at all, because color-mix() between two
   fully opaque colours (--color-surface and --state-shade/--color-brand)
   is itself fully opaque. The fix is a wash, not a fill: color-mix()
   toward the `transparent` keyword instead of toward an opaque surface,
   reusing the SAME state-amount tokens (--state-hover-amount,
   --state-selected-amount below) those surface tokens already use for
   their own intensity, so the wash and this package's ordinary hover/
   selected states stay visually related without becoming the same
   declaration. */
@media (hover: hover) {
  .editor-overlay__box[data-target-kind="block"]:hover {
    outline-color: var(--color-border-strong);
    background-color: color-mix(in oklab, var(--color-border-strong) var(--state-hover-amount), transparent);
  }
}

.editor-overlay__box[data-target-kind="section"] {
  outline-style: dashed;
}

@media (hover: hover) {
  .editor-overlay__box[data-target-kind="section"]:hover {
    outline-color: var(--color-border);
  }
}

/* [data-target-kind] (matches either value) is not decoration: the two hover
   rules above are class + attribute + :hover = (0,3,0), so a plain
   `[data-state="selected"]` at (0,2,0) would lose specificity to :hover and
   a selected-and-hovered box would flash back to the hover colour. The extra
   attribute selector matches that specificity; placement after the hover
   rules then settles it by source order — a selected box stays the brand
   colour while hovered, whichever kind it is. */
.editor-overlay__box[data-target-kind][data-state="selected"] {
  outline-color: var(--color-brand);
  outline-width: var(--border-width-2);
  /* Selection must read as clearly stronger than the plain hover fill above
     it, not a deeper shade of the same grey — brand-derived, at roughly the
     same intensity --color-surface-selected mixes for an opaque surface
     (--state-selected-amount, the same token that drives it), but composited
     toward `transparent` here instead of toward --color-surface: this box
     sits OVER the real content (see the hover rule's own note just above),
     so an opaque fill hides the block's text rather than tinting it. A
     translucent brand wash reads as clearly stronger than the hover wash
     (12% vs 10%, and brand-coloured rather than neutral) while the block's
     own text stays fully legible through it. */
  background-color: color-mix(in oklab, var(--color-brand) var(--state-selected-amount), transparent);
}

/* Forced colours. Left to the UA, every box's transparent outline is forced
   to CanvasText, so every block on the canvas is outlined, a selected box
   looks exactly like an idle one, and the focus band (box-shadow) is dropped.
   Measured in Chrome's forced-colors emulation. So the boxes opt out of
   forcing and name system colours themselves:

     idle       nothing
     hover      2px GrayText
     selected   2px LinkText (dashed on a section, as everywhere)
     focused    4px solid Highlight — thicker, solid, and another colour

   The focus ring is the same raised ::after the ordinary ring is drawn on
   (see AT A 0PX GAP above), as an outline because a forced palette has no
   use for a halo: outset from the box edge to the same 4px extent, so it
   rings the block instead of painting over its content, and above every
   other box. It used to be the box's own outline pulled 4px inward, which
   covered the block's first 4px on every side and, an outline being one
   line, replaced the selection mark. A box that is both now shows both, as
   it does outside forced colours.
   The hover and selection selectors carry [data-target-kind] so they meet
   the unforced rules' specificity, and come after them to win on order. */
@media (forced-colors: active) {
  .editor-overlay__box {
    forced-color-adjust: none;
  }

  /* The property inherits; the resize handles inside a box keep the UA's
     forcing rather than showing their author hover tint. */
  .editor-overlay__box > * {
    forced-color-adjust: auto;
  }

  .editor-overlay__box[data-target-kind]:hover {
    outline-color: GrayText;
  }

  .editor-overlay__box[data-target-kind][data-state="selected"] {
    outline-color: LinkText;
  }

  .editor-overlay__box:focus-visible::after,
  .editor-overlay__box:focus:not([data-state="selected"])::after {
    box-shadow: none;
    outline: var(--border-width-4) solid Highlight;
  }
}

/* ---------------------------------------------------------------------------
   Drag ghost + drop indicator (stage 3)
   ---------------------------------------------------------------------------
   Both live in the same overlay layer as the selection boxes and are
   positioned in JS from the gesture's cached rect snapshot via the same
   stageOffsets() conversion the boxes use — see editor.js.

   Neither ever carries text. A block's translatable label is exactly the kind
   of string the long-text rule says must be free to wrap or must not appear
   at all; a cursor-following ghost and a one-pixel-thin indicator bar are
   the "must not appear at all" side of that choice, not an oversight.
   --------------------------------------------------------------------------- */

.editor-drag-ghost {
  position: absolute;
  pointer-events: none;
  z-index: var(--z-tooltip);
  box-sizing: border-box;
  border: var(--border-width-2) dashed var(--color-brand);
  border-radius: var(--radius-sm);
  /* A brand tint at the same mix amount the design system already uses for
     "this is the selected one" (color-surface-selected), reused rather than
     inventing a new derived color for "this is the one being dragged". */
  background-color: var(--color-surface-selected);
  box-shadow: var(--shadow-lg);
}

/* Palette items have no on-canvas rect to mirror, so their ghost is a fixed
   chip rather than a copy of a source box. */
.editor-drag-ghost[data-kind="palette"] {
  inline-size: var(--space-9);
  block-size: var(--space-9);
  border-radius: var(--radius-full);
}

.editor-drop-indicator {
  position: absolute;
  pointer-events: none;
  z-index: var(--z-popover);
  border-radius: var(--radius-full);
  background-color: var(--color-brand);
}

/* Section reorder: a full-width band boundary. JS sets inset-block-start and
   inline-size; the thickness below is the one dimension it deliberately
   leaves alone. */
.editor-drop-indicator[data-axis="block"] {
  block-size: var(--space-1);
}

/* Block move / palette insert: a row-height slot boundary. JS sets
   inset-inline-start, inset-block-start and block-size; same deal, mirrored
   onto the other axis. */
.editor-drop-indicator[data-axis="inline"] {
  inline-size: var(--space-1);
}

/* Column snap: the destination cell, not a seam. JS sets all four of
   inset-inline-start, inset-block-start, inline-size and block-size, because
   here every one of them is a real measurement — the column the pointer
   snapped to and the block's own span. Outlined rather than solid so the
   content underneath stays readable while the gesture is running; the fill
   and border reuse the drag ghost's own pair, so the ghost following the
   cursor and the slot it will land in read as the same object. */
.editor-drop-indicator[data-axis="cell"] {
  border: var(--border-width-2) solid var(--color-brand);
  border-radius: var(--radius-sm);
  background-color: var(--color-surface-selected);
}

/* ---------------------------------------------------------------------------
   Empty container or card — an editor-only affordance
   ---------------------------------------------------------------------------
   A container with no children is zero pixels tall, so there is nothing to
   aim a drop at: you can add one and then have no way to put anything in it.
   This gives it a floor and an outline until it holds something.

   THE ONE PLACE THIS FILE STYLES A .cms-* CLASS, and it is scoped so it can
   only ever apply here. `#cms-canvas` is the editor's own id on the render
   root (editor.html); render.js emits `<div class="cms-page">` with no id, so
   a published page cannot match this rule and gets no reserved space for a
   box that should not exist in it. That scoping is the whole reason the rule
   lives in editor.css rather than cms.css: an empty container is an
   authoring state, not a design element.

   Dashed on all four sides — this marks a region that can receive something,
   which is what a drop target is, and it is not a single-edge accent rule.
   --------------------------------------------------------------------------- */

#cms-canvas [data-cms-children]:empty {
  min-block-size: var(--space-9);
  border: var(--border-width-1) dashed var(--color-border-subtle);
  border-radius: var(--radius-sm);
}

/* A loop with nothing to repeat needs a box back before the rule above can
   give it a floor. cms.css makes every loop `display: contents` so its copies
   become items of the section grid (that is the whole of the repeater's layout
   code); a loop with no data list, or no template yet, therefore paints
   nothing at all and cannot be aimed at. This puts the box back for exactly
   that state — empty, in the editor canvas, never in a published page, which
   is why it is scoped to #cms-canvas like the rule above it.

   `display: block` rather than the initial value: an ID beats cms.css's
   `.cms-block[data-cms-block="loop"]` (0,2,0), and the loop's own data-span
   then puts it back on the grid, so the dashed box spans the row an author is
   about to fill. */
#cms-canvas [data-cms-block="loop"]:empty {
  display: block;
}

/* ---------------------------------------------------------------------------
   Drag-to-remove
   ---------------------------------------------------------------------------
   A bar that slides up from the block-end edge of the canvas while an
   existing block or section is being dragged. Two states, both set by
   editor.js on the element itself: [data-state="armed"] (a drag is running,
   the bar is offering itself) and [data-state="over"] (the pointer is inside
   it, so releasing will delete).

   WHY IT IS NOT `hidden`, AND NOT `visibility` EITHER. display:none cannot
   transition, and the slide is the whole point. The resting state is instead
   translated a full block-size past the block-end edge, where
   .editor-canvas-wrap's own `overflow: hidden` clips it completely.

   Hiding it is deliberately NOT expressed as `visibility`, even though that
   would also take it out of the hit-test: transitioned visibility only flips
   at the end of the transition, so anything that stops transitions from
   running — a background tab, a frozen renderer — would leave the bar
   permanently invisible while the pointer hit-test (which reads a rect, not a
   style) still resolved to it. That is a bar you can drop into but cannot
   see. Clipping by translate has no such failure mode: with the transition
   suppressed the bar simply appears and disappears instantly, which is
   exactly what the reduced-motion branch below wants anyway.

   The 1px block-start border is a REGION SEPARATOR, not a decorative accent
   rule: it divides the canvas above from the drop target below, which is the
   distinction hard rule 9 draws. The variant is carried by the fill and the
   text colour, exactly as the rule requires.

   It sits in .editor-canvas-wrap (which is position: relative for this) and
   not in the overlay, deliberately — the overlay scrolls with the stage and
   is rebuilt wholesale by layoutOverlay(); this bar must stay pinned to the
   canvas viewport for the length of a gesture. --------------------------- */

.editor-trash {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-tooltip);

  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  align-items: baseline;
  justify-content: center;
  padding: var(--space-4) var(--space-5);

  border-block-start: var(--border-width-1) solid var(--color-border-subtle);
  background-color: var(--color-surface-raised);
  color: var(--color-text-muted);

  translate: 0 100%;
  transition:
    translate var(--transition-base),
    background-color var(--transition-base),
    color var(--transition-base),
    border-color var(--transition-base);
}

.editor-trash[data-state="armed"],
.editor-trash[data-state="over"] {
  translate: 0 0;
}

/* Over the target: the danger family carries the meaning through fill, text
   and an even 1px edge — no accent bar, no icon-only signal. */
.editor-trash[data-state="over"] {
  border-block-start-color: var(--color-danger-border);
  background-color: var(--color-danger-subtle);
  color: var(--color-danger-text);
}

/* Long-text safe (rule 6): the label wraps whenever the canvas is narrow and
   is never clamped. A translated string runs longer; the flex-wrap above
   keeps room for a second label line if one returns. */
.editor-trash__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-align: center;
}

.editor-trash[data-state="over"] .editor-trash__label {
  font-weight: var(--weight-semibold);
}

/* The bar still appears and disappears under reduced motion — it just stops
   travelling to get there. Only the transition is dropped; the resting
   `translate: 0 100%` above is left alone deliberately, because that is what
   parks the bar outside the canvas rather than on top of it. Overriding it to
   0 here would leave an un-armed bar sitting over the block-end edge of the
   canvas, inert but occupying it. Matches how button.css suppresses its own
   transition. */
@media (prefers-reduced-motion: reduce) {
  .editor-trash {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
   Resize handles + badge (stage 4)
   ---------------------------------------------------------------------------
   Each handle is a child of the overlay box it resizes (editor.js's
   attachResizeHandles), positioned entirely with inset/100%/token lengths —
   no JS-computed pixel geometry is needed for the handle itself, only for
   what it drags. `inset: 0` and `100%` are both in the hard rules' literal
   allow-list; nothing here is a raw design value.

   Fluid gesture, quantized output: what a handle WRITES into `page` is a
   grid span, a --space-* step or an aspect-ratio, never a pixel — this file
   only places the drag targets, editor.js decides what dragging one means.

   aria-hidden + tabindex="-1" (set in JS): a handle is a pointer-only entry
   point. Every value it can produce is also reachable from the inspector
   (span already was; padding step and aspect-ratio gained inspector
   controls in this same change), so hiding these from the keyboard/AT tree
   costs no capability.
   --------------------------------------------------------------------------- */

.editor-resize-handle {
  position: absolute;
  pointer-events: auto;
  /* Same reason as .editor-overlay__box: without this a touch drag on a
     handle scrolls the page instead of resizing the block. */
  touch-action: none;
  background-color: transparent;
  transition: background-color var(--transition-base);
}

/* Side handle: a full-block-size strip at the block's inline-end edge. */
.editor-resize-handle[data-handle="span"] {
  inset-block-start: 0;
  inset-inline-end: 0;
  block-size: 100%;
  inline-size: var(--space-3);
  cursor: col-resize;
}

/* Top/bottom handle: a full-inline-size strip at the block-end edge. One
   handle rather than two, because padding-block and aspect-ratio are each a
   SINGLE value (padding-block sets both the block-start and block-end side
   together — src/utilities.css:158-167 — and an aspect-ratio has no
   independent top/bottom half), so a second handle at the block-start edge
   would just be a second way to write the same one value. */
.editor-resize-handle[data-handle="pad"],
.editor-resize-handle[data-handle="ratio"] {
  inset-inline-start: 0;
  inset-block-end: 0;
  inline-size: 100%;
  block-size: var(--space-3);
  cursor: row-resize;
}

@media (hover: hover) {
  .editor-resize-handle:hover {
    background-color: var(--color-surface-selected);
  }
}

/* Live value readout while a handle is being dragged — bare digits or a
   ratio string ("16/9"), never a translatable label, so the long-text
   no-nowrap rule this file follows everywhere else does not apply to it (same
   reasoning as the plain numeric span values already in the inspector's
   <select>). Positioned with the same cursor-following nudge as the drag
   ghost (editor.js's updateGhostPosition / positionResizeBadge). */
.editor-resize-badge {
  position: absolute;
  pointer-events: none;
  z-index: var(--z-tooltip);
  padding-block: var(--space-1);
  padding-inline: var(--space-3);
  border-radius: var(--radius-full);
  background-color: var(--color-brand);
  color: var(--color-text-inverse);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  box-shadow: var(--shadow-md);
}

/* ---------------------------------------------------------------------------
   Toolbar
   --------------------------------------------------------------------------- */

/* --masthead-min-block-size, not a raw block-size or flex-shrink: this file's
   own header says it never restyles .masthead beyond the local custom
   property API masthead.css already publishes, and this IS that API — the
   base rule reads it straight into min-block-size. It has to be re-pointed
   at all: .editor-body is a flex column with a FIXED block-size (100dvb, see
   its own note), and .masthead's default flex-shrink:1 with no floor beyond
   the token's own --space-9 means that once the wrapped toolbar below needs
   more than one row, the flex-shrink algorithm compresses the masthead back
   toward that small floor instead of growing to fit it — verified: at 375,
   4 wrapped toolbar rows measured 232px tall while the masthead's own
   rendered box stayed pinned to 64px, and the extra rows painted straight
   over the canvas underneath rather than pushing it down. `max-content`
   (not a fixed px guess) makes the floor equal to whatever the masthead's
   own content actually needs at ANY row count — 1 row at 1200px, 4+ at 375,
   however many a longer translated toolbar takes — so `.editor-shell`'s own
   `min-block-size: 0` (Page shell section) is what absorbs the deficit
   instead, exactly the same direction .editor-body's comment already argues
   for masthead height in general. */
.masthead {
  --masthead-min-block-size: max-content;
}

.editor-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
}

/* Region separators between toolbar groups (rule 9: this divides two
   groups, it does not decorate one control, so it is exempt from the
   accent-border ban the same way a divider or panel separator is). A
   border rather than relying on `gap` alone to imply grouping, so History /
   Canvas width / Save-and-publish read as three distinct clusters
   at a glance instead of one long row of identically-weighted buttons.
   `:not(:last-child)` rather than naming each group/button-group class so a
   future group needs no second rule here. */
.editor-toolbar > *:not(:last-child) {
  padding-inline-end: var(--space-4);
  border-inline-end: var(--border-width-1) solid var(--color-border-subtle);
}

.editor-toolbar__group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
}

/* Active/pressed state for a quiet toggle in the toolbar (the width
   presets): an inset underline rather than a filled
   background, so "this is the active one" reads without the quiet variant
   fighting Publish's own solid fill for attention (top problem 1 in the
   critique — every control read as the same weight). Neutral ink, not
   brand colour: colour on this page is reserved for the one primary
   action and for data, per the refresh's own direction. */
[data-variant="quiet"][aria-pressed="true"] {
  --button-fg: var(--color-text);
  font-weight: var(--weight-semibold);
  box-shadow: inset 0 calc(var(--border-width-2) * -1) 0 var(--color-text);
}

/* Compact width control (Narrow toolbar section below) mirrors the same
   pressed state on its own control shape — see that section. */
#undo-btn:disabled,
#redo-btn:disabled {
  opacity: var(--state-disabled-opacity);
  cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
   Narrow toolbar: compact width control
   ---------------------------------------------------------------------------
   Below ~600px the four discrete width-preset buttons (375 / 900 / 1200 /
   Full) are replaced by one `<select>` carrying the same options — critique
   problem 8: four wrapped toolbar rows at 390 already spend over a fifth of
   the viewport before any canvas is visible, and the preset GROUP is the one
   with headroom to fold, since History and Save/Publish
   are each already as small as a control can get. The select is real markup
   present at every width (see editor.html); which one is visible is a pure
   CSS swap, so there is nothing for editor.js to keep in sync on first paint
   — wireToolbar()'s applyStageWidth() (Toolbar section, editor.js) is the one
   function both controls call, so a change made through either stays true of
   both regardless of which was visible when it was made.

   37.5rem matches --size-container-sm's sibling tier rather than a bare
   guess — the same "just past a real breakpoint" reasoning the stage
   presets themselves use (see the Canvas section's long note) — chosen so
   the swap lands well clear of 390 and 768 alike instead of teetering on
   either. */
@media (width < 37.5rem) {
  .editor-toolbar__group--width {
    display: none;
  }
}

@media (width >= 37.5rem) {
  .editor-toolbar__preset-select-wrap {
    display: none;
  }
}

/* Re-points the field's own local API rather than a new control shape — the
   select is a real .field__control, just sized to sit in a dense toolbar
   the way a `data-size="sm"` button already does elsewhere in this row, and
   quiet at rest like the rest of this row's controls. field.css's own
   `--field-border-hover` (native :hover) and `--field-ring-color`
   (:focus-visible) are left at their defaults, so the edge still appears on
   hover and focus with no override needed here — only the resting state and
   the density change. */
.editor-toolbar__preset-select-wrap {
  --field-min-block-size: var(--size-control);
  --field-padding-inline: var(--space-3);
  --field-padding-block: var(--space-1);
  --field-font-size: var(--text-sm);
  --field-bg: transparent;
  --field-border: transparent;
  inline-size: auto;
}

/* This wrap's own --field-min-block-size above is unlayered, so it beats
   field.css's own layered `@media (pointer: coarse) .field` restore
   (convention 10, the same one button.css's coarse block follows) instead of
   losing to it. Repeat the restore here, scoped to this wrap, so the one
   control this rule shows only below 37.5rem (a phone) does not end up
   smaller than every other touch target in the same toolbar. */
@media (pointer: coarse) {
  .editor-toolbar__preset-select-wrap {
    --field-min-block-size: var(--size-touch);
  }
}

/* ---------------------------------------------------------------------------
   Inspector fields
   ---------------------------------------------------------------------------
   A text field in editor.js is a plain `.field-row` from field.css — "two
   or more fields side by side that collapse to a stack on their own" is
   exactly what one input per language needs, reused verbatim with no override
   here. The package ships English copy, so the row holds one input today.
   A long value gives up the row and takes the full width instead
   of being squeezed or clipped, with no media query. The inspector column
   (--size-card-min minus its own padding) is narrow enough that
   --field-row-min's default (--size-column-min) would keep a second language
   stacked here in practice — the same long-text-safe outcome, reached by the
   row's own logic rather than by a local override.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   Rows editor
   ---------------------------------------------------------------------------
   The list-of-rows inspector control (buildRowsField in editor.js). Every
   part inside a row is already a real component — `.field`, `.field-row`,
   `.button` — so the only thing left for this file is the stacking between
   them and the box that says where one row ends and the next begins.

   That box is an EVEN 1px border on all four sides, never a thick coloured
   rule down the inline-start edge: the accent-rule signature is banned
   outright by the house rules, and a row is a bounded region rather than two
   regions needing a divider between them.

   Nothing here sets an inline-size, a nowrap or a clamp. A cell is a
   `<textarea rows="2">`, so a long value grows the row instead of
   scrolling out of it, and the row grows the panel.
   --------------------------------------------------------------------------- */

.editor-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.editor-rows__row {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  border: var(--border-width-1) solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

/* wrap, because "Row 12" plus three labelled buttons does not reliably fit
   the inspector column on one line and must not try to. */
.editor-rows__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
}

.editor-rows__index {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-text-muted);
  overflow-wrap: break-word;
}

.editor-rows__actions,
.editor-rows__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* A cell grows with its value instead of scrolling it. `rows="2"` stays the
   floor (and keeps field.css's tall --size-textarea minimum off, which only
   applies to a textarea WITHOUT rows), and field-sizing takes it up from
   there — a 55-character string wraps to four lines and the row
   gets four lines tall.

   Support and why this is still inside the browser floor: field-sizing is
   Chrome/Edge 123+ (exactly the floor), Firefox 137+, Safari 26+. Where it is
   missing the cell falls back to the two-row box, which WRAPS and scrolls —
   the value is complete and reachable, there is no clamp, no ellipsis and no
   nowrap anywhere. That is a progressive enhancement, not a raised floor.

   Declared on this file's own class rather than on .field__control: the
   component's local custom property API has no "size to content" knob, and
   editor.css does not reach past that API into a component's internals. */
.editor-rows__cell {
  field-sizing: content;
}

.editor-rows__empty {
  margin-block: var(--space-0);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------------
   Icon picker
   ---------------------------------------------------------------------------
   buildIconField's body (editor.js): the fieldset, its legend and each
   .choice are the real field.css components, unmodified. The only thing
   .fieldset__body does not offer is a row that WRAPS instead of a column
   stack of twelve, so the body uses this class instead of .fieldset__body
   rather than reaching past its local custom property API (this file's own
   header forbids restyling a component beyond what it exposes). Each choice
   sizes to its own content — no fixed width, so a longer name still wraps
   the row rather than being clipped.
   --------------------------------------------------------------------------- */

.editor-icon-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* ---------------------------------------------------------------------------
   Preview
   ---------------------------------------------------------------------------
   The published document, in its own browsing context, over the editor.

   A NATIVE <dialog> OPENED WITH showModal(), so focus trapping, Escape, the
   inert background and top-layer stacking are the platform's problem and not
   this file's — the same argument dialog.css makes for the shipped component.
   `display` is scoped to [open] for the reason dialog.css states outright: the
   rule that hides a closed dialog is the UA's own `dialog:not([open])`, and
   any author declaration outranks it, so a bare `display: grid` here would
   render the preview permanently.

   AN IFRAME RATHER THAN A NEW TAB, decided after measuring: window.open()
   returned null even with a live user activation, because a popup blocker
   does not care. A preview that silently does nothing is worse than none. The
   frame also previews better — the page's own container queries measure
   against the frame's inline size, so what is shown is genuinely responsive
   rather than a fixed-width snapshot. "Open in new tab" is still offered in
   the bar, and says so when it is refused.
   --------------------------------------------------------------------------- */

.editor-preview {
  padding: var(--space-0);
  border: var(--border-width-1) solid var(--color-border);
  border-radius: var(--radius-lg);
  background-color: var(--color-surface);
  inline-size: min(100%, var(--size-container-2xl));
  block-size: min(100%, var(--size-container-xl));
  max-inline-size: none;
  max-block-size: none;
  overflow: hidden;
}

.editor-preview[open] {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
}

.editor-preview::backdrop {
  background-color: var(--color-overlay);
}

.editor-preview__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-block-end: var(--border-width-1) solid var(--color-border-subtle);
  background-color: var(--color-surface-raised);
}

.editor-preview__title {
  font-weight: var(--weight-semibold);
}

/* Pushes the two buttons to the inline end and carries the "new tab was
   blocked" line. min-inline-size: 0 so a long message wraps inside the bar
   instead of forcing it wider than the dialog. */
.editor-preview__note {
  flex: 1 1 auto;
  min-inline-size: 0;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.editor-preview__frame {
  border: 0;
  inline-size: 100%;
  block-size: 100%;
  background-color: var(--color-bg);
}
